This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch jbang-hardening in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6745b89eb93d85fdb2901a6f33c120dd8c6b6ae8 Author: Andrea Cosentino <[email protected]> AuthorDate: Thu Feb 5 14:31:28 2026 +0100 CAMEL-22960 - Camel-Jbang: Add an harden command and the related tool to camel-jbang-mcp Signed-off-by: Andrea Cosentino <[email protected]> --- .../pages/jbang-commands/camel-jbang-commands.adoc | 1 + .../pages/jbang-commands/camel-jbang-harden.adoc | 40 ++ .../partials/jbang-commands/examples/harden.adoc | 161 +++++ .../META-INF/camel-jbang-commands-metadata.json | 1 + .../dsl/jbang/core/commands/CamelJBangMain.java | 1 + .../camel/dsl/jbang/core/commands/Harden.java | 679 +++++++++++++++++++++ .../dsl/jbang/core/commands/mcp/HardenTools.java | 452 ++++++++++++++ .../src/main/resources/application.properties | 3 + 8 files changed, 1338 insertions(+) diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc index d519eb557fe6..179bbddbfaa8 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc @@ -23,6 +23,7 @@ TIP: You can also use `camel --help` or `camel <command> --help` to see availabl | xref:jbang-commands/camel-jbang-explain.adoc[camel explain] | Explain what a Camel route does using AI/LLM | xref:jbang-commands/camel-jbang-export.adoc[camel export] | Export to other runtimes (Camel Main, Spring Boot, or Quarkus) | xref:jbang-commands/camel-jbang-get.adoc[camel get] | Get status of Camel integrations +| xref:jbang-commands/camel-jbang-harden.adoc[camel harden] | Suggest security hardening for Camel routes using AI/LLM | xref:jbang-commands/camel-jbang-hawtio.adoc[camel hawtio] | Launch Hawtio web console | xref:jbang-commands/camel-jbang-infra.adoc[camel infra] | List and Run external services for testing and prototyping | xref:jbang-commands/camel-jbang-init.adoc[camel init] | Creates a new Camel integration diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-harden.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-harden.adoc new file mode 100644 index 000000000000..f1d0e9f1b7ca --- /dev/null +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-harden.adoc @@ -0,0 +1,40 @@ + +// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE += camel harden + +Suggest security hardening for Camel routes using AI/LLM + + +== Usage + +[source,bash] +---- +camel harden [options] +---- + + + +== Options + +[cols="2,5,1,2",options="header"] +|=== +| Option | Description | Default | Type +| `--api-key` | API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars | | String +| `--api-type` | API type: 'ollama' or 'openai' (OpenAI-compatible) | ollama | ApiType +| `--catalog-context` | Include Camel Catalog descriptions in the prompt | | boolean +| `--format` | Output format: text, markdown | text | String +| `--model` | Model to use | DEFAULT_MODEL | String +| `--show-prompt` | Show the prompt sent to the LLM | | boolean +| `--stream` | Stream the response as it's generated (shows progress) | true | boolean +| `--system-prompt` | Custom system prompt | | String +| `--temperature` | Temperature for response generation (0.0-2.0) | 0.7 | double +| `--timeout` | Timeout in seconds for LLM response | 120 | int +| `--url` | LLM API endpoint URL. Auto-detected from 'camel infra' for Ollama if not specified. | | String +| `--verbose,-v` | Include detailed security recommendations with code examples | | boolean +| `-h,--help` | Display the help and sub-commands | | boolean +|=== + + + +include::partial$jbang-commands/examples/harden.adoc[] + diff --git a/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/harden.adoc b/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/harden.adoc new file mode 100644 index 000000000000..266cc2f1a3a7 --- /dev/null +++ b/docs/user-manual/modules/ROOT/partials/jbang-commands/examples/harden.adoc @@ -0,0 +1,161 @@ +== Examples + +The `camel harden` command uses AI/LLM to analyze Camel routes and provide security hardening recommendations. +It supports multiple LLM providers including Ollama (local), OpenAI, Azure OpenAI, vLLM, LM Studio, and LocalAI. + +=== Prerequisites + +Start Ollama locally using Camel infra: + +[source,bash] +---- +camel infra run ollama +---- + +=== Basic Usage + +Analyze a YAML route for security issues: + +[source,bash] +---- +camel harden my-route.yaml +---- + +Analyze a Java route: + +[source,bash] +---- +camel harden OrderRoute.java +---- + +Analyze multiple route files: + +[source,bash] +---- +camel harden route1.yaml route2.xml MyRoute.java +---- + +=== Security Analysis Focus + +The harden command analyzes routes for these security concerns: + +* **Authentication & Authorization** - Missing or weak authentication, credential exposure +* **Encryption & Data Protection** - TLS/SSL configuration, data in transit security +* **Secrets Management** - Hardcoded credentials, vault integration recommendations +* **Input Validation & Injection Prevention** - SQL, command, and path traversal vulnerabilities +* **Secure Component Configuration** - Insecure defaults, missing security headers +* **Logging & Monitoring** - Sensitive data in logs, audit trail recommendations + +=== Output Options + +Use verbose mode for detailed recommendations with code examples: + +[source,bash] +---- +camel harden my-route.yaml --verbose +---- + +Output as Markdown for documentation: + +[source,bash] +---- +camel harden my-route.yaml --format=markdown +---- + +=== Prompt Options + +Include Camel Catalog descriptions for component-specific security advice: + +[source,bash] +---- +camel harden my-route.yaml --catalog-context +---- + +Show the prompt sent to the LLM (useful for debugging): + +[source,bash] +---- +camel harden my-route.yaml --show-prompt +---- + +Use a custom system prompt: + +[source,bash] +---- +camel harden my-route.yaml --system-prompt="Focus on OWASP Top 10 vulnerabilities." +---- + +=== LLM Configuration + +Use OpenAI or compatible services: + +[source,bash] +---- +camel harden my-route.yaml --url=https://api.openai.com --api-type=openai --api-key=sk-... +---- + +Use environment variables for the API key: + +[source,bash] +---- +export OPENAI_API_KEY=sk-... +camel harden my-route.yaml --url=https://api.openai.com --api-type=openai +---- + +Use a specific model: + +[source,bash] +---- +camel harden my-route.yaml --model=llama3.1:70b +---- + +=== Advanced Options + +Disable streaming (wait for complete response): + +[source,bash] +---- +camel harden my-route.yaml --stream=false +---- + +Adjust temperature (0.0 = deterministic, 2.0 = creative): + +[source,bash] +---- +camel harden my-route.yaml --temperature=0.3 +---- + +Set a custom timeout (in seconds): + +[source,bash] +---- +camel harden my-route.yaml --timeout=300 +---- + +=== Security Findings Severity Levels + +The harden command categorizes findings by severity: + +* **Critical** - Immediate security risks (command injection, hardcoded credentials, disabled TLS) +* **High** - Significant security concerns (HTTP instead of HTTPS, SQL injection risk, plain FTP) +* **Medium** - Moderate security issues (missing authentication hints, path validation concerns) +* **Low** - Minor security improvements (missing optional security headers) + +=== Example Workflow + +A typical security review workflow: + +[source,bash] +---- +# 1. First, understand what the route does +camel explain my-route.yaml + +# 2. Perform security analysis +camel harden my-route.yaml + +# 3. Get detailed recommendations with code examples +camel harden my-route.yaml --verbose --format=markdown + +# 4. Full analysis with catalog context +camel harden my-route.yaml --catalog-context --verbose +---- diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index bc094d14443a..49a490218e55 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -12,6 +12,7 @@ { "name": "explain", "fullName": "explain", "description": "Explain what a Camel route does using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", "javaTyp [...] { "name": "export", "fullName": "export", "description": "Export to other runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names": "--build-property", "description": "Maven\/Gradle build properties, ex. --build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { "names": "--build-tool", "description": "DEPRECATED: Build tool to use (maven or gradle) (gradle is deprecated)", "defaultV [...] { "name": "get", "fullName": "get", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean", "fullName": "get [...] + { "name": "harden", "fullName": "harden", "description": "Suggest security hardening for Camel routes using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...] { "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio web console", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ { "names": "--openUrl", "description": "To automatic open Hawtio web console in the web browser", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { "names": "--port", "description": "Port number to use for Hawtio web console (port 8888 by default)", "defaultValue": "8888", "javaType": "int", "t [...] { "name": "infra", "fullName": "infra", "description": "List and Run external services for testing and prototyping", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "infra [...] { "name": "init", "fullName": "init", "description": "Creates a new Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Init", "options": [ { "names": "--clean-dir,--clean-directory", "description": "Whether to clean directory first (deletes all files in directory)", "javaType": "boolean", "type": "boolean" }, { "names": "--dir,--directory", "description": "Directory relative path where the new Camel integration will be saved", "defaultValue": ".", "javaType" [...] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 885d8d490dfd..c62c219295e8 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -127,6 +127,7 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("dirty", new CommandLine(new Dirty(main))) .addSubcommand("export", new CommandLine(new Export(main))) .addSubcommand("explain", new CommandLine(new Explain(main))) + .addSubcommand("harden", new CommandLine(new Harden(main))) .addSubcommand("get", new CommandLine(new CamelStatus(main)) .addSubcommand("bean", new CommandLine(new CamelBeanDump(main))) .addSubcommand("blocked", new CommandLine(new ListBlocked(main))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java new file mode 100644 index 000000000000..244d03619b5b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java @@ -0,0 +1,679 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.util.FileUtil; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +/** + * Command to suggest security hardening improvements for Camel routes using AI/LLM services. + * <p> + * Analyzes routes for security vulnerabilities, authentication issues, encryption gaps, secrets management, input + * validation, and other security best practices. Supports multiple LLM providers: Ollama, OpenAI, Azure OpenAI, vLLM, + * LM Studio, LocalAI, etc. + */ +@Command(name = "harden", + description = "Suggest security hardening for Camel routes using AI/LLM", + sortOptions = false, showDefaultValues = true) +public class Harden extends CamelCommand { + + private static final String DEFAULT_OLLAMA_URL = "http://localhost:11434"; + private static final String DEFAULT_MODEL = "llama3.2"; + private static final int CONNECT_TIMEOUT_SECONDS = 10; + private static final int HEALTH_CHECK_TIMEOUT_SECONDS = 5; + + // Components with significant security considerations + private static final List<String> SECURITY_SENSITIVE_COMPONENTS = Arrays.asList( + // Network/API components - need TLS, authentication + "http", "https", "netty-http", "vertx-http", "websocket", + "rest", "rest-api", "platform-http", "servlet", "undertow", "jetty", + // Messaging - need authentication, encryption + "kafka", "jms", "activemq", "amqp", "rabbitmq", "pulsar", + "aws2-sqs", "aws2-sns", "aws2-kinesis", + "azure-servicebus", "azure-eventhubs", + "google-pubsub", + // File/Storage - need access control, path validation + "file", "ftp", "sftp", "ftps", + "aws2-s3", "azure-storage-blob", "azure-storage-queue", "azure-files", + "google-storage", "minio", + // Database - need authentication, SQL injection prevention + "sql", "jdbc", "mongodb", "couchdb", "cassandraql", + "elasticsearch", "opensearch", "redis", + // Email - need authentication, TLS + "smtp", "smtps", "imap", "imaps", "pop3", "pop3s", + // Remote execution - high risk, need strict validation + "exec", "ssh", "docker", + // Directory services - need secure binding + "ldap", "ldaps", + // Secrets management + "hashicorp-vault", "aws2-secrets-manager", "azure-key-vault", "google-secret-manager"); + + // Security-related categories + private static final List<String> OWASP_CATEGORIES = Arrays.asList( + "Injection (SQL, Command, LDAP, XPath)", + "Broken Authentication", + "Sensitive Data Exposure", + "XML External Entities (XXE)", + "Broken Access Control", + "Security Misconfiguration", + "Cross-Site Scripting (XSS)", + "Insecure Deserialization", + "Using Components with Known Vulnerabilities", + "Insufficient Logging & Monitoring"); + + enum ApiType { + ollama((harden, prompts) -> harden.callOllama(prompts[0], prompts[1], prompts[2])), + openai((harden, prompts) -> harden.callOpenAiCompatible(prompts[0], prompts[1], prompts[2], prompts[3])); + + private final BiFunction<Harden, String[], String> caller; + + ApiType(BiFunction<Harden, String[], String> caller) { + this.caller = caller; + } + + String call(Harden harden, String endpoint, String sysPrompt, String userPrompt, String apiKey) { + return caller.apply(harden, new String[] { endpoint, sysPrompt, userPrompt, apiKey }); + } + } + + @Parameters(description = "Route file(s) to analyze for security hardening", arity = "1..*") + List<String> files; + + @Option(names = { "--url" }, + description = "LLM API endpoint URL. Auto-detected from 'camel infra' for Ollama if not specified.") + String url; + + @Option(names = { "--api-type" }, + description = "API type: 'ollama' or 'openai' (OpenAI-compatible)", + defaultValue = "ollama") + ApiType apiType = ApiType.ollama; + + @Option(names = { "--api-key" }, + description = "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars") + String apiKey; + + @Option(names = { "--model" }, + description = "Model to use", + defaultValue = DEFAULT_MODEL) + String model = DEFAULT_MODEL; + + @Option(names = { "--verbose", "-v" }, + description = "Include detailed security recommendations with code examples") + boolean verbose; + + @Option(names = { "--format" }, + description = "Output format: text, markdown", + defaultValue = "text") + String format = "text"; + + @Option(names = { "--timeout" }, + description = "Timeout in seconds for LLM response", + defaultValue = "120") + int timeout = 120; + + @Option(names = { "--catalog-context" }, + description = "Include Camel Catalog descriptions in the prompt") + boolean catalogContext; + + @Option(names = { "--show-prompt" }, + description = "Show the prompt sent to the LLM") + boolean showPrompt; + + @Option(names = { "--temperature" }, + description = "Temperature for response generation (0.0-2.0)", + defaultValue = "0.7") + double temperature = 0.7; + + @Option(names = { "--system-prompt" }, + description = "Custom system prompt") + String systemPrompt; + + @Option(names = { "--stream" }, + description = "Stream the response as it's generated (shows progress)", + defaultValue = "true") + boolean stream = true; + + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)) + .build(); + + public Harden(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + String endpoint = detectEndpoint(); + if (endpoint == null) { + printUsageHelp(); + return 1; + } + + String resolvedApiKey = resolveApiKey(); + printConfiguration(endpoint, resolvedApiKey); + + for (String file : files) { + int result = hardenRoute(file, endpoint, resolvedApiKey); + if (result != 0) { + return result; + } + } + return 0; + } + + private void printConfiguration(String endpoint, String resolvedApiKey) { + printer().println("LLM Configuration:"); + printer().println(" URL: " + endpoint); + printer().println(" API Type: " + apiType); + printer().println(" Model: " + model); + printMaskedApiKey(resolvedApiKey); + printer().println(); + } + + private void printMaskedApiKey(String key) { + if (key == null || key.isBlank()) { + return; + } + String masked = "****" + key.substring(Math.max(0, key.length() - 4)); + printer().println(" API Key: " + masked); + } + + private void printUsageHelp() { + printer().printErr("LLM service is not running or not reachable."); + printer().printErr(""); + printer().printErr("Options:"); + printer().printErr(" 1. camel infra run ollama"); + printer().printErr(" 2. camel harden my-route.yaml --url=http://localhost:11434"); + printer().printErr(" 3. camel harden my-route.yaml --url=https://api.openai.com --api-type=openai --api-key=sk-..."); + } + + private String detectEndpoint() { + return tryExplicitUrl() + .or(this::tryInfraOllama) + .or(this::tryDefaultOllama) + .orElse(null); + } + + private Optional<String> tryExplicitUrl() { + if (url == null || url.isBlank()) { + return Optional.empty(); + } + if (isEndpointReachable(url)) { + return Optional.of(url); + } + printer().printErr("Cannot connect to LLM service at: " + url); + return Optional.empty(); + } + + private Optional<String> tryInfraOllama() { + try { + Map<Long, Path> pids = findOllamaPids(); + for (Path pidFile : pids.values()) { + String baseUrl = readBaseUrlFromPidFile(pidFile); + if (baseUrl != null && isEndpointReachable(baseUrl)) { + apiType = ApiType.ollama; + return Optional.of(baseUrl); + } + } + } catch (Exception e) { + // ignore + } + return Optional.empty(); + } + + private String readBaseUrlFromPidFile(Path pidFile) throws Exception { + String json = Files.readString(pidFile); + JsonObject jo = (JsonObject) Jsoner.deserialize(json); + return jo.getString("baseUrl"); + } + + private Optional<String> tryDefaultOllama() { + if (isEndpointReachable(DEFAULT_OLLAMA_URL)) { + apiType = ApiType.ollama; + return Optional.of(DEFAULT_OLLAMA_URL); + } + return Optional.empty(); + } + + private String resolveApiKey() { + if (apiKey != null && !apiKey.isBlank()) { + return apiKey; + } + return Stream.of("OPENAI_API_KEY", "LLM_API_KEY") + .map(System::getenv) + .filter(k -> k != null && !k.isBlank()) + .findFirst() + .orElse(null); + } + + private Map<Long, Path> findOllamaPids() throws Exception { + Map<Long, Path> pids = new HashMap<>(); + Path camelDir = CommandLineHelper.getCamelDir(); + + if (!Files.exists(camelDir)) { + return pids; + } + + try (Stream<Path> fileStream = Files.list(camelDir)) { + fileStream + .filter(this::isOllamaPidFile) + .forEach(p -> addPidEntry(pids, p)); + } + return pids; + } + + private boolean isOllamaPidFile(Path p) { + String name = p.getFileName().toString(); + return name.startsWith("infra-ollama-") && name.endsWith(".json"); + } + + private void addPidEntry(Map<Long, Path> pids, Path p) { + String name = p.getFileName().toString(); + String pidStr = name.substring(name.lastIndexOf("-") + 1, name.lastIndexOf('.')); + try { + pids.put(Long.valueOf(pidStr), p); + } catch (NumberFormatException e) { + // ignore + } + } + + private boolean isEndpointReachable(String endpoint) { + return tryHealthCheck(endpoint + "/api/tags") + || tryHealthCheck(endpoint + "/v1/models") + || tryHealthCheck(endpoint); + } + + private boolean tryHealthCheck(String healthUrl) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(healthUrl)) + .timeout(Duration.ofSeconds(HEALTH_CHECK_TIMEOUT_SECONDS)) + .GET() + .build(); + HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + return response.statusCode() == 200; + } catch (Exception e) { + return false; + } + } + + private int hardenRoute(String file, String endpoint, String resolvedApiKey) throws Exception { + Path path = Path.of(file); + if (!Files.exists(path)) { + printer().printErr("File not found: " + file); + return 1; + } + + String routeContent = Files.readString(path); + String ext = Optional.ofNullable(FileUtil.onlyExt(file, false)).orElse("yaml"); + + printFileHeader(file); + + String sysPrompt = buildSystemPrompt(); + String userPrompt = buildUserPrompt(routeContent, ext, file); + + printPromptsIfRequested(sysPrompt, userPrompt); + + String suggestions = apiType.call(this, endpoint, sysPrompt, userPrompt, resolvedApiKey); + + return handleHardeningResult(suggestions); + } + + private void printFileHeader(String file) { + printer().println("=".repeat(70)); + printer().println("Security Hardening Analysis: " + file); + printer().println("=".repeat(70)); + printer().println(); + } + + private void printPromptsIfRequested(String sysPrompt, String userPrompt) { + if (!showPrompt) { + return; + } + printer().println("--- SYSTEM PROMPT ---"); + printer().println(sysPrompt); + printer().println("--- USER PROMPT ---"); + printer().println(userPrompt); + printer().println("--- END PROMPTS ---"); + printer().println(); + } + + private int handleHardeningResult(String suggestions) { + if (suggestions == null) { + printer().printErr("Failed to get security hardening suggestions from LLM"); + return 1; + } + // With streaming, response was already printed during generation + // Without streaming, we need to print it now + if (!stream) { + printer().println(suggestions); + } + printer().println(); + return 0; + } + + private String buildSystemPrompt() { + if (systemPrompt != null && !systemPrompt.isBlank()) { + return systemPrompt; + } + + StringBuilder prompt = new StringBuilder(); + prompt.append("You are an Apache Camel security expert specializing in integration security. "); + prompt.append("Your task is to analyze Camel routes and provide security hardening recommendations.\n\n"); + + prompt.append("Focus on these security areas:\n"); + prompt.append("1. AUTHENTICATION & AUTHORIZATION\n"); + prompt.append(" - Missing or weak authentication mechanisms\n"); + prompt.append(" - Lack of authorization checks\n"); + prompt.append(" - API key and credential exposure\n\n"); + + prompt.append("2. ENCRYPTION & DATA PROTECTION\n"); + prompt.append(" - Data in transit (TLS/SSL configuration)\n"); + prompt.append(" - Data at rest encryption\n"); + prompt.append(" - Certificate validation\n\n"); + + prompt.append("3. SECRETS MANAGEMENT\n"); + prompt.append(" - Hardcoded credentials or secrets\n"); + prompt.append(" - Secrets in configuration files\n"); + prompt.append(" - Recommend vault integration (HashiCorp Vault, AWS Secrets Manager, etc.)\n\n"); + + prompt.append("4. INPUT VALIDATION & INJECTION PREVENTION\n"); + prompt.append(" - SQL injection vulnerabilities\n"); + prompt.append(" - Command injection risks\n"); + prompt.append(" - XML/JSON injection\n"); + prompt.append(" - Path traversal vulnerabilities\n\n"); + + prompt.append("5. SECURE COMPONENT CONFIGURATION\n"); + prompt.append(" - Insecure default settings\n"); + prompt.append(" - Missing security headers\n"); + prompt.append(" - Overly permissive configurations\n\n"); + + prompt.append("6. LOGGING & MONITORING\n"); + prompt.append(" - Sensitive data in logs\n"); + prompt.append(" - Missing audit trails\n"); + prompt.append(" - Security event monitoring\n\n"); + + prompt.append("Guidelines:\n"); + prompt.append("- Start with an executive summary of the security posture\n"); + prompt.append("- Prioritize findings by severity: Critical, High, Medium, Low\n"); + prompt.append("- For each finding, explain the risk and provide a specific remediation\n"); + prompt.append("- Reference OWASP guidelines where applicable\n"); + + if ("markdown".equals(format)) { + prompt.append("- Format output as Markdown with clear sections and code blocks\n"); + } + if (verbose) { + prompt.append("- Include specific code examples showing secure implementations\n"); + prompt.append("- Provide configuration snippets for recommended security settings\n"); + } + + return prompt.toString(); + } + + private String buildUserPrompt(String routeContent, String fileExtension, String fileName) { + StringBuilder prompt = new StringBuilder(); + + if (catalogContext) { + appendCatalogContext(prompt, routeContent); + } + + prompt.append("File: ").append(fileName).append("\n"); + prompt.append("Format: ").append(fileExtension.toUpperCase()).append("\n\n"); + prompt.append("Route definition:\n```").append(fileExtension).append("\n"); + prompt.append(routeContent).append("\n```\n\n"); + prompt.append("Please perform a security analysis of this Camel route and provide hardening recommendations:"); + + return prompt.toString(); + } + + private void appendCatalogContext(StringBuilder prompt, String routeContent) { + String catalogInfo = buildCatalogContext(routeContent); + if (catalogInfo.isEmpty()) { + return; + } + prompt.append("Security-relevant component information:\n").append(catalogInfo).append("\n"); + } + + private String buildCatalogContext(String routeContent) { + StringBuilder context = new StringBuilder(); + CamelCatalog catalog = new DefaultCamelCatalog(); + String lowerContent = routeContent.toLowerCase(); + + SECURITY_SENSITIVE_COMPONENTS.stream() + .filter(comp -> containsComponent(lowerContent, comp)) + .forEach(comp -> { + ComponentModel model = catalog.componentModel(comp); + if (model != null) { + context.append("- ").append(comp).append(": ").append(model.getDescription()); + String securityNote = getSecurityNote(comp); + if (securityNote != null) { + context.append(" [Security: ").append(securityNote).append("]"); + } + context.append("\n"); + } + }); + + return context.toString(); + } + + private String getSecurityNote(String component) { + return switch (component) { + case "http" -> "Prefer HTTPS; validate certificates; configure timeouts"; + case "https" -> "Verify TLS version >= 1.2; validate certificates"; + case "kafka" -> "Enable SASL authentication; use SSL; configure ACLs"; + case "sql", "jdbc" -> "Use parameterized queries to prevent SQL injection"; + case "file" -> "Validate file paths; prevent path traversal"; + case "ftp" -> "Prefer SFTP/FTPS over plain FTP"; + case "exec" -> "High risk - validate all inputs to prevent command injection"; + case "ssh" -> "Use key-based authentication; validate host keys"; + case "rest", "rest-api", "platform-http" -> + "Implement authentication; validate input; set security headers"; + case "ldap" -> "Use LDAPS; prevent LDAP injection"; + case "mongodb", "redis" -> "Enable authentication; use TLS"; + case "jms", "activemq", "amqp" -> "Enable authentication; use SSL/TLS"; + case "aws2-s3", "aws2-sqs", "aws2-sns" -> "Use IAM roles; enable server-side encryption"; + case "azure-storage-blob" -> "Use managed identities; enable encryption"; + case "smtp", "imap" -> "Use TLS (SMTPS/IMAPS); authenticate securely"; + default -> null; + }; + } + + private boolean containsComponent(String content, String comp) { + return content.contains(comp + ":") + || content.contains("\"" + comp + "\"") + || content.contains("'" + comp + "'"); + } + + String callOllama(String endpoint, String sysPrompt, String userPrompt) { + JsonObject request = new JsonObject(); + request.put("model", model); + request.put("prompt", userPrompt); + request.put("system", sysPrompt); + request.put("stream", stream); + + JsonObject options = new JsonObject(); + options.put("temperature", temperature); + request.put("options", options); + + printer().println("Performing security analysis with " + model + " (Ollama)..."); + printer().println(); + + if (stream) { + return sendStreamingRequest(endpoint + "/api/generate", request); + } + JsonObject response = sendRequest(endpoint + "/api/generate", request, null); + return response != null ? response.getString("response") : null; + } + + String callOpenAiCompatible(String endpoint, String sysPrompt, String userPrompt, String resolvedApiKey) { + JsonArray messages = new JsonArray(); + messages.add(createMessage("system", sysPrompt)); + messages.add(createMessage("user", userPrompt)); + + JsonObject request = new JsonObject(); + request.put("model", model); + request.put("messages", messages); + request.put("temperature", temperature); + + String apiUrl = normalizeOpenAiUrl(endpoint); + + printer().println("Performing security analysis with " + model + " (OpenAI-compatible)..."); + printer().println(); + + JsonObject response = sendRequest(apiUrl, request, resolvedApiKey); + return extractOpenAiContent(response); + } + + private JsonObject createMessage(String role, String content) { + JsonObject msg = new JsonObject(); + msg.put("role", role); + msg.put("content", content); + return msg; + } + + private String normalizeOpenAiUrl(String endpoint) { + String normalizedUrl = endpoint.endsWith("/") ? endpoint.substring(0, endpoint.length() - 1) : endpoint; + if (!normalizedUrl.endsWith("/v1/chat/completions")) { + normalizedUrl = normalizedUrl.endsWith("/v1") ? normalizedUrl : normalizedUrl + "/v1"; + normalizedUrl = normalizedUrl + "/chat/completions"; + } + return normalizedUrl; + } + + private String extractOpenAiContent(JsonObject response) { + if (response == null) { + return null; + } + JsonArray choices = (JsonArray) response.get("choices"); + if (choices == null || choices.isEmpty()) { + return null; + } + JsonObject firstChoice = (JsonObject) choices.get(0); + JsonObject message = (JsonObject) firstChoice.get("message"); + return message != null ? message.getString("content") : null; + } + + private String sendStreamingRequest(String requestUrl, JsonObject body) { + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJson())) + .build(); + + HttpResponse<Stream<String>> response = httpClient.send( + request, HttpResponse.BodyHandlers.ofLines()); + + if (response.statusCode() != 200) { + handleErrorStatus(response.statusCode(), "Streaming request failed"); + return null; + } + + StringBuilder fullResponse = new StringBuilder(); + response.body().forEach(line -> { + if (line.isBlank()) { + return; + } + try { + JsonObject chunk = (JsonObject) Jsoner.deserialize(line); + String text = chunk.getString("response"); + if (text != null) { + printer().print(text); + fullResponse.append(text); + } + } catch (Exception e) { + // Skip malformed chunks + } + }); + + printer().println(); + return fullResponse.toString(); + + } catch (java.net.http.HttpTimeoutException e) { + printer().printErr("\nRequest timed out after " + timeout + " seconds."); + return null; + } catch (Exception e) { + printer().printErr("\nError during streaming: " + e.getMessage()); + return null; + } + } + + private JsonObject sendRequest(String requestUrl, JsonObject body, String authKey) { + try { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(requestUrl)) + .timeout(Duration.ofSeconds(timeout)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body.toJson())); + + if (authKey != null && !authKey.isBlank()) { + builder.header("Authorization", "Bearer " + authKey); + } + + HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + return (JsonObject) Jsoner.deserialize(response.body()); + } + + handleErrorStatus(response.statusCode(), response.body()); + return null; + + } catch (java.net.http.HttpTimeoutException e) { + printer().printErr("Request timed out after " + timeout + " seconds."); + return null; + } catch (Exception e) { + printer().printErr("Error calling LLM: " + e.getMessage()); + return null; + } + } + + private void handleErrorStatus(int statusCode, String body) { + printer().printErr("LLM returned status: " + statusCode); + switch (statusCode) { + case 401 -> printer().printErr("Authentication failed. Check your API key."); + case 404 -> printer().printErr("Model '" + model + "' not found."); + case 429 -> printer().printErr("Rate limit exceeded."); + default -> printer().printErr(body); + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java new file mode 100644 index 000000000000..8832fd3c336a --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java @@ -0,0 +1,452 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.mcp; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.mcp.server.Tool; +import io.quarkiverse.mcp.server.ToolArg; +import io.quarkiverse.mcp.server.ToolCallException; +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +/** + * MCP Tool for providing security hardening context and analysis for Camel routes. + * <p> + * This tool analyzes routes for security-sensitive components, identifies potential vulnerabilities, and provides + * structured context that an LLM can use to formulate security hardening recommendations. + */ +@ApplicationScoped +public class HardenTools { + + // Components with significant security considerations + private static final List<String> SECURITY_SENSITIVE_COMPONENTS = Arrays.asList( + // Network/API components - need TLS, authentication + "http", "https", "netty-http", "vertx-http", "websocket", + "rest", "rest-api", "platform-http", "servlet", "undertow", "jetty", + // Messaging - need authentication, encryption + "kafka", "jms", "activemq", "amqp", "rabbitmq", "pulsar", + "aws2-sqs", "aws2-sns", "aws2-kinesis", + "azure-servicebus", "azure-eventhubs", + "google-pubsub", + // File/Storage - need access control, path validation + "file", "ftp", "sftp", "ftps", + "aws2-s3", "azure-storage-blob", "azure-storage-queue", "azure-files", + "google-storage", "minio", + // Database - need authentication, SQL injection prevention + "sql", "jdbc", "mongodb", "couchdb", "cassandraql", + "elasticsearch", "opensearch", "redis", + // Email - need authentication, TLS + "smtp", "smtps", "imap", "imaps", "pop3", "pop3s", + // Remote execution - high risk, need strict validation + "exec", "ssh", "docker", + // Directory services - need secure binding + "ldap", "ldaps", + // Secrets management + "hashicorp-vault", "aws2-secrets-manager", "azure-key-vault", "google-secret-manager"); + + private static final List<String> SECURITY_BEST_PRACTICES = Arrays.asList( + "Use TLS/SSL (version 1.2+) for all network communications", + "Store secrets in vault services (HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, etc.)", + "Use property placeholders for sensitive configuration values", + "Enable authentication for all endpoints and services", + "Validate and sanitize all input data to prevent injection attacks", + "Use parameterized queries for database operations", + "Implement proper certificate validation - do not disable SSL verification", + "Use principle of least privilege for service accounts and IAM roles", + "Enable audit logging for sensitive operations", + "Implement proper error handling without exposing internal details", + "Use HTTPS instead of HTTP for all external communications", + "Configure appropriate timeouts to prevent resource exhaustion", + "Validate file paths to prevent path traversal attacks", + "Use SFTP/FTPS instead of plain FTP"); + + private final CamelCatalog catalog; + + public HardenTools() { + this.catalog = new DefaultCamelCatalog(); + } + + /** + * Tool to get security hardening context for a Camel route. + */ + @Tool(description = "Get security hardening analysis context for a Camel route. " + + "Returns security-sensitive components, potential vulnerabilities, " + + "and security best practices. Use this context to provide security " + + "hardening recommendations for the route.") + public String camel_route_harden_context( + @ToolArg(description = "The Camel route content (YAML, XML, or Java DSL)") String route, + @ToolArg(description = "Route format: yaml, xml, or java (default: yaml)") String format) { + + if (route == null || route.isBlank()) { + throw new ToolCallException("Route content is required", null); + } + + String resolvedFormat = format != null && !format.isBlank() ? format.toLowerCase() : "yaml"; + + JsonObject result = new JsonObject(); + result.put("format", resolvedFormat); + result.put("route", route); + + // Analyze security-sensitive components + List<String> securityComponents = extractSecurityComponents(route); + JsonArray securityComponentsJson = new JsonArray(); + for (String comp : securityComponents) { + ComponentModel model = catalog.componentModel(comp); + if (model != null) { + JsonObject compJson = new JsonObject(); + compJson.put("name", comp); + compJson.put("title", model.getTitle()); + compJson.put("description", model.getDescription()); + compJson.put("label", model.getLabel()); + compJson.put("securityConsiderations", getSecurityConsiderations(comp)); + compJson.put("riskLevel", getRiskLevel(comp)); + securityComponentsJson.add(compJson); + } + } + result.put("securitySensitiveComponents", securityComponentsJson); + + // Security analysis + JsonObject securityAnalysis = analyzeSecurityConcerns(route); + result.put("securityAnalysis", securityAnalysis); + + // Best practices + JsonArray bestPractices = new JsonArray(); + for (String practice : SECURITY_BEST_PRACTICES) { + bestPractices.add(practice); + } + result.put("securityBestPractices", bestPractices); + + // Summary + JsonObject summary = new JsonObject(); + summary.put("securityComponentCount", securityComponentsJson.size()); + summary.put("criticalRiskComponents", countComponentsByRisk(securityComponents, "critical")); + summary.put("highRiskComponents", countComponentsByRisk(securityComponents, "high")); + summary.put("concernCount", securityAnalysis.getInteger("concernCount")); + summary.put("positiveCount", securityAnalysis.getInteger("positiveCount")); + summary.put("hasExternalConnections", hasExternalConnections(route)); + summary.put("hasSecretsManagement", hasSecretsManagement(route)); + summary.put("usesTLS", usesTLS(route)); + summary.put("hasAuthentication", hasAuthentication(route)); + result.put("summary", summary); + + return result.toJson(); + } + + /** + * Extract security-sensitive components from route content. + */ + private List<String> extractSecurityComponents(String route) { + List<String> found = new ArrayList<>(); + String lowerRoute = route.toLowerCase(); + + for (String comp : SECURITY_SENSITIVE_COMPONENTS) { + if (containsComponent(lowerRoute, comp)) { + found.add(comp); + } + } + + return found; + } + + /** + * Analyze security concerns in the route. + */ + private JsonObject analyzeSecurityConcerns(String route) { + JsonObject analysis = new JsonObject(); + JsonArray concerns = new JsonArray(); + JsonArray positives = new JsonArray(); + String lowerRoute = route.toLowerCase(); + + // Check for hardcoded credentials + if (containsHardcodedCredentials(lowerRoute)) { + JsonObject concern = new JsonObject(); + concern.put("severity", "critical"); + concern.put("category", "Secrets Management"); + concern.put("issue", "Potential hardcoded credentials detected"); + concern.put("recommendation", "Use property placeholders {{secret}} or vault services for credentials"); + concerns.add(concern); + } + + // Check for HTTP instead of HTTPS + if (lowerRoute.contains("http:") && !lowerRoute.contains("https:")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "high"); + concern.put("category", "Encryption"); + concern.put("issue", "Using HTTP instead of HTTPS"); + concern.put("recommendation", "Use HTTPS for secure communication. Configure TLS version 1.2 or higher"); + concerns.add(concern); + } + + // Check for plain FTP + if (lowerRoute.contains("ftp:") && !lowerRoute.contains("sftp:") && !lowerRoute.contains("ftps:")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "high"); + concern.put("category", "Encryption"); + concern.put("issue", "Using plain FTP instead of SFTP/FTPS"); + concern.put("recommendation", "Use SFTP or FTPS for encrypted file transfers"); + concerns.add(concern); + } + + // Check for SSL/TLS disabled + if (lowerRoute.contains("sslcontextparameters") && lowerRoute.contains("false")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "critical"); + concern.put("category", "Encryption"); + concern.put("issue", "SSL/TLS may be disabled or misconfigured"); + concern.put("recommendation", "Ensure SSL/TLS is properly enabled and configured"); + concerns.add(concern); + } + + // Check for exec component (high risk) + if (lowerRoute.contains("exec:")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "critical"); + concern.put("category", "Command Injection"); + concern.put("issue", "Using exec component - high risk for command injection"); + concern.put("recommendation", + "Validate all inputs strictly. Consider if exec is really necessary or if safer alternatives exist"); + concerns.add(concern); + } + + // Check for SQL without parameterized queries indicator + if (lowerRoute.contains("sql:") && !lowerRoute.contains(":#") && !lowerRoute.contains(":?")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "high"); + concern.put("category", "SQL Injection"); + concern.put("issue", "SQL query may not use parameterized queries"); + concern.put("recommendation", "Use parameterized queries with named parameters (:#param) or positional (:?)"); + concerns.add(concern); + } + + // Check for LDAP injection risk + if (lowerRoute.contains("ldap:") && !lowerRoute.contains("ldaps:")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "medium"); + concern.put("category", "Encryption"); + concern.put("issue", "Using LDAP instead of LDAPS"); + concern.put("recommendation", "Use LDAPS for encrypted LDAP communication"); + concerns.add(concern); + } + + // Check for file component path validation + if (lowerRoute.contains("file:") && lowerRoute.contains("${")) { + JsonObject concern = new JsonObject(); + concern.put("severity", "medium"); + concern.put("category", "Path Traversal"); + concern.put("issue", "File path contains dynamic expression - potential path traversal risk"); + concern.put("recommendation", "Validate file paths and restrict to allowed directories"); + concerns.add(concern); + } + + // POSITIVE CHECKS + + // Check for TLS/SSL usage + if (usesTLS(lowerRoute)) { + JsonObject positive = new JsonObject(); + positive.put("category", "Encryption"); + positive.put("finding", "TLS/SSL encryption is configured"); + positives.add(positive); + } + + // Check for property placeholders (good practice) + if (lowerRoute.contains("{{") && lowerRoute.contains("}}")) { + JsonObject positive = new JsonObject(); + positive.put("category", "Secrets Management"); + positive.put("finding", "Using property placeholders for configuration"); + positives.add(positive); + } + + // Check for vault integration + if (hasSecretsManagement(lowerRoute)) { + JsonObject positive = new JsonObject(); + positive.put("category", "Secrets Management"); + positive.put("finding", "Integrated with secrets management service"); + positives.add(positive); + } + + // Check for authentication configuration + if (hasAuthentication(lowerRoute)) { + JsonObject positive = new JsonObject(); + positive.put("category", "Authentication"); + positive.put("finding", "Authentication appears to be configured"); + positives.add(positive); + } + + // Check for HTTPS usage + if (lowerRoute.contains("https:")) { + JsonObject positive = new JsonObject(); + positive.put("category", "Encryption"); + positive.put("finding", "Using HTTPS for secure HTTP communication"); + positives.add(positive); + } + + // Check for SFTP/FTPS usage + if (lowerRoute.contains("sftp:") || lowerRoute.contains("ftps:")) { + JsonObject positive = new JsonObject(); + positive.put("category", "Encryption"); + positive.put("finding", "Using secure file transfer protocol (SFTP/FTPS)"); + positives.add(positive); + } + + analysis.put("concerns", concerns); + analysis.put("positives", positives); + analysis.put("concernCount", concerns.size()); + analysis.put("positiveCount", positives.size()); + + return analysis; + } + + /** + * Get security considerations for a specific component. + */ + private String getSecurityConsiderations(String component) { + return switch (component) { + case "http" -> + "Prefer HTTPS over HTTP. Validate certificates. Configure appropriate timeouts. Set security headers."; + case "https" -> + "Verify TLS version is 1.2 or higher. Enable certificate validation. Configure secure cipher suites."; + case "kafka" -> + "Enable SASL authentication (SCRAM-SHA-256/512 or GSSAPI). Use SSL for encryption. Configure ACLs for authorization."; + case "sql", "jdbc" -> + "Use parameterized queries to prevent SQL injection. Limit database user privileges. Enable connection encryption."; + case "file" -> + "Validate file paths to prevent traversal attacks. Restrict directory access. Set appropriate file permissions."; + case "ftp" -> + "INSECURE: Use SFTP or FTPS instead. Plain FTP transmits credentials in cleartext."; + case "sftp" -> + "Use key-based authentication. Validate host keys. Configure known_hosts file."; + case "ftps" -> + "Enable explicit FTPS. Verify server certificates. Use strong TLS version."; + case "exec" -> + "HIGH RISK: Validate and sanitize all inputs to prevent command injection. Consider safer alternatives."; + case "ssh" -> + "Use key-based authentication. Validate host keys. Disable password authentication if possible."; + case "rest", "rest-api", "platform-http" -> + "Implement authentication (OAuth2, JWT, API keys). Validate all input. Set CORS policies. Add security headers."; + case "ldap" -> + "Use LDAPS for encryption. Escape special characters to prevent LDAP injection. Use service account with minimal privileges."; + case "ldaps" -> + "Verify server certificates. Use strong TLS. Escape special characters in queries."; + case "mongodb" -> + "Enable authentication. Use TLS for connections. Limit network exposure. Use SCRAM authentication."; + case "redis" -> + "Enable authentication (requirepass or ACL). Use TLS. Limit network exposure. Disable dangerous commands."; + case "jms", "activemq", "amqp", "rabbitmq" -> + "Enable authentication. Use SSL/TLS for connections. Configure authorization policies."; + case "aws2-s3", "aws2-sqs", "aws2-sns", "aws2-kinesis" -> + "Use IAM roles instead of access keys. Enable server-side encryption. Configure bucket/queue policies."; + case "aws2-secrets-manager" -> + "Use IAM roles for access. Enable automatic rotation. Audit secret access."; + case "azure-storage-blob", "azure-storage-queue", "azure-files" -> + "Use managed identities. Enable encryption at rest. Configure access policies."; + case "azure-key-vault" -> + "Use managed identities. Enable soft-delete. Configure access policies and RBAC."; + case "google-storage", "google-pubsub" -> + "Use service accounts with minimal permissions. Enable encryption. Configure IAM policies."; + case "google-secret-manager" -> + "Use service accounts. Enable automatic rotation. Audit access."; + case "hashicorp-vault" -> + "Use AppRole or Kubernetes auth. Configure token TTLs. Enable audit logging."; + case "elasticsearch", "opensearch" -> + "Enable authentication. Use TLS. Configure role-based access control."; + case "smtp", "smtps", "imap", "imaps", "pop3", "pop3s" -> + "Use TLS variants (SMTPS, IMAPS, POP3S). Use secure authentication. Store credentials securely."; + case "websocket" -> + "Use WSS (WebSocket Secure). Implement authentication. Validate origin headers."; + case "docker" -> + "HIGH RISK: Validate all inputs. Use least privilege. Consider container security policies."; + case "netty-http", "vertx-http", "undertow", "jetty", "servlet" -> + "Enable TLS. Implement authentication. Set security headers. Validate input."; + case "pulsar" -> + "Enable TLS encryption. Configure authentication (JWT, Athenz). Set authorization policies."; + case "minio" -> + "Enable TLS. Use access/secret keys securely. Configure bucket policies."; + case "couchdb", "cassandraql" -> + "Enable authentication. Use TLS for connections. Configure role-based access."; + default -> "Review security configuration for this component"; + }; + } + + /** + * Get risk level for a component. + */ + private String getRiskLevel(String component) { + return switch (component) { + case "exec", "docker" -> "critical"; + case "http", "ftp", "ldap", "sql", "jdbc" -> "high"; + case "file", "ssh", "rest", "rest-api", "platform-http", "kafka", "mongodb", "redis" -> "medium"; + default -> "low"; + }; + } + + private int countComponentsByRisk(List<String> components, String riskLevel) { + return (int) components.stream() + .filter(c -> riskLevel.equals(getRiskLevel(c))) + .count(); + } + + private boolean containsHardcodedCredentials(String route) { + return (route.contains("password=") || route.contains("password:")) + && !route.contains("{{") && !route.contains("$"); + } + + private boolean containsComponent(String content, String comp) { + return content.contains(comp + ":") + || content.contains("\"" + comp + "\"") + || content.contains("'" + comp + "'"); + } + + private boolean hasExternalConnections(String route) { + String lowerRoute = route.toLowerCase(); + return lowerRoute.contains("http:") || lowerRoute.contains("https:") + || lowerRoute.contains("kafka:") || lowerRoute.contains("jms:") + || lowerRoute.contains("sql:") || lowerRoute.contains("mongodb:") + || lowerRoute.contains("aws2-") || lowerRoute.contains("azure-"); + } + + private boolean hasSecretsManagement(String route) { + String lowerRoute = route.toLowerCase(); + return lowerRoute.contains("hashicorp-vault") || lowerRoute.contains("aws-secrets-manager") + || lowerRoute.contains("aws2-secrets-manager") + || lowerRoute.contains("azure-key-vault") || lowerRoute.contains("google-secret-manager"); + } + + private boolean usesTLS(String route) { + String lowerRoute = route.toLowerCase(); + return lowerRoute.contains("ssl=true") || lowerRoute.contains("usessl=true") + || lowerRoute.contains("https:") || lowerRoute.contains("sftp:") + || lowerRoute.contains("ftps:") || lowerRoute.contains("ldaps:") + || lowerRoute.contains("smtps:") || lowerRoute.contains("imaps:") + || lowerRoute.contains("sslcontextparameters"); + } + + private boolean hasAuthentication(String route) { + String lowerRoute = route.toLowerCase(); + return lowerRoute.contains("username=") || lowerRoute.contains("authmethod=") + || lowerRoute.contains("saslmechanism=") || lowerRoute.contains("oauth") + || lowerRoute.contains("bearer") || lowerRoute.contains("apikey") + || lowerRoute.contains("securityprovider"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties b/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties index 16a9e79de4f8..bc542097a290 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/resources/application.properties @@ -29,3 +29,6 @@ quarkus.log.console.stderr=true quarkus.log.level=WARN quarkus.log.category."org.apache.camel".level=INFO quarkus.log.category."io.quarkiverse.mcp".level=INFO + +# Disable HTTP server - use STDIO transport only for CLI integration +quarkus.http.host-enabled=false
