This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new a16b68f93a16 CAMEL-24220: Add camel_security_scan tool for route 
security analysis (#25026)
a16b68f93a16 is described below

commit a16b68f93a16576502ef1062ce4006be7d41cbc7
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Jul 28 09:15:56 2026 +0200

    CAMEL-24220: Add camel_security_scan tool for route security analysis 
(#25026)
    
    * CAMEL-24220: Add camel_security_scan tool for route security analysis
    
    Adds a new MCP tool that performs static analysis of Camel route definitions
    to detect security anti-patterns. Distinct from camel_route_harden_context
    (which provides general security context and CVE advisories), this tool
    performs line-by-line analysis and returns actionable findings with 
severity,
    line numbers, and remediation guidance.
    
    Detection categories:
    - Insecure options from SecurityUtils (trustAllCertificates, 
allowJavaSerializedObject,
      transferException, etc. — 24 options across ssl/serialization/dev 
categories)
    - Plain-text secrets in URIs (password, token, apiKey, etc.)
    - Connection strings with embedded credentials (mongodb://, amqp://, etc.)
    - Unencrypted protocols (HTTP vs HTTPS, FTP vs SFTP, LDAP vs LDAPS, SMTP vs 
SMTPS)
    - Missing Camel* header filters on HTTP consumers (CVE-2025-27636 family)
    - Command injection via exec component
    - SQL injection risk (missing parameterized queries)
    - File path traversal with dynamic expressions
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24220: Address review feedback
    
    - Fix RAW() false negative: remove RAW() negative lookahead from
      SECRET_IN_URI regex — RAW() is a URI encoding wrapper, not a
      security mechanism
    - Fix empty insecureValue false positives: skip options with empty
      insecureValue to avoid matching any assignment
    - Fix SQL scheme false positive on composite schemes: use boundary
      check in containsScheme() to avoid matching google-bigquery-sql:
      as sql:
    - Fix route-level header filter check: scope per-consumer line with
      hasHeaderFilterNearby() instead of checking entire route text
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24220: Add regression tests and cleanup
    
    - Add 4 regression tests for fixed bugs: RAW() detection, empty
      insecureValue, composite scheme SQL, per-consumer header filter
    - Add explicit parentheses in containsScheme() for readability
    - Remove dead findLineContaining() method
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    * CAMEL-24220: delegate insecure-option matching to 
SecurityUtils.isInsecureValue
    
    Addresses the remaining gnodet review point on scanInsecureOptions: instead 
of
    reimplementing the value comparison with lower.contains(optionKey + "=" +
    insecureValue) — which either false-positived on empty insecureValue
    (sslEndpointAlgorithm= matched any assignment) or, after guarding, skipped
    those options entirely — the scanner now extracts the assigned value and 
calls
    SecurityUtils.isInsecureValue(). That is the canonical check: 
case-insensitive,
    and it models the sslEndpointAlgorithm case where the empty/none/false 
value is
    the insecure one. So sslEndpointAlgorithm=HTTPS is not flagged while
    sslEndpointAlgorithm=none now is (previously silently dropped).
    
    Adds flagsSslEndpointAlgorithmDisabled to pin the restored coverage.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
    
    ---------
    
    Signed-off-by: Andrea Cosentino <[email protected]>
    Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
---
 .../jbang/core/commands/mcp/SecurityScanTools.java | 379 ++++++++++++++++++
 .../core/commands/mcp/SecurityScanToolsTest.java   | 424 +++++++++++++++++++++
 2 files changed, 803 insertions(+)

diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
new file mode 100644
index 000000000000..55a56b6cb54f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
@@ -0,0 +1,379 @@
+/*
+ * 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 normalized = 
lines[i].toLowerCase(Locale.ROOT).replaceAll("[\\s-]", "");
+            for (Map.Entry<String, SecurityUtils.SecurityOption> entry : 
securityOptions.entrySet()) {
+                String optionKey = entry.getKey();
+                String category = entry.getValue().category();
+
+                String value = extractOptionValue(normalized, optionKey);
+                if (value == null) {
+                    continue;
+                }
+                // Delegate the insecure-value decision to SecurityUtils 
rather than reimplementing string
+                // comparison here. That keeps the scanner in sync with the 
canonical logic — case-insensitive
+                // matching, and the sslEndpointAlgorithm case where the 
*empty*/none/false value is the insecure
+                // one — so an empty insecureValue no longer either 
false-positives on any assignment nor gets
+                // skipped entirely.
+                if (SecurityUtils.isInsecureValue(optionKey, value)) {
+                    findings.add(new Finding(
+                            severityForCategory(category),
+                            category,
+                            "Insecure option: " + optionKey + "=" + value,
+                            i + 1,
+                            remediationForCategory(category, optionKey)));
+                }
+            }
+        }
+    }
+
+    /**
+     * Extract the value assigned to {@code optionKey} on a normalized 
(lowercased, whitespace/dash-stripped) line,
+     * covering the URI-query ({@code key=value}) and YAML/JSON ({@code 
key:value}, {@code "key":value}) forms. Returns
+     * {@code null} when the option does not appear with an assignment on the 
line, or an empty string when it is
+     * assigned an empty value — which is itself the insecure case for some 
options (e.g. sslEndpointAlgorithm).
+     */
+    private static String extractOptionValue(String normalized, String 
optionKey) {
+        int from = 0;
+        while (true) {
+            int idx = normalized.indexOf(optionKey, from);
+            if (idx < 0) {
+                return null;
+            }
+            int after = idx + optionKey.length();
+            // a quoted key ("key":value) leaves a closing quote before the 
separator
+            if (after < normalized.length() && normalized.charAt(after) == 
'"') {
+                after++;
+            }
+            if (after < normalized.length()
+                    && (normalized.charAt(after) == '=' || 
normalized.charAt(after) == ':')) {
+                int valStart = after + 1;
+                int valEnd = valStart;
+                while (valEnd < normalized.length() && 
"=:,;}'\"]&".indexOf(normalized.charAt(valEnd)) < 0) {
+                    valEnd++;
+                }
+                return normalized.substring(valStart, valEnd);
+            }
+            from = idx + 1;
+        }
+    }
+
+    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 String severityForCategory(String category) {
+        return switch (category) {
+            case "insecure:serialization" -> "critical";
+            case "insecure:ssl" -> "high";
+            case "insecure:dev" -> "medium";
+            case "secret" -> "critical";
+            default -> "medium";
+        };
+    }
+
+    private static String remediationForCategory(String category, String 
optionKey) {
+        return switch (category) {
+            case "insecure:ssl" -> "Remove " + optionKey + " or set it to a 
secure value. "
+                                   + "Configure TLS via SSLContextParameters 
with proper certificate validation";
+            case "insecure:serialization" -> "Remove " + optionKey + " or set 
it to a secure value. "
+                                             + "Java serialization of 
untrusted input enables remote code execution";
+            case "insecure:dev" -> "Disable " + optionKey + " in production. "
+                                   + "Development features can expose internal 
state or enable remote control";
+            case "secret" -> "Use property placeholders or vault services 
instead of plain-text secrets";
+            default -> "Review and correct the insecure configuration";
+        };
+    }
+
+    private static int severityRank(String severity) {
+        return switch (severity) {
+            case "critical" -> 0;
+            case "high" -> 1;
+            case "medium" -> 2;
+            case "low" -> 3;
+            default -> 4;
+        };
+    }
+
+    // Result records
+
+    public record SecurityScanResult(
+            String format, List<Finding> findings, int totalFindings,
+            SeverityCounts severityCounts) {
+    }
+
+    public record Finding(
+            String severity, String category, String issue,
+            int line, String remediation) {
+    }
+
+    public record SeverityCounts(int critical, int high, int medium, int low) {
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanToolsTest.java
new file mode 100644
index 000000000000..2eb0b5f4d247
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanToolsTest.java
@@ -0,0 +1,424 @@
+/*
+ * 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);
+    }
+
+    @Test
+    void detectsRawWrappedPasswordAsPlainTextSecret() {
+        String route = "- route:\n    from:\n      uri: 
\"kafka:topic?password=RAW(mysecret123)\"";
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings())
+                .anyMatch(f -> "secret".equals(f.category()) && 
f.issue().contains("password"));
+    }
+
+    @Test
+    void doesNotFlagSecureOptionValuesAsInsecure() {
+        String route = "- route:\n    from:\n      uri: 
\"https:endpoint?sslEndpointAlgorithm=HTTPS\"";
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings())
+                .noneMatch(f -> f.issue().contains("sslendpointalgorithm"));
+    }
+
+    @Test
+    void flagsSslEndpointAlgorithmDisabled() {
+        // sslEndpointAlgorithm is the option with an empty insecureValue: the 
insecure case is the empty/none/false
+        // value (no hostname verification), which 
SecurityUtils.isInsecureValue models. The scanner must flag it.
+        String route = "- route:\n    from:\n      uri: 
\"https:endpoint?sslEndpointAlgorithm=none\"";
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings())
+                .anyMatch(f -> f.issue().contains("sslendpointalgorithm") && 
"insecure:ssl".equals(f.category()));
+    }
+
+    @Test
+    void doesNotFlagCompositeSchemeAsSql() {
+        String route = "- route:\n    from:\n      uri: 
\"google-bigquery-sql:project:dataset.table\"";
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings())
+                .noneMatch(f -> "sql-injection".equals(f.category()));
+    }
+
+    @Test
+    void perConsumerHeaderFilterCheckDoesNotSuppressGlobally() {
+        String route = """
+                - route:
+                    from:
+                      uri: "platform-http:/api/a"
+                    steps:
+                      - removeHeaders:
+                          pattern: "Camel*"
+                      - to: "direct:a"
+                - route:
+                    from:
+                      uri: "netty-http:0.0.0.0:8080/api/b"
+                    steps:
+                      - to: "direct:b"
+                """;
+        SecurityScanTools.SecurityScanResult result = 
tools.camel_security_scan(route, "yaml");
+
+        assertThat(result.findings())
+                .anyMatch(f -> "header-injection".equals(f.category())
+                        && f.issue().contains("netty-http"));
+    }
+
+    private static int severityRank(String severity) {
+        return switch (severity) {
+            case "critical" -> 0;
+            case "high" -> 1;
+            case "medium" -> 2;
+            case "low" -> 3;
+            default -> 4;
+        };
+    }
+}

Reply via email to