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


##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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*(?!\\{\\{)(?!\\$\\{)(?!RAW\\()([^\\s,;}'\"\\]&]+)");
+

Review Comment:
   Addressed — the current SECRET_IN_URI regex has no RAW() negative lookahead, 
and its value capture includes the RAW(...) wrapper, so 
password=RAW(mysecret123) IS flagged. Pinned by the new 
detectsRawWrappedPasswordAsPlainTextSecret test.\n\n_Claude Code on behalf of 
Andrea Cosentino (@oscerd)._



##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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*(?!\\{\\{)(?!\\$\\{)(?!RAW\\()([^\\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();

Review Comment:
   Done in e4f2712 — scanInsecureOptions now extracts the assigned value and 
calls SecurityUtils.isInsecureValue() instead of a string contains. That is the 
canonical check (case-insensitive, and the sslEndpointAlgorithm 
empty/none/false special case), so sslEndpointAlgorithm=HTTPS is not flagged 
while sslEndpointAlgorithm=none now is. Covered by 
doesNotFlagSecureOptionValuesAsInsecure and the new 
flagsSslEndpointAlgorithmDisabled.\n\n_Claude Code on behalf of Andrea 
Cosentino (@oscerd)._



##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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*(?!\\{\\{)(?!\\$\\{)(?!RAW\\()([^\\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 (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) {
+        String lower = route.toLowerCase(Locale.ROOT);
+        for (String consumer : CONSUMER_COMPONENTS) {
+            if (containsScheme(lower, consumer)) {
+                if (!lower.contains("removeheaders") && 
!lower.contains("headerfilter")
+                        && !lower.contains("camel*")) {
+                    int lineNum = findLineContaining(lines, consumer + ":");
+                    findings.add(new Finding(
+                            "high",
+                            "header-injection",
+                            "Consumer '" + consumer + "' without Camel* header 
filter",
+                            lineNum,

Review Comment:
   Addressed — containsScheme() now requires the char before the scheme to be 
neither a letter/digit nor a dash, so google-bigquery-sql: no longer matches 
sql. Pinned by doesNotFlagCompositeSchemeAsSql.\n\n_Claude Code on behalf of 
Andrea Cosentino (@oscerd)._



##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java:
##########
@@ -0,0 +1,323 @@
+/*
+ * 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*(?!\\{\\{)(?!\\$\\{)(?!RAW\\()([^\\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 (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",

Review Comment:
   Addressed — scanMissingHeaderFilters is now per-consumer: for each consumer 
line it calls hasHeaderFilterNearby() which searches only the window until the 
next consumer, so a filter on one consumer no longer suppresses another. Pinned 
by perConsumerHeaderFilterCheckDoesNotSuppressGlobally.\n\n_Claude Code on 
behalf of Andrea Cosentino (@oscerd)._



##########
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:
   Done — the parentheses are in place: idx == 0 || 
(!Character.isLetterOrDigit(...) && ... != '-').\n\n_Claude Code on behalf of 
Andrea Cosentino (@oscerd)._



-- 
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