This is an automated email from the ASF dual-hosted git repository. acosentino pushed a commit to branch CAMEL-23273 in repository https://gitbox.apache.org/repos/asf/camel.git
commit b472c6854b8fe546a7a8bdcb9d10b9aed75f690d Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Mar 30 18:13:06 2026 +0200 CAMEL-23273 - Camel-Jbang-mcp: Warn about sensitive data in POM content passed to migration tools Add PomSanitizer utility to detect and mask sensitive data (passwords, tokens, API keys, secrets) in POM content before processing. Strips <servers> and <distributionManagement> sections. Add sanitizePom boolean parameter (default: true) to camel_migration_analyze, camel_dependency_check, and camel_migration_wildfly_karaf tools. Update tool descriptions with sanitization guidance. Add 21 tests covering detection, masking, placeholder preservation, and tool integration. Signed-off-by: Andrea Cosentino <[email protected]> --- .../core/commands/mcp/DependencyCheckTools.java | 35 ++- .../jbang/core/commands/mcp/MigrationTools.java | 25 +- .../commands/mcp/MigrationWildflyKarafTools.java | 27 +- .../dsl/jbang/core/commands/mcp/PomSanitizer.java | 136 +++++++++ .../commands/mcp/DependencyCheckToolsTest.java | 89 ++++-- .../jbang/core/commands/mcp/PomSanitizerTest.java | 317 +++++++++++++++++++++ 6 files changed, 596 insertions(+), 33 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java index 2c964e5a9162..eb4201a80f15 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java @@ -16,6 +16,7 @@ */ package org.apache.camel.dsl.jbang.core.commands.mcp; +import java.util.ArrayList; import java.util.List; import jakarta.enterprise.context.ApplicationScoped; @@ -51,27 +52,51 @@ public class DependencyCheckTools { + "detects outdated Camel dependencies compared to the latest catalog version, " + "missing Maven dependencies for components used in routes, " + "and version conflicts between the Camel BOM and explicit dependency overrides. " - + "Returns actionable recommendations with corrected dependency snippets.") + + "Returns actionable recommendations with corrected dependency snippets. " + + "POM content is automatically sanitized to remove sensitive data (passwords, tokens, API keys, " + + "repository credentials) unless sanitizePom is set to false.") public String camel_dependency_check( - @ToolArg(description = "The pom.xml file content") String pomContent, + @ToolArg(description = "The pom.xml file content. " + + "IMPORTANT: Avoid including sensitive data such as passwords, tokens, or API keys. " + + "Sensitive content is automatically detected and masked.") String pomContent, @ToolArg(description = "Route definitions (YAML, XML, or Java DSL) to check for missing component dependencies. " + "Multiple routes can be provided concatenated.") String routes, @ToolArg(description = "Runtime type: main, spring-boot, or quarkus (default: main)") String runtime, @ToolArg(description = "Camel version to use (e.g., 4.17.0). If not specified, uses the default catalog version.") String camelVersion, @ToolArg(description = "Platform BOM coordinates in GAV format (groupId:artifactId:version). " - + "When provided, overrides camelVersion.") String platformBom) { + + "When provided, overrides camelVersion.") String platformBom, + @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials " + + "and stripping <servers> and <distributionManagement> sections") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { + // Sanitize POM content + String processedPom = pomContent; + List<String> sanitizationWarnings = new ArrayList<>(); + if (sanitizePom == null || sanitizePom) { + PomSanitizer.SanitizationResult sr = PomSanitizer.sanitize(pomContent); + processedPom = sr.pomContent(); + for (String pattern : sr.detectedPatterns()) { + sanitizationWarnings.add("Sensitive data detected and masked: " + pattern); + } + } + CamelCatalog catalog = catalogService.loadCatalog(runtime, camelVersion, platformBom); - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processedPom); JsonObject result = new JsonObject(); + // Add sanitization warnings if any + if (!sanitizationWarnings.isEmpty()) { + JsonArray sanitizationArr = new JsonArray(); + sanitizationWarnings.forEach(sanitizationArr::add); + result.put("sanitizationWarnings", sanitizationArr); + } + // Project info JsonObject projectInfo = new JsonObject(); projectInfo.put("camelVersion", pom.camelVersion()); @@ -92,7 +117,7 @@ public class DependencyCheckTools { result.put("missingDependencies", missingDeps); // 3. Check for version conflicts (explicit overrides when BOM is present) - JsonArray conflicts = checkVersionConflicts(pomContent, pom); + JsonArray conflicts = checkVersionConflicts(processedPom, pom); result.put("versionConflicts", conflicts); // 4. Build recommendations diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java index f62bc97157e2..a3b0d22effe6 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java @@ -49,21 +49,38 @@ public class MigrationTools { */ @Tool(description = "Analyze a Camel project's pom.xml to detect the runtime type (main, spring-boot, quarkus, " + "wildfly, karaf), Camel version, Java version, and Camel component dependencies. " - + "This is the first step in a migration workflow.") + + "This is the first step in a migration workflow. " + + "POM content is automatically sanitized to remove sensitive data (passwords, tokens, API keys, " + + "repository credentials) unless sanitizePom is set to false.") public ProjectAnalysisResult camel_migration_analyze( - @ToolArg(description = "The pom.xml file content") String pomContent) { + @ToolArg(description = "The pom.xml file content. " + + "IMPORTANT: Avoid including sensitive data such as passwords, tokens, or API keys. " + + "Sensitive content is automatically detected and masked.") String pomContent, + @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials " + + "and stripping <servers> and <distributionManagement> sections") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + // Sanitize POM content + String processedPom = pomContent; + List<String> sanitizationWarnings = new ArrayList<>(); + if (sanitizePom == null || sanitizePom) { + PomSanitizer.SanitizationResult sr = PomSanitizer.sanitize(pomContent); + processedPom = sr.pomContent(); + for (String pattern : sr.detectedPatterns()) { + sanitizationWarnings.add("Sensitive data detected and masked: " + pattern); + } + } + + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processedPom); String runtimeType = pom.runtimeType(); int majorVersion = pom.majorVersion(); - List<String> warnings = new ArrayList<>(); + List<String> warnings = new ArrayList<>(sanitizationWarnings); if (pom.camelVersion() == null) { warnings.add("Could not detect Camel version from pom.xml. " + "Check if the version is defined in a parent POM."); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java index 17f938204dc2..1f83a74018cd 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java @@ -48,18 +48,35 @@ public class MigrationWildflyKarafTools { + "IMPORTANT: When migrating to a different runtime (e.g., WildFly to Quarkus, Karaf to Spring Boot), " + "you MUST use the archetype command returned by this tool to create a new project. " + "Do NOT manually rewrite the pom.xml — always generate a new project with the archetype first, " - + "then migrate routes and source files into it.") + + "then migrate routes and source files into it. " + + "POM content is automatically sanitized to remove sensitive data (passwords, tokens, API keys, " + + "repository credentials) unless sanitizePom is set to false.") public WildflyKarafMigrationResult camel_migration_wildfly_karaf( - @ToolArg(description = "The pom.xml file content of the WildFly/Karaf project") String pomContent, + @ToolArg(description = "The pom.xml file content of the WildFly/Karaf project. " + + "IMPORTANT: Avoid including sensitive data such as passwords, tokens, or API keys. " + + "Sensitive content is automatically detected and masked.") String pomContent, @ToolArg(description = "Target runtime: spring-boot or quarkus (default: quarkus)") String targetRuntime, - @ToolArg(description = "Target Camel version (e.g., 4.18.0)") String targetVersion) { + @ToolArg(description = "Target Camel version (e.g., 4.18.0)") String targetVersion, + @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials " + + "and stripping <servers> and <distributionManagement> sections") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + // Sanitize POM content + String processedPom = pomContent; + List<String> sanitizationWarnings = new ArrayList<>(); + if (sanitizePom == null || sanitizePom) { + PomSanitizer.SanitizationResult sr = PomSanitizer.sanitize(pomContent); + processedPom = sr.pomContent(); + for (String pattern : sr.detectedPatterns()) { + sanitizationWarnings.add("Sensitive data detected and masked: " + pattern); + } + } + + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processedPom); String sourceRuntime = pom.isWildfly() ? "wildfly" : pom.isKaraf() ? "karaf" : "unknown"; String resolvedTarget = targetRuntime != null && !targetRuntime.isBlank() @@ -83,7 +100,7 @@ public class MigrationWildflyKarafTools { .collect(Collectors.toList()); // Warnings specific to the source runtime - List<String> warnings = new ArrayList<>(); + List<String> warnings = new ArrayList<>(sanitizationWarnings); if ("karaf".equals(sourceRuntime)) { warnings.add("Blueprint XML is not supported in Camel 3.x+. " + "Routes must be converted to YAML DSL, XML DSL, or Java DSL."); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizer.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizer.java new file mode 100644 index 000000000000..766eb743b74e --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizer.java @@ -0,0 +1,136 @@ +/* + * 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.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jboss.logging.Logger; + +/** + * Utility to detect and sanitize sensitive data in POM content before processing. + * <p> + * Scans for common credential patterns (passwords, tokens, API keys, secrets) and optionally strips or masks them. Also + * removes {@code <servers>} and {@code <distributionManagement>} sections which may contain private repository + * credentials and URLs. + */ +final class PomSanitizer { + + private static final Logger LOG = Logger.getLogger(PomSanitizer.class); + + private static final String SENSITIVE_KEYWORDS + = "password|passwd|token|apikey|api-key|api_key|secret|secretkey|secret-key|secret_key" + + "|accesskey|access-key|access_key|passphrase|privatekey|private-key|private_key|credentials"; + + /** + * Pattern matching XML elements whose tag names contain sensitive keywords. Captures: group(1) = element name, + * group(2) = element value. + */ + private static final Pattern SENSITIVE_ELEMENT_PATTERN = Pattern.compile( + "<([a-zA-Z0-9_.:-]*(?:" + SENSITIVE_KEYWORDS + ")[a-zA-Z0-9_.:-]*)>" + + "\\s*([^<]+?)\\s*" + + "</\\1>", + Pattern.CASE_INSENSITIVE); + + /** Pattern matching {@code <servers>...</servers>} sections. */ + private static final Pattern SERVERS_SECTION_PATTERN = Pattern.compile( + "<servers>.*?</servers>", Pattern.DOTALL); + + /** Pattern matching {@code <distributionManagement>...</distributionManagement>} sections. */ + private static final Pattern DIST_MGMT_SECTION_PATTERN = Pattern.compile( + "<distributionManagement>.*?</distributionManagement>", Pattern.DOTALL); + + private PomSanitizer() { + } + + /** + * Detect sensitive content patterns in POM content. + * + * @return list of descriptions of detected sensitive patterns + */ + static List<String> detectSensitiveContent(String pomContent) { + Set<String> findings = new LinkedHashSet<>(); + + Matcher matcher = SENSITIVE_ELEMENT_PATTERN.matcher(pomContent); + while (matcher.find()) { + String value = matcher.group(2).trim(); + // Property placeholders like ${my.password} are not actual secrets + if (!value.startsWith("${")) { + findings.add(matcher.group(1)); + } + } + + if (SERVERS_SECTION_PATTERN.matcher(pomContent).find()) { + findings.add("<servers> section (may contain repository credentials)"); + } + + if (DIST_MGMT_SECTION_PATTERN.matcher(pomContent).find()) { + findings.add("<distributionManagement> section (may contain private repository URLs)"); + } + + return new ArrayList<>(findings); + } + + /** + * Sanitize POM content by masking sensitive element values and stripping credential sections ({@code <servers>} and + * {@code <distributionManagement>}). + * <p> + * Property placeholders (e.g., {@code ${db.password}}) are preserved since they do not contain actual secret + * values. + * + * @return sanitization result with the processed POM content and detected patterns + */ + static SanitizationResult sanitize(String pomContent) { + List<String> detected = detectSensitiveContent(pomContent); + + String sanitized = pomContent; + + // Mask sensitive element values (preserve property placeholders) + sanitized = SENSITIVE_ELEMENT_PATTERN.matcher(sanitized).replaceAll(mr -> { + String value = mr.group(2).trim(); + if (value.startsWith("${")) { + return Matcher.quoteReplacement(mr.group()); + } + return Matcher.quoteReplacement( + "<" + mr.group(1) + ">***MASKED***</" + mr.group(1) + ">"); + }); + + // Strip servers section + sanitized = SERVERS_SECTION_PATTERN.matcher(sanitized).replaceAll(""); + + // Strip distributionManagement section + sanitized = DIST_MGMT_SECTION_PATTERN.matcher(sanitized).replaceAll(""); + + boolean wasSanitized = !sanitized.equals(pomContent); + + if (!detected.isEmpty()) { + LOG.warnf("Sensitive data detected in pomContent: %s. Content was sanitized before processing.", detected); + } + + return new SanitizationResult(sanitized, detected, wasSanitized); + } + + record SanitizationResult( + String pomContent, + List<String> detectedPatterns, + boolean wasSanitized) { + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java index de560acfafaf..4be2de6b648a 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckToolsTest.java @@ -143,14 +143,14 @@ class DependencyCheckToolsTest { @Test void nullPomThrows() { - assertThatThrownBy(() -> tools.camel_dependency_check(null, null, null, null, null)) + assertThatThrownBy(() -> tools.camel_dependency_check(null, null, null, null, null, null)) .isInstanceOf(ToolCallException.class) .hasMessageContaining("required"); } @Test void blankPomThrows() { - assertThatThrownBy(() -> tools.camel_dependency_check(" ", null, null, null, null)) + assertThatThrownBy(() -> tools.camel_dependency_check(" ", null, null, null, null, null)) .isInstanceOf(ToolCallException.class) .hasMessageContaining("required"); } @@ -159,7 +159,7 @@ class DependencyCheckToolsTest { @Test void resultContainsProjectInfo() throws Exception { - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonObject projectInfo = result.getMap("projectInfo"); @@ -170,7 +170,7 @@ class DependencyCheckToolsTest { @Test void detectsSpringBootRuntime() throws Exception { - String json = tools.camel_dependency_check(POM_SPRING_BOOT, null, null, null, null); + String json = tools.camel_dependency_check(POM_SPRING_BOOT, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonObject projectInfo = result.getMap("projectInfo"); @@ -181,7 +181,7 @@ class DependencyCheckToolsTest { @Test void detectsOutdatedVersion() throws Exception { - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonObject versionStatus = result.getMap("versionStatus"); @@ -193,7 +193,7 @@ class DependencyCheckToolsTest { @Test void versionStatusContainsCatalogVersion() throws Exception { - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonObject versionStatus = result.getMap("versionStatus"); @@ -207,7 +207,7 @@ class DependencyCheckToolsTest { // POM without BOM has only camel-core, route uses kafka String route = "from:\n uri: kafka:myTopic\n steps:\n - to: log:out"; - String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null); + String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray missing = (JsonArray) result.get("missingDependencies"); @@ -222,7 +222,7 @@ class DependencyCheckToolsTest { // POM_WITH_BOM already has camel-kafka String route = "from:\n uri: kafka:myTopic\n steps:\n - to: log:out"; - String json = tools.camel_dependency_check(POM_WITH_BOM, route, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, route, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray missing = (JsonArray) result.get("missingDependencies"); @@ -237,7 +237,7 @@ class DependencyCheckToolsTest { void missingDepContainsSnippet() throws Exception { String route = "from:\n uri: kafka:myTopic\n steps:\n - to: log:out"; - String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null); + String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray missing = (JsonArray) result.get("missingDependencies"); @@ -256,7 +256,7 @@ class DependencyCheckToolsTest { @Test void noMissingDepsWithoutRoutes() throws Exception { - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray missing = (JsonArray) result.get("missingDependencies"); @@ -268,7 +268,7 @@ class DependencyCheckToolsTest { // timer, log, direct are core components - should not be reported as missing String route = "from:\n uri: timer:tick\n steps:\n - to: log:out"; - String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null); + String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray missing = (JsonArray) result.get("missingDependencies"); @@ -282,7 +282,7 @@ class DependencyCheckToolsTest { @Test void detectsVersionConflictWithBom() throws Exception { - String json = tools.camel_dependency_check(POM_WITH_CONFLICT, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_CONFLICT, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray conflicts = (JsonArray) result.get("versionConflicts"); @@ -296,7 +296,7 @@ class DependencyCheckToolsTest { @Test void noConflictWithPropertyPlaceholderVersion() throws Exception { // POM_WITH_BOM uses ${camel.version} - not a conflict - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray conflicts = (JsonArray) result.get("versionConflicts"); @@ -306,7 +306,7 @@ class DependencyCheckToolsTest { @Test void noConflictWithoutBom() throws Exception { // No BOM means explicit versions are expected - String json = tools.camel_dependency_check(POM_WITHOUT_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITHOUT_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray conflicts = (JsonArray) result.get("versionConflicts"); @@ -317,7 +317,7 @@ class DependencyCheckToolsTest { @Test void recommendsUpgradeWhenOutdated() throws Exception { - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray recommendations = (JsonArray) result.get("recommendations"); @@ -329,7 +329,7 @@ class DependencyCheckToolsTest { @Test void recommendsBomWhenMissing() throws Exception { - String json = tools.camel_dependency_check(POM_WITHOUT_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITHOUT_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray recommendations = (JsonArray) result.get("recommendations"); @@ -343,7 +343,7 @@ class DependencyCheckToolsTest { void recommendsMissingDeps() throws Exception { String route = "from:\n uri: kafka:myTopic\n steps:\n - to: log:out"; - String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null); + String json = tools.camel_dependency_check(POM_WITHOUT_BOM, route, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonArray recommendations = (JsonArray) result.get("recommendations"); @@ -359,7 +359,7 @@ class DependencyCheckToolsTest { void summaryShowsHealthyWhenNoIssues() throws Exception { // Use a pom with current catalog version to avoid outdated flag // Since we can't easily match the catalog version, we just check structure - String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_BOM, null, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonObject summary = result.getMap("summary"); @@ -373,7 +373,7 @@ class DependencyCheckToolsTest { void summaryCountsAllIssues() throws Exception { String route = "from:\n uri: kafka:myTopic\n steps:\n - to: log:out"; - String json = tools.camel_dependency_check(POM_WITH_CONFLICT, route, null, null, null); + String json = tools.camel_dependency_check(POM_WITH_CONFLICT, route, null, null, null, null); JsonObject result = (JsonObject) Jsoner.deserialize(json); JsonObject summary = result.getMap("summary"); @@ -392,4 +392,55 @@ class DependencyCheckToolsTest { assertThat(DependencyCheckTools.compareVersions("3.20.0", "4.0.0")).isNegative(); assertThat(DependencyCheckTools.compareVersions("4.19.0-SNAPSHOT", "4.19.0")).isZero(); } + + // ---- POM sanitization ---- + + private static final String POM_WITH_SENSITIVE_DATA = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + <maven.compiler.release>21</maven.compiler.release> + <db.password>superSecret123</db.password> + </properties> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + @Test + void sanitizationMasksSensitiveData() throws Exception { + String json = tools.camel_dependency_check(POM_WITH_SENSITIVE_DATA, null, null, null, null, null); + JsonObject result = (JsonObject) Jsoner.deserialize(json); + + // Should have sanitization warnings + JsonArray warnings = (JsonArray) result.get("sanitizationWarnings"); + assertThat(warnings).isNotNull(); + assertThat(warnings).isNotEmpty(); + assertThat(warnings.stream().map(Object::toString).toList()) + .anyMatch(w -> w.contains("db.password")); + } + + @Test + void sanitizationDisabledWhenFalse() throws Exception { + String json = tools.camel_dependency_check(POM_WITH_SENSITIVE_DATA, null, null, null, null, false); + JsonObject result = (JsonObject) Jsoner.deserialize(json); + + // Should NOT have sanitization warnings + assertThat(result.get("sanitizationWarnings")).isNull(); + } + + @Test + void sanitizationStillParsesCorrectly() throws Exception { + String json = tools.camel_dependency_check(POM_WITH_SENSITIVE_DATA, null, null, null, null, null); + JsonObject result = (JsonObject) Jsoner.deserialize(json); + JsonObject projectInfo = result.getMap("projectInfo"); + + // Core analysis should still work after sanitization + assertThat(projectInfo.getString("camelVersion")).isEqualTo("4.10.0"); + assertThat(projectInfo.getString("runtimeType")).isEqualTo("main"); + } } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizerTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizerTest.java new file mode 100644 index 000000000000..6f2758d72804 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizerTest.java @@ -0,0 +1,317 @@ +/* + * 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.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PomSanitizerTest { + + // A clean POM with no sensitive data + private static final String CLEAN_POM = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + <maven.compiler.release>21</maven.compiler.release> + </properties> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + // A POM with various sensitive elements + private static final String POM_WITH_CREDENTIALS = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + <db.password>superSecret123</db.password> + <api.token>tok_abc123xyz</api.token> + </properties> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + // A POM with property placeholders (not actual secrets) + private static final String POM_WITH_PLACEHOLDERS = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + <db.password>${env.DB_PASSWORD}</db.password> + <api.token>${env.API_TOKEN}</api.token> + </properties> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + // A POM with servers section + private static final String POM_WITH_SERVERS = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + </properties> + <servers> + <server> + <id>my-repo</id> + <username>admin</username> + <password>repoPassword</password> + </server> + </servers> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + // A POM with distributionManagement section + private static final String POM_WITH_DIST_MGMT = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + </properties> + <distributionManagement> + <repository> + <id>internal-releases</id> + <url>https://private.repo.example.com/releases</url> + </repository> + </distributionManagement> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + // A POM with multiple sensitive patterns + private static final String POM_WITH_MULTIPLE_SENSITIVE = """ + <project> + <properties> + <camel.version>4.10.0</camel.version> + <secret>myAppSecret</secret> + <apiKey>key_12345</apiKey> + <accessKey>AKIA1234567890</accessKey> + </properties> + <servers> + <server> + <id>repo</id> + <password>pass</password> + </server> + </servers> + <distributionManagement> + <repository> + <id>releases</id> + <url>https://repo.example.com/releases</url> + </repository> + </distributionManagement> + <dependencies> + <dependency> + <groupId>org.apache.camel</groupId> + <artifactId>camel-core</artifactId> + </dependency> + </dependencies> + </project> + """; + + // ---- Detection tests ---- + + @Test + void detectsPasswordElement() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_CREDENTIALS); + assertThat(findings).anyMatch(f -> f.contains("password")); + } + + @Test + void detectsTokenElement() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_CREDENTIALS); + assertThat(findings).anyMatch(f -> f.contains("token")); + } + + @Test + void detectsApiKeyElement() { + String pom = "<project><properties><apiKey>key123</apiKey></properties></project>"; + List<String> findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("apiKey")); + } + + @Test + void detectsSecretElement() { + String pom = "<project><properties><secret>s3cr3t</secret></properties></project>"; + List<String> findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("secret")); + } + + @Test + void detectsPropertyStyleNames() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_CREDENTIALS); + assertThat(findings).anyMatch(f -> f.equals("db.password")); + assertThat(findings).anyMatch(f -> f.equals("api.token")); + } + + @Test + void ignoresPropertyPlaceholders() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_PLACEHOLDERS); + // Property placeholders should not be flagged as sensitive + assertThat(findings).noneMatch(f -> f.equals("db.password")); + assertThat(findings).noneMatch(f -> f.equals("api.token")); + } + + @Test + void detectsServersSection() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_SERVERS); + assertThat(findings).anyMatch(f -> f.contains("<servers>")); + } + + @Test + void detectsDistributionManagementSection() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_DIST_MGMT); + assertThat(findings).anyMatch(f -> f.contains("<distributionManagement>")); + } + + @Test + void noDetectionForCleanPom() { + List<String> findings = PomSanitizer.detectSensitiveContent(CLEAN_POM); + assertThat(findings).isEmpty(); + } + + @Test + void detectsMultipleSensitiveElements() { + List<String> findings = PomSanitizer.detectSensitiveContent(POM_WITH_MULTIPLE_SENSITIVE); + assertThat(findings.size()).isGreaterThanOrEqualTo(3); + } + + // ---- Sanitization tests ---- + + @Test + void masksPasswordValues() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_CREDENTIALS); + assertThat(result.pomContent()).contains("<db.password>***MASKED***</db.password>"); + assertThat(result.pomContent()).doesNotContain("superSecret123"); + } + + @Test + void masksTokenValues() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_CREDENTIALS); + assertThat(result.pomContent()).contains("<api.token>***MASKED***</api.token>"); + assertThat(result.pomContent()).doesNotContain("tok_abc123xyz"); + } + + @Test + void preservesPropertyPlaceholders() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_PLACEHOLDERS); + assertThat(result.pomContent()).contains("${env.DB_PASSWORD}"); + assertThat(result.pomContent()).contains("${env.API_TOKEN}"); + } + + @Test + void stripsServersSection() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_SERVERS); + assertThat(result.pomContent()).doesNotContain("<servers>"); + assertThat(result.pomContent()).doesNotContain("repoPassword"); + assertThat(result.wasSanitized()).isTrue(); + } + + @Test + void stripsDistributionManagement() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_DIST_MGMT); + assertThat(result.pomContent()).doesNotContain("<distributionManagement>"); + assertThat(result.pomContent()).doesNotContain("private.repo.example.com"); + assertThat(result.wasSanitized()).isTrue(); + } + + @Test + void cleanPomUnchanged() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(CLEAN_POM); + assertThat(result.pomContent()).isEqualTo(CLEAN_POM); + assertThat(result.wasSanitized()).isFalse(); + assertThat(result.detectedPatterns()).isEmpty(); + } + + @Test + void sanitizedPomStillParseable() throws Exception { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_CREDENTIALS); + // Should still be valid XML that can be parsed + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(result.pomContent()); + assertThat(pom.camelVersion()).isEqualTo("4.10.0"); + } + + @Test + void sanitizesMultiplePatterns() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_MULTIPLE_SENSITIVE); + assertThat(result.pomContent()).doesNotContain("myAppSecret"); + assertThat(result.pomContent()).doesNotContain("key_12345"); + assertThat(result.pomContent()).doesNotContain("AKIA1234567890"); + assertThat(result.pomContent()).doesNotContain("<servers>"); + assertThat(result.pomContent()).doesNotContain("<distributionManagement>"); + assertThat(result.wasSanitized()).isTrue(); + assertThat(result.detectedPatterns().size()).isGreaterThanOrEqualTo(3); + } + + @Test + void resultReportsWasSanitized() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_CREDENTIALS); + assertThat(result.wasSanitized()).isTrue(); + assertThat(result.detectedPatterns()).isNotEmpty(); + } + + @Test + void resultReportsNotSanitizedForCleanPom() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(CLEAN_POM); + assertThat(result.wasSanitized()).isFalse(); + } + + @Test + void caseInsensitiveDetection() { + String pom = "<project><properties><PASSWORD>secret</PASSWORD></properties></project>"; + List<String> findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).isNotEmpty(); + } + + @Test + void detectsAccessKeyElement() { + String pom = "<project><properties><accessKey>AKIA123</accessKey></properties></project>"; + List<String> findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("accessKey")); + } + + @Test + void detectsPassphraseElement() { + String pom = "<project><properties><passphrase>my-passphrase</passphrase></properties></project>"; + List<String> findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("passphrase")); + } +}
