gnodet commented on code in PR #25026:
URL: https://github.com/apache/camel/pull/25026#discussion_r3645625205


##########
dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanToolsTest.java:
##########
@@ -0,0 +1,363 @@
+/*
+ * 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 io.quarkiverse.mcp.server.ToolCallException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SecurityScanToolsTest {
+
+    private final SecurityScanTools tools = new SecurityScanTools();
+
+    @Test
+    void nullRouteThrowsException() {
+        assertThatThrownBy(() -> tools.camel_security_scan(null, null))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("required");
+    }
+
+    @Test
+    void blankRouteThrowsException() {
+        assertThatThrownBy(() -> tools.camel_security_scan("  ", null))
+                .isInstanceOf(ToolCallException.class);
+    }
+
+    @Test
+    void cleanRouteProducesNoFindings() {
+        String route = """
+                - route:
+                    from:
+                      uri: timer:tick
+                    steps:
+                      - log: "Hello"
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.totalFindings()).isZero();
+        assertThat(result.format()).isEqualTo("yaml");
+    }
+
+    @Test
+    void detectsTrustAllCertificates() {
+        String route = """
+                - route:
+                    from:
+                      uri: https://api.example.com?trustAllCertificates=true
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.issue().contains("trustallcertificates")
+                && f.category().equals("insecure:ssl"));
+    }
+
+    @Test
+    void detectsAllowJavaSerializedObject() {
+        String route = """
+                - route:
+                    from:
+                      uri: jms:queue:orders?allowJavaSerializedObject=true
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.issue().contains("allowjavaserializedobject")
+                && f.category().equals("insecure:serialization")
+                && f.severity().equals("critical"));
+    }
+
+    @Test
+    void detectsTransferExceptionEnabled() {
+        String route = """
+                - route:
+                    from:
+                      uri: netty-http:0.0.0.0:8080?transferException=true
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.issue().contains("transferexception"));
+    }
+
+    @Test
+    void detectsPlainTextPasswordInUri() {
+        String route = """
+                - route:
+                    from:
+                      uri: kafka:topic?password=mysecret123&brokers=localhost
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.category().equals("secret")
+                && f.severity().equals("critical")
+                && f.issue().contains("password"));
+    }
+
+    @Test
+    void doesNotFlagPlaceholderPassword() {
+        String route = """
+                - route:
+                    from:
+                      uri: 
kafka:topic?password={{kafka.password}}&brokers=localhost
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).noneMatch(f -> 
f.category().equals("secret")
+                && f.issue().contains("password"));
+    }
+
+    @Test
+    void detectsConnectionStringWithCredentials() {
+        String route = """
+                - route:
+                    from:
+                      uri: mongodb:myDb?connectionBean=#mongoClient
+                    steps:
+                      - setBody:
+                          constant: "mongodb://admin:secret@host:27017/db"
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.issue().contains("Connection string with embedded credentials"));
+    }
+
+    @Test
+    void detectsHttpInsteadOfHttps() {
+        String route = """
+                - route:
+                    from:
+                      uri: http://api.example.com/data
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> f.issue().contains("HTTP 
instead of HTTPS")
+                && f.severity().equals("high"));
+    }
+
+    @Test
+    void doesNotFlagHttpsAsInsecure() {
+        String route = """
+                - route:
+                    from:
+                      uri: https://api.example.com/data
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).noneMatch(f -> f.issue().contains("HTTP 
instead of HTTPS"));
+    }
+
+    @Test
+    void detectsPlainFtp() {
+        String route = """
+                - route:
+                    from:
+                      uri: ftp://server/files
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> f.issue().contains("plain 
FTP"));
+    }
+
+    @Test
+    void doesNotFlagSftp() {
+        String route = """
+                - route:
+                    from:
+                      uri: sftp://server/files
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).noneMatch(f -> f.issue().contains("plain 
FTP"));
+    }
+
+    @Test
+    void detectsMissingHeaderFilterOnConsumer() {
+        String route = """
+                - route:
+                    from:
+                      uri: platform-http:/api/data
+                    steps:
+                      - log: "received"
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.category().equals("header-injection")
+                && f.issue().contains("platform-http"));
+    }
+
+    @Test
+    void headerFilterPresentSuppressesFinding() {
+        String route = """
+                - route:
+                    from:
+                      uri: platform-http:/api/data
+                    steps:
+                      - removeHeaders: "Camel*"
+                      - log: "received"
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).noneMatch(f -> 
f.category().equals("header-injection"));
+    }
+
+    @Test
+    void detectsExecComponent() {
+        String route = """
+                - route:
+                    from:
+                      uri: direct:run
+                    steps:
+                      - to: exec:ls
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.category().equals("command-injection")
+                && f.severity().equals("critical"));
+    }
+
+    @Test
+    void detectsSqlWithoutParameterizedQueries() {
+        String route = """
+                - route:
+                    from:
+                      uri: "sql:SELECT * FROM users WHERE name = ${body}"
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.category().equals("sql-injection"));
+    }
+
+    @Test
+    void detectsFilePathTraversal() {
+        String route = """
+                - route:
+                    from:
+                      uri: "file:/data/${header.dir}"
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.category().equals("path-traversal"));
+    }
+
+    @Test
+    void findingsAreSortedBySeverity() {
+        String route = """
+                - route:
+                    from:
+                      uri: 
http://api.example.com?password=secret&allowJavaSerializedObject=true
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).isNotEmpty();
+        for (int i = 1; i < result.findings().size(); i++) {
+            String prev = result.findings().get(i - 1).severity();
+            String curr = result.findings().get(i).severity();
+            assertThat(severityRank(prev))
+                    .as("findings should be sorted by severity: %s before %s", 
prev, curr)
+                    .isLessThanOrEqualTo(severityRank(curr));
+        }
+    }
+
+    @Test
+    void severityCountsAreAccurate() {
+        String route = """
+                - route:
+                    from:
+                      uri: 
http://api.example.com?password=secret&allowJavaSerializedObject=true
+                    steps:
+                      - to: exec:cmd
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.severityCounts().critical()).isGreaterThan(0);
+        assertThat(result.totalFindings())
+                .isEqualTo(result.severityCounts().critical() + 
result.severityCounts().high()
+                           + result.severityCounts().medium() + 
result.severityCounts().low());
+    }
+
+    @Test
+    void defaultFormatIsYaml() {
+        String route = "from: timer:tick";
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, null);
+        assertThat(result.format()).isEqualTo("yaml");
+    }
+
+    @Test
+    void detectsUnencryptedSmtp() {
+        String route = """
+                - route:
+                    from:
+                      uri: smtp://mail.example.com
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> f.issue().contains("SMTP 
instead of SMTPS"));
+    }
+
+    @Test
+    void detectsDevConsoleEnabled() {
+        String route = """
+                - route:
+                    from:
+                      uri: timer:tick?devConsoleEnabled=true
+                """;
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings()).anyMatch(f -> 
f.category().equals("insecure:dev"));
+    }
+
+    @Test
+    void lineNumbersAreCorrect() {
+        String route = "line1\nline2\npassword=mysecret\nline4";
+
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings())
+                .filteredOn(f -> f.category().equals("secret"))
+                .allMatch(f -> f.line() == 3);
+    }
+
+    private static int severityRank(String severity) {
+        return switch (severity) {
+            case "critical" -> 0;
+            case "high" -> 1;
+            case "medium" -> 2;
+            case "low" -> 3;
+            default -> 4;
+        };
+    }
+}

Review Comment:
   **Medium:** The 4 bug fixes from this commit lack dedicated regression 
tests. Per project rules, every PR must include tests for bug fixes. 
Specifically:
   
   1. Test that `password=RAW(mysecret)` IS detected as a plain-text secret 
(existing `detectsPlainTextPasswordInUri` uses `password=mysecret123` which 
doesn't exercise the RAW() path)
   2. Test that `sslEndpointAlgorithm=HTTPS` is NOT flagged (empty 
`insecureValue` case)
   3. Test that `google-bigquery-sql:project:dataset` is NOT flagged as SQL 
injection (composite scheme)
   4. Test with two consumers where only one has a header filter, verifying the 
unprotected one is still flagged
   
   Also: no test for LDAP/LDAPS detection, unlike the other protocol pairs 
(HTTP/FTP/SMTP).



##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java:
##########
@@ -0,0 +1,356 @@
+/*
+ * 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.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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.util.SecurityUtils;
+
+/**
+ * MCP Tool for static security analysis of Camel route definitions.
+ * <p>
+ * Scans route content (YAML, XML, or Java DSL) for security anti-patterns 
defined in the Camel security model
+ * ({@code design/security.adoc}). Produces actionable findings with severity, 
line location, and remediation guidance.
+ * <p>
+ * Distinct from {@link HardenTools} which provides general security context 
and CVE advisories — this tool performs
+ * line-by-line static analysis to find specific violations.
+ */
+@ApplicationScoped
+public class SecurityScanTools {
+
+    private static final Pattern SECRET_IN_URI = Pattern.compile(
+            
"(?i)(password|passwd|pwd|secret|token|apikey|api[_-]?key|access[_-]?key)\\s*[=:]\\s*(?!\\{\\{)(?!\\$\\{)([^\\s,;}'\"\\]&]+)");
+
+    private static final Pattern CONNECTION_STRING_CREDS = Pattern.compile(
+            "(?i)(mongodb(\\+srv)?://|amqp://|redis://|jdbc:)\\S+:\\S+@");
+
+    private static final String[] CONSUMER_COMPONENTS = {
+            "platform-http", "netty-http", "jetty", "undertow", "servlet",
+            "cxf", "cxfrs", "rest", "coap", "grpc"
+    };
+
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
+          description = "Scan a Camel route for security anti-patterns. "
+                        + "Performs static analysis to detect: exposed secrets 
in URIs, "
+                        + "insecure configuration options 
(trustAllCertificates, allowJavaSerializedObject, etc.), "
+                        + "missing Camel* header filters on consumers, 
unencrypted protocols, "
+                        + "and other violations from the Camel security model. 
"
+                        + "Returns findings with severity, line number, and 
remediation guidance. "
+                        + "Distinct from camel_route_harden_context which 
provides general security context "
+                        + "and CVE advisories.")
+    public SecurityScanResult camel_security_scan(
+            @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);
+        }
+
+        try {
+            String resolvedFormat = format != null && !format.isBlank() ? 
format.toLowerCase(Locale.ROOT) : "yaml";
+            String[] lines = route.split("\n", -1);
+
+            List<Finding> findings = new ArrayList<>();
+
+            scanInsecureOptions(lines, findings);
+            scanSecretsInUris(lines, findings);
+            scanConnectionStringCredentials(lines, findings);
+            scanUnencryptedProtocols(lines, findings);
+            scanMissingHeaderFilters(route, lines, findings);
+            scanExecComponent(lines, findings);
+            scanSqlInjection(lines, findings);
+            scanFilePathTraversal(lines, findings);
+
+            findings.sort((a, b) -> severityRank(a.severity()) - 
severityRank(b.severity()));
+
+            int critical = (int) findings.stream().filter(f -> 
"critical".equals(f.severity())).count();
+            int high = (int) findings.stream().filter(f -> 
"high".equals(f.severity())).count();
+            int medium = (int) findings.stream().filter(f -> 
"medium".equals(f.severity())).count();
+            int low = (int) findings.stream().filter(f -> 
"low".equals(f.severity())).count();
+
+            return new SecurityScanResult(
+                    resolvedFormat, findings, findings.size(),
+                    new SeverityCounts(critical, high, medium, low));
+        } catch (ToolCallException e) {
+            throw e;
+        } catch (Throwable e) {
+            throw new ToolCallException(
+                    "Failed to scan route (" + e.getClass().getName() + "): " 
+ e.getMessage(), null);
+        }
+    }
+
+    private void scanInsecureOptions(String[] lines, List<Finding> findings) {
+        Map<String, SecurityUtils.SecurityOption> securityOptions = 
SecurityUtils.getSecurityOptions();
+
+        for (int i = 0; i < lines.length; i++) {
+            String lower = 
lines[i].toLowerCase(Locale.ROOT).replaceAll("[\\s-]", "");
+            for (Map.Entry<String, SecurityUtils.SecurityOption> entry : 
securityOptions.entrySet()) {
+                String optionKey = entry.getKey();
+                String insecureValue = entry.getValue().insecureValue();
+                String category = entry.getValue().category();
+
+                if (insecureValue == null || insecureValue.isEmpty()) {
+                    continue;
+                }
+                if (lower.contains(optionKey + "=" + insecureValue)
+                        || lower.contains(optionKey + ":" + insecureValue)
+                        || lower.contains(optionKey + "\":" + insecureValue)
+                        || lower.contains("\"" + optionKey + "\":" + 
insecureValue)) {
+                    findings.add(new Finding(
+                            severityForCategory(category),
+                            category,
+                            "Insecure option: " + optionKey + "=" + 
insecureValue,
+                            i + 1,
+                            remediationForCategory(category, optionKey)));
+                }
+            }
+        }
+    }
+
+    private void scanSecretsInUris(String[] lines, List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            Matcher matcher = SECRET_IN_URI.matcher(lines[i]);
+            while (matcher.find()) {
+                String key = matcher.group(1);
+                findings.add(new Finding(
+                        "critical",
+                        "secret",
+                        "Potential plain-text secret in URI: " + key,
+                        i + 1,
+                        "Use property placeholders {{" + key + "}} or a 
secrets management vault "
+                               + "(HashiCorp Vault, AWS Secrets Manager, Azure 
Key Vault)"));
+            }
+        }
+    }
+
+    private void scanConnectionStringCredentials(String[] lines, List<Finding> 
findings) {
+        for (int i = 0; i < lines.length; i++) {
+            if (CONNECTION_STRING_CREDS.matcher(lines[i]).find()) {
+                findings.add(new Finding(
+                        "critical",
+                        "secret",
+                        "Connection string with embedded credentials",
+                        i + 1,
+                        "Extract credentials from the connection string and 
use property placeholders or vault services"));
+            }
+        }
+    }
+
+    private void scanUnencryptedProtocols(String[] lines, List<Finding> 
findings) {
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+
+            if (containsScheme(lower, "http") && !containsScheme(lower, 
"https")
+                    && !lower.contains("platform-http")) {
+                findings.add(new Finding(
+                        "high",
+                        "insecure:ssl",
+                        "Using HTTP instead of HTTPS",
+                        i + 1,
+                        "Use HTTPS with TLS 1.2+ for secure communication"));
+            }
+            if (containsScheme(lower, "ftp") && !containsScheme(lower, "sftp") 
&& !containsScheme(lower, "ftps")) {
+                findings.add(new Finding(
+                        "high",
+                        "insecure:ssl",
+                        "Using plain FTP instead of SFTP/FTPS",
+                        i + 1,
+                        "Use SFTP or FTPS for encrypted file transfers"));
+            }
+            if (containsScheme(lower, "ldap") && !containsScheme(lower, 
"ldaps")) {
+                findings.add(new Finding(
+                        "medium",
+                        "insecure:ssl",
+                        "Using LDAP instead of LDAPS",
+                        i + 1,
+                        "Use LDAPS for encrypted LDAP communication"));
+            }
+            if (containsScheme(lower, "smtp") && !containsScheme(lower, 
"smtps")) {
+                findings.add(new Finding(
+                        "medium",
+                        "insecure:ssl",
+                        "Using SMTP instead of SMTPS",
+                        i + 1,
+                        "Use SMTPS for encrypted email communication"));
+            }
+        }
+    }
+
+    private void scanMissingHeaderFilters(String route, String[] lines, 
List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            for (String consumer : CONSUMER_COMPONENTS) {
+                if (containsScheme(lower, consumer)) {
+                    if (!hasHeaderFilterNearby(lines, i)) {
+                        findings.add(new Finding(
+                                "high",
+                                "header-injection",
+                                "Consumer '" + consumer + "' without Camel* 
header filter",
+                                i + 1,
+                                "Add removeHeaders(\"Camel*\") or a 
HeaderFilterStrategy to prevent "
+                                       + "external clients from injecting 
Camel-internal headers "
+                                       + "(CVE-2025-27636 and related)"));
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean hasHeaderFilterNearby(String[] lines, int consumerLine) {
+        int searchEnd = Math.min(lines.length, consumerLine + 20);
+        for (int i = consumerLine; i < searchEnd; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            if (lower.contains("removeheaders") || 
lower.contains("headerfilter")
+                    || lower.contains("\"camel*\"") || 
lower.contains("'camel*'")) {
+                return true;
+            }
+            if (i > consumerLine && containsAnyConsumerScheme(lower)) {
+                break;
+            }
+        }
+        return false;
+    }
+
+    private boolean containsAnyConsumerScheme(String lower) {
+        for (String consumer : CONSUMER_COMPONENTS) {
+            if (containsScheme(lower, consumer)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void scanExecComponent(String[] lines, List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            if (containsScheme(lines[i].toLowerCase(Locale.ROOT), "exec")) {
+                findings.add(new Finding(
+                        "critical",
+                        "command-injection",
+                        "Using exec component — high risk for command 
injection",
+                        i + 1,
+                        "Validate all inputs strictly. Consider safer 
alternatives. "
+                               + "Never pass untrusted input directly to 
exec"));
+            }
+        }
+    }
+
+    private void scanSqlInjection(String[] lines, List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            if (containsScheme(lower, "sql") && !lower.contains(":#") && 
!lower.contains(":?")) {
+                findings.add(new Finding(
+                        "high",
+                        "sql-injection",
+                        "SQL query may not use parameterized queries",
+                        i + 1,
+                        "Use parameterized queries with named parameters 
(:#param) or positional (:?)"));
+            }
+        }
+    }
+
+    private void scanFilePathTraversal(String[] lines, List<Finding> findings) 
{
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            if (containsScheme(lower, "file") && (lower.contains("${") || 
lower.contains("$simple{"))) {
+                findings.add(new Finding(
+                        "medium",
+                        "path-traversal",
+                        "File path contains dynamic expression — potential 
path traversal risk",
+                        i + 1,
+                        "Validate file paths and restrict to allowed 
directories using fileName option"));
+            }
+        }
+    }
+
+    private static boolean containsScheme(String text, String scheme) {
+        int idx = text.indexOf(scheme + ":");
+        if (idx >= 0) {
+            if (idx == 0 || !Character.isLetterOrDigit(text.charAt(idx - 1)) 
&& text.charAt(idx - 1) != '-') {
+                return true;
+            }
+        }
+        return text.contains("\"" + scheme + "\"") || text.contains("'" + 
scheme + "'");
+    }
+

Review Comment:
   **Low:** Operator precedence readability — the expression `idx == 0 || 
!Character.isLetterOrDigit(text.charAt(idx - 1)) && text.charAt(idx - 1) != 
'-'` is functionally correct due to Java precedence (`!` > `&&` > `||`), but 
adding explicit parentheses would make the intent immediately clear:
   
   ```suggestion
               if (idx == 0 || (!Character.isLetterOrDigit(text.charAt(idx - 
1)) && text.charAt(idx - 1) != '-')) {
   ```



##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java:
##########
@@ -0,0 +1,356 @@
+/*
+ * 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.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+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.util.SecurityUtils;
+
+/**
+ * MCP Tool for static security analysis of Camel route definitions.
+ * <p>
+ * Scans route content (YAML, XML, or Java DSL) for security anti-patterns 
defined in the Camel security model
+ * ({@code design/security.adoc}). Produces actionable findings with severity, 
line location, and remediation guidance.
+ * <p>
+ * Distinct from {@link HardenTools} which provides general security context 
and CVE advisories — this tool performs
+ * line-by-line static analysis to find specific violations.
+ */
+@ApplicationScoped
+public class SecurityScanTools {
+
+    private static final Pattern SECRET_IN_URI = Pattern.compile(
+            
"(?i)(password|passwd|pwd|secret|token|apikey|api[_-]?key|access[_-]?key)\\s*[=:]\\s*(?!\\{\\{)(?!\\$\\{)([^\\s,;}'\"\\]&]+)");
+
+    private static final Pattern CONNECTION_STRING_CREDS = Pattern.compile(
+            "(?i)(mongodb(\\+srv)?://|amqp://|redis://|jdbc:)\\S+:\\S+@");
+
+    private static final String[] CONSUMER_COMPONENTS = {
+            "platform-http", "netty-http", "jetty", "undertow", "servlet",
+            "cxf", "cxfrs", "rest", "coap", "grpc"
+    };
+
+    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
+          description = "Scan a Camel route for security anti-patterns. "
+                        + "Performs static analysis to detect: exposed secrets 
in URIs, "
+                        + "insecure configuration options 
(trustAllCertificates, allowJavaSerializedObject, etc.), "
+                        + "missing Camel* header filters on consumers, 
unencrypted protocols, "
+                        + "and other violations from the Camel security model. 
"
+                        + "Returns findings with severity, line number, and 
remediation guidance. "
+                        + "Distinct from camel_route_harden_context which 
provides general security context "
+                        + "and CVE advisories.")
+    public SecurityScanResult camel_security_scan(
+            @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);
+        }
+
+        try {
+            String resolvedFormat = format != null && !format.isBlank() ? 
format.toLowerCase(Locale.ROOT) : "yaml";
+            String[] lines = route.split("\n", -1);
+
+            List<Finding> findings = new ArrayList<>();
+
+            scanInsecureOptions(lines, findings);
+            scanSecretsInUris(lines, findings);
+            scanConnectionStringCredentials(lines, findings);
+            scanUnencryptedProtocols(lines, findings);
+            scanMissingHeaderFilters(route, lines, findings);
+            scanExecComponent(lines, findings);
+            scanSqlInjection(lines, findings);
+            scanFilePathTraversal(lines, findings);
+
+            findings.sort((a, b) -> severityRank(a.severity()) - 
severityRank(b.severity()));
+
+            int critical = (int) findings.stream().filter(f -> 
"critical".equals(f.severity())).count();
+            int high = (int) findings.stream().filter(f -> 
"high".equals(f.severity())).count();
+            int medium = (int) findings.stream().filter(f -> 
"medium".equals(f.severity())).count();
+            int low = (int) findings.stream().filter(f -> 
"low".equals(f.severity())).count();
+
+            return new SecurityScanResult(
+                    resolvedFormat, findings, findings.size(),
+                    new SeverityCounts(critical, high, medium, low));
+        } catch (ToolCallException e) {
+            throw e;
+        } catch (Throwable e) {
+            throw new ToolCallException(
+                    "Failed to scan route (" + e.getClass().getName() + "): " 
+ e.getMessage(), null);
+        }
+    }
+
+    private void scanInsecureOptions(String[] lines, List<Finding> findings) {
+        Map<String, SecurityUtils.SecurityOption> securityOptions = 
SecurityUtils.getSecurityOptions();
+
+        for (int i = 0; i < lines.length; i++) {
+            String lower = 
lines[i].toLowerCase(Locale.ROOT).replaceAll("[\\s-]", "");
+            for (Map.Entry<String, SecurityUtils.SecurityOption> entry : 
securityOptions.entrySet()) {
+                String optionKey = entry.getKey();
+                String insecureValue = entry.getValue().insecureValue();
+                String category = entry.getValue().category();
+
+                if (insecureValue == null || insecureValue.isEmpty()) {
+                    continue;
+                }
+                if (lower.contains(optionKey + "=" + insecureValue)
+                        || lower.contains(optionKey + ":" + insecureValue)
+                        || lower.contains(optionKey + "\":" + insecureValue)
+                        || lower.contains("\"" + optionKey + "\":" + 
insecureValue)) {
+                    findings.add(new Finding(
+                            severityForCategory(category),
+                            category,
+                            "Insecure option: " + optionKey + "=" + 
insecureValue,
+                            i + 1,
+                            remediationForCategory(category, optionKey)));
+                }
+            }
+        }
+    }
+
+    private void scanSecretsInUris(String[] lines, List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            Matcher matcher = SECRET_IN_URI.matcher(lines[i]);
+            while (matcher.find()) {
+                String key = matcher.group(1);
+                findings.add(new Finding(
+                        "critical",
+                        "secret",
+                        "Potential plain-text secret in URI: " + key,
+                        i + 1,
+                        "Use property placeholders {{" + key + "}} or a 
secrets management vault "
+                               + "(HashiCorp Vault, AWS Secrets Manager, Azure 
Key Vault)"));
+            }
+        }
+    }
+
+    private void scanConnectionStringCredentials(String[] lines, List<Finding> 
findings) {
+        for (int i = 0; i < lines.length; i++) {
+            if (CONNECTION_STRING_CREDS.matcher(lines[i]).find()) {
+                findings.add(new Finding(
+                        "critical",
+                        "secret",
+                        "Connection string with embedded credentials",
+                        i + 1,
+                        "Extract credentials from the connection string and 
use property placeholders or vault services"));
+            }
+        }
+    }
+
+    private void scanUnencryptedProtocols(String[] lines, List<Finding> 
findings) {
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+
+            if (containsScheme(lower, "http") && !containsScheme(lower, 
"https")
+                    && !lower.contains("platform-http")) {
+                findings.add(new Finding(
+                        "high",
+                        "insecure:ssl",
+                        "Using HTTP instead of HTTPS",
+                        i + 1,
+                        "Use HTTPS with TLS 1.2+ for secure communication"));
+            }
+            if (containsScheme(lower, "ftp") && !containsScheme(lower, "sftp") 
&& !containsScheme(lower, "ftps")) {
+                findings.add(new Finding(
+                        "high",
+                        "insecure:ssl",
+                        "Using plain FTP instead of SFTP/FTPS",
+                        i + 1,
+                        "Use SFTP or FTPS for encrypted file transfers"));
+            }
+            if (containsScheme(lower, "ldap") && !containsScheme(lower, 
"ldaps")) {
+                findings.add(new Finding(
+                        "medium",
+                        "insecure:ssl",
+                        "Using LDAP instead of LDAPS",
+                        i + 1,
+                        "Use LDAPS for encrypted LDAP communication"));
+            }
+            if (containsScheme(lower, "smtp") && !containsScheme(lower, 
"smtps")) {
+                findings.add(new Finding(
+                        "medium",
+                        "insecure:ssl",
+                        "Using SMTP instead of SMTPS",
+                        i + 1,
+                        "Use SMTPS for encrypted email communication"));
+            }
+        }
+    }
+
+    private void scanMissingHeaderFilters(String route, String[] lines, 
List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            for (String consumer : CONSUMER_COMPONENTS) {
+                if (containsScheme(lower, consumer)) {
+                    if (!hasHeaderFilterNearby(lines, i)) {
+                        findings.add(new Finding(
+                                "high",
+                                "header-injection",
+                                "Consumer '" + consumer + "' without Camel* 
header filter",
+                                i + 1,
+                                "Add removeHeaders(\"Camel*\") or a 
HeaderFilterStrategy to prevent "
+                                       + "external clients from injecting 
Camel-internal headers "
+                                       + "(CVE-2025-27636 and related)"));
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean hasHeaderFilterNearby(String[] lines, int consumerLine) {
+        int searchEnd = Math.min(lines.length, consumerLine + 20);
+        for (int i = consumerLine; i < searchEnd; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            if (lower.contains("removeheaders") || 
lower.contains("headerfilter")
+                    || lower.contains("\"camel*\"") || 
lower.contains("'camel*'")) {
+                return true;
+            }
+            if (i > consumerLine && containsAnyConsumerScheme(lower)) {
+                break;
+            }
+        }
+        return false;
+    }
+
+    private boolean containsAnyConsumerScheme(String lower) {
+        for (String consumer : CONSUMER_COMPONENTS) {
+            if (containsScheme(lower, consumer)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void scanExecComponent(String[] lines, List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            if (containsScheme(lines[i].toLowerCase(Locale.ROOT), "exec")) {
+                findings.add(new Finding(
+                        "critical",
+                        "command-injection",
+                        "Using exec component — high risk for command 
injection",
+                        i + 1,
+                        "Validate all inputs strictly. Consider safer 
alternatives. "
+                               + "Never pass untrusted input directly to 
exec"));
+            }
+        }
+    }
+
+    private void scanSqlInjection(String[] lines, List<Finding> findings) {
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            if (containsScheme(lower, "sql") && !lower.contains(":#") && 
!lower.contains(":?")) {
+                findings.add(new Finding(
+                        "high",
+                        "sql-injection",
+                        "SQL query may not use parameterized queries",
+                        i + 1,
+                        "Use parameterized queries with named parameters 
(:#param) or positional (:?)"));
+            }
+        }
+    }
+
+    private void scanFilePathTraversal(String[] lines, List<Finding> findings) 
{
+        for (int i = 0; i < lines.length; i++) {
+            String lower = lines[i].toLowerCase(Locale.ROOT);
+            if (containsScheme(lower, "file") && (lower.contains("${") || 
lower.contains("$simple{"))) {
+                findings.add(new Finding(
+                        "medium",
+                        "path-traversal",
+                        "File path contains dynamic expression — potential 
path traversal risk",
+                        i + 1,
+                        "Validate file paths and restrict to allowed 
directories using fileName option"));
+            }
+        }
+    }
+
+    private static boolean containsScheme(String text, String scheme) {
+        int idx = text.indexOf(scheme + ":");
+        if (idx >= 0) {
+            if (idx == 0 || !Character.isLetterOrDigit(text.charAt(idx - 1)) 
&& text.charAt(idx - 1) != '-') {
+                return true;
+            }
+        }
+        return text.contains("\"" + scheme + "\"") || text.contains("'" + 
scheme + "'");
+    }
+
+    private static int findLineContaining(String[] lines, String text) {
+        String lower = text.toLowerCase(Locale.ROOT);
+        for (int i = 0; i < lines.length; i++) {
+            if (lines[i].toLowerCase(Locale.ROOT).contains(lower)) {
+                return i + 1;
+            }
+        }

Review Comment:
   **Low:** Dead code — `findLineContaining()` is defined but never called. The 
refactoring to per-consumer `hasHeaderFilterNearby()` eliminated its only call 
site. Can be removed.
   
   ```suggestion
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to