From b472c6854b8fe546a7a8bdcb9d10b9aed75f690d Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Mon, 30 Mar 2026 18:13:06 +0200 Subject: [PATCH 1/2] 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 and 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 --- .../commands/mcp/DependencyCheckTools.java | 35 +- .../core/commands/mcp/MigrationTools.java | 25 +- .../mcp/MigrationWildflyKarafTools.java | 27 +- .../jbang/core/commands/mcp/PomSanitizer.java | 136 ++++++++ .../mcp/DependencyCheckToolsTest.java | 89 +++-- .../core/commands/mcp/PomSanitizerTest.java | 317 ++++++++++++++++++ 6 files changed, 596 insertions(+), 33 deletions(-) create mode 100644 dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizer.java create mode 100644 dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PomSanitizerTest.java 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 2c964e5a91620..eb4201a80f154 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 and sections") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { + // Sanitize POM content + String processedPom = pomContent; + List 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 String camel_dependency_check( 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 f62bc97157e2d..a3b0d22effe62 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 and 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 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 warnings = new ArrayList<>(); + List 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 17f938204dc2b..1f83a74018cda 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 and 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 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 WildflyKarafMigrationResult camel_migration_wildfly_karaf( .collect(Collectors.toList()); // Warnings specific to the source runtime - List warnings = new ArrayList<>(); + List 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 0000000000000..766eb743b74ea --- /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. + *

+ * Scans for common credential patterns (passwords, tokens, API keys, secrets) and optionally strips or masks them. Also + * removes {@code } and {@code } 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*" + + "", + Pattern.CASE_INSENSITIVE); + + /** Pattern matching {@code ...} sections. */ + private static final Pattern SERVERS_SECTION_PATTERN = Pattern.compile( + ".*?", Pattern.DOTALL); + + /** Pattern matching {@code ...} sections. */ + private static final Pattern DIST_MGMT_SECTION_PATTERN = Pattern.compile( + ".*?", Pattern.DOTALL); + + private PomSanitizer() { + } + + /** + * Detect sensitive content patterns in POM content. + * + * @return list of descriptions of detected sensitive patterns + */ + static List detectSensitiveContent(String pomContent) { + Set 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(" section (may contain repository credentials)"); + } + + if (DIST_MGMT_SECTION_PATTERN.matcher(pomContent).find()) { + findings.add(" section (may contain private repository URLs)"); + } + + return new ArrayList<>(findings); + } + + /** + * Sanitize POM content by masking sensitive element values and stripping credential sections ({@code } and + * {@code }). + *

+ * 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 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***"); + }); + + // 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 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 de560acfafafc..4be2de6b648a0 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 @@ void blankPomThrows() { @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 @@ void resultContainsProjectInfo() throws Exception { @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 @@ void detectsSpringBootRuntime() throws Exception { @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 @@ void detectsOutdatedVersion() throws Exception { @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 @@ void detectsMissingKafkaDependency() throws Exception { // 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 @@ void noMissingDepsWhenAllPresent() throws Exception { // 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 @@ void noMissingDepsWhenAllPresent() throws Exception { 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 @@ void missingDepContainsSnippet() throws Exception { @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 @@ void coreComponentsNotReportedAsMissing() throws Exception { // 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 @@ void coreComponentsNotReportedAsMissing() throws Exception { @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 @@ void detectsVersionConflictWithBom() throws Exception { @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 @@ void noConflictWithPropertyPlaceholderVersion() throws Exception { @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 @@ void noConflictWithoutBom() throws Exception { @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 @@ void recommendsUpgradeWhenOutdated() throws Exception { @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 @@ void recommendsBomWhenMissing() throws Exception { 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 @@ void recommendsMissingDeps() throws Exception { 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 @@ void summaryShowsHealthyWhenNoIssues() throws Exception { 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 @@ void compareVersionsCorrectly() { 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 = """ + + + 4.10.0 + 21 + superSecret123 + + + + org.apache.camel + camel-core + + + + """; + + @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 0000000000000..6f2758d728045 --- /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 = """ + + + 4.10.0 + 21 + + + + org.apache.camel + camel-core + + + + """; + + // A POM with various sensitive elements + private static final String POM_WITH_CREDENTIALS = """ + + + 4.10.0 + superSecret123 + tok_abc123xyz + + + + org.apache.camel + camel-core + + + + """; + + // A POM with property placeholders (not actual secrets) + private static final String POM_WITH_PLACEHOLDERS = """ + + + 4.10.0 + ${env.DB_PASSWORD} + ${env.API_TOKEN} + + + + org.apache.camel + camel-core + + + + """; + + // A POM with servers section + private static final String POM_WITH_SERVERS = """ + + + 4.10.0 + + + + my-repo + admin + repoPassword + + + + + org.apache.camel + camel-core + + + + """; + + // A POM with distributionManagement section + private static final String POM_WITH_DIST_MGMT = """ + + + 4.10.0 + + + + internal-releases + https://private.repo.example.com/releases + + + + + org.apache.camel + camel-core + + + + """; + + // A POM with multiple sensitive patterns + private static final String POM_WITH_MULTIPLE_SENSITIVE = """ + + + 4.10.0 + myAppSecret + key_12345 + AKIA1234567890 + + + + repo + pass + + + + + releases + https://repo.example.com/releases + + + + + org.apache.camel + camel-core + + + + """; + + // ---- Detection tests ---- + + @Test + void detectsPasswordElement() { + List findings = PomSanitizer.detectSensitiveContent(POM_WITH_CREDENTIALS); + assertThat(findings).anyMatch(f -> f.contains("password")); + } + + @Test + void detectsTokenElement() { + List findings = PomSanitizer.detectSensitiveContent(POM_WITH_CREDENTIALS); + assertThat(findings).anyMatch(f -> f.contains("token")); + } + + @Test + void detectsApiKeyElement() { + String pom = "key123"; + List findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("apiKey")); + } + + @Test + void detectsSecretElement() { + String pom = "s3cr3t"; + List findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("secret")); + } + + @Test + void detectsPropertyStyleNames() { + List 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 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 findings = PomSanitizer.detectSensitiveContent(POM_WITH_SERVERS); + assertThat(findings).anyMatch(f -> f.contains("")); + } + + @Test + void detectsDistributionManagementSection() { + List findings = PomSanitizer.detectSensitiveContent(POM_WITH_DIST_MGMT); + assertThat(findings).anyMatch(f -> f.contains("")); + } + + @Test + void noDetectionForCleanPom() { + List findings = PomSanitizer.detectSensitiveContent(CLEAN_POM); + assertThat(findings).isEmpty(); + } + + @Test + void detectsMultipleSensitiveElements() { + List 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("***MASKED***"); + assertThat(result.pomContent()).doesNotContain("superSecret123"); + } + + @Test + void masksTokenValues() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_CREDENTIALS); + assertThat(result.pomContent()).contains("***MASKED***"); + 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(""); + 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(""); + 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(""); + assertThat(result.pomContent()).doesNotContain(""); + 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 = "secret"; + List findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).isNotEmpty(); + } + + @Test + void detectsAccessKeyElement() { + String pom = "AKIA123"; + List findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("accessKey")); + } + + @Test + void detectsPassphraseElement() { + String pom = "my-passphrase"; + List findings = PomSanitizer.detectSensitiveContent(pom); + assertThat(findings).anyMatch(f -> f.contains("passphrase")); + } +} From e70d594154e7c80f372802eb71ebe613e2030b61 Mon Sep 17 00:00:00 2001 From: Andrea Cosentino Date: Tue, 31 Mar 2026 09:26:31 +0200 Subject: [PATCH 2/2] CAMEL-23273: Address review feedback on POM sanitizer - Remove and section stripping (servers belongs to settings.xml, distributionManagement contains URLs not credentials) - Extract PomSanitizer.process() helper to eliminate code duplication across MigrationTools, DependencyCheckTools, and MigrationWildflyKarafTools - Consolidate per-pattern warnings into a single summary warning - Remove unused wasSanitized field from SanitizationResult - Document regex-based detection limitations (false positives/negatives) - Add sanitization integration tests for MigrationTools and MigrationWildflyKarafTools Co-Authored-By: Claude Opus 4.6 (1M context) --- .../commands/mcp/DependencyCheckTools.java | 27 ++-- .../core/commands/mcp/MigrationTools.java | 22 +--- .../mcp/MigrationWildflyKarafTools.java | 22 +--- .../jbang/core/commands/mcp/PomSanitizer.java | 76 ++++++----- .../core/commands/mcp/MigrationToolsTest.java | 98 ++++++++++++++ .../mcp/MigrationWildflyKarafToolsTest.java | 111 ++++++++++++++++ .../core/commands/mcp/PomSanitizerTest.java | 124 ++++-------------- 7 files changed, 296 insertions(+), 184 deletions(-) create mode 100644 dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java create mode 100644 dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafToolsTest.java 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 eb4201a80f154..9efe5211e4a49 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,7 +16,6 @@ */ package org.apache.camel.dsl.jbang.core.commands.mcp; -import java.util.ArrayList; import java.util.List; import jakarta.enterprise.context.ApplicationScoped; @@ -53,8 +52,8 @@ public class DependencyCheckTools { + "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. " - + "POM content is automatically sanitized to remove sensitive data (passwords, tokens, API keys, " - + "repository credentials) unless sanitizePom is set to false.") + + "POM content is automatically sanitized to mask sensitive data (passwords, tokens, API keys) " + + "unless sanitizePom is set to false.") public String camel_dependency_check( @ToolArg(description = "The pom.xml file content. " + "IMPORTANT: Avoid including sensitive data such as passwords, tokens, or API keys. " @@ -65,35 +64,25 @@ public String camel_dependency_check( @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, - @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials " - + "and stripping and sections") Boolean sanitizePom) { + @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - // Sanitize POM content - String processedPom = pomContent; - List 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); - } - } + PomSanitizer.ProcessedPom processed = PomSanitizer.process(pomContent, sanitizePom); CamelCatalog catalog = catalogService.loadCatalog(runtime, camelVersion, platformBom); - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processedPom); + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processed.content()); JsonObject result = new JsonObject(); // Add sanitization warnings if any - if (!sanitizationWarnings.isEmpty()) { + if (!processed.warnings().isEmpty()) { JsonArray sanitizationArr = new JsonArray(); - sanitizationWarnings.forEach(sanitizationArr::add); + processed.warnings().forEach(sanitizationArr::add); result.put("sanitizationWarnings", sanitizationArr); } @@ -117,7 +106,7 @@ public String camel_dependency_check( result.put("missingDependencies", missingDeps); // 3. Check for version conflicts (explicit overrides when BOM is present) - JsonArray conflicts = checkVersionConflicts(processedPom, pom); + JsonArray conflicts = checkVersionConflicts(processed.content(), 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 a3b0d22effe62..97a5d74fdcd4d 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 @@ -50,37 +50,27 @@ 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. " - + "POM content is automatically sanitized to remove sensitive data (passwords, tokens, API keys, " - + "repository credentials) unless sanitizePom is set to false.") + + "POM content is automatically sanitized to mask sensitive data (passwords, tokens, API keys) " + + "unless sanitizePom is set to false.") public ProjectAnalysisResult camel_migration_analyze( @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 and sections") Boolean sanitizePom) { + @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - // Sanitize POM content - String processedPom = pomContent; - List 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); - } - } + PomSanitizer.ProcessedPom processed = PomSanitizer.process(pomContent, sanitizePom); - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processedPom); + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processed.content()); String runtimeType = pom.runtimeType(); int majorVersion = pom.majorVersion(); - List warnings = new ArrayList<>(sanitizationWarnings); + List warnings = new ArrayList<>(processed.warnings()); 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 1f83a74018cda..f6b37903308d9 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 @@ -49,34 +49,24 @@ public class MigrationWildflyKarafTools { + "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. " - + "POM content is automatically sanitized to remove sensitive data (passwords, tokens, API keys, " - + "repository credentials) unless sanitizePom is set to false.") + + "POM content is automatically sanitized to mask sensitive data (passwords, tokens, API keys) " + + "unless sanitizePom is set to false.") public WildflyKarafMigrationResult camel_migration_wildfly_karaf( @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 = "If true (default), automatically sanitize POM content by masking credentials " - + "and stripping and sections") Boolean sanitizePom) { + @ToolArg(description = "If true (default), automatically sanitize POM content by masking credentials") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - // Sanitize POM content - String processedPom = pomContent; - List 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); - } - } + PomSanitizer.ProcessedPom processed = PomSanitizer.process(pomContent, sanitizePom); - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processedPom); + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processed.content()); String sourceRuntime = pom.isWildfly() ? "wildfly" : pom.isKaraf() ? "karaf" : "unknown"; String resolvedTarget = targetRuntime != null && !targetRuntime.isBlank() @@ -100,7 +90,7 @@ public WildflyKarafMigrationResult camel_migration_wildfly_karaf( .collect(Collectors.toList()); // Warnings specific to the source runtime - List warnings = new ArrayList<>(sanitizationWarnings); + List warnings = new ArrayList<>(processed.warnings()); 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 index 766eb743b74ea..ecb50eddb2d4a 100644 --- 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 @@ -22,15 +22,26 @@ import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.jboss.logging.Logger; /** * Utility to detect and sanitize sensitive data in POM content before processing. *

- * Scans for common credential patterns (passwords, tokens, API keys, secrets) and optionally strips or masks them. Also - * removes {@code } and {@code } sections which may contain private repository - * credentials and URLs. + * Scans for common credential patterns (passwords, tokens, API keys, secrets) in XML element values and masks them. + * Property placeholders (e.g., {@code ${db.password}}) are preserved since they reference external values and do not + * contain actual secrets. + *

+ * Limitations: Detection is tag-name-based using keyword matching. This means: + *

    + *
  • False positives — non-secret values in elements whose names happen to contain a keyword (e.g., + * {@code strict}, + * {@code 300}).
  • + *
  • False negatives — actual secrets in elements with non-obvious names (e.g., credentials embedded in JDBC + * URLs, or elements named {@code } where the singular form is not in the keyword list).
  • + *
+ * This heuristic is a best-effort safety net, not a guarantee. Users should still avoid passing sensitive data. */ final class PomSanitizer { @@ -50,21 +61,13 @@ final class PomSanitizer { + "", Pattern.CASE_INSENSITIVE); - /** Pattern matching {@code ...} sections. */ - private static final Pattern SERVERS_SECTION_PATTERN = Pattern.compile( - ".*?", Pattern.DOTALL); - - /** Pattern matching {@code ...} sections. */ - private static final Pattern DIST_MGMT_SECTION_PATTERN = Pattern.compile( - ".*?", Pattern.DOTALL); - private PomSanitizer() { } /** * Detect sensitive content patterns in POM content. * - * @return list of descriptions of detected sensitive patterns + * @return list of element names that contain sensitive values */ static List detectSensitiveContent(String pomContent) { Set findings = new LinkedHashSet<>(); @@ -78,20 +81,11 @@ static List detectSensitiveContent(String pomContent) { } } - if (SERVERS_SECTION_PATTERN.matcher(pomContent).find()) { - findings.add(" section (may contain repository credentials)"); - } - - if (DIST_MGMT_SECTION_PATTERN.matcher(pomContent).find()) { - findings.add(" section (may contain private repository URLs)"); - } - return new ArrayList<>(findings); } /** - * Sanitize POM content by masking sensitive element values and stripping credential sections ({@code } and - * {@code }). + * Sanitize POM content by masking sensitive element values. *

* Property placeholders (e.g., {@code ${db.password}}) are preserved since they do not contain actual secret * values. @@ -113,24 +107,40 @@ static SanitizationResult sanitize(String pomContent) { "<" + mr.group(1) + ">***MASKED***"); }); - // 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); + return new SanitizationResult(sanitized, detected); + } + + /** + * Process POM content with optional sanitization. This is the entry point for tool methods. + * + * @param pomContent the raw POM content + * @param sanitize if {@code null} or {@code true}, sanitize; if {@code false}, skip sanitization + * @return processed result with content and any warnings + */ + static ProcessedPom process(String pomContent, Boolean sanitize) { + if (sanitize != null && !sanitize) { + return new ProcessedPom(pomContent, List.of()); + } + SanitizationResult sr = sanitize(pomContent); + List warnings = new ArrayList<>(); + if (!sr.detectedPatterns().isEmpty()) { + warnings.add("Sensitive data detected and masked: " + + sr.detectedPatterns().stream().collect(Collectors.joining(", "))); + } + return new ProcessedPom(sr.pomContent(), warnings); } record SanitizationResult( String pomContent, - List detectedPatterns, - boolean wasSanitized) { + List detectedPatterns) { + } + + record ProcessedPom( + String content, + List warnings) { } } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java new file mode 100644 index 0000000000000..ed801bae50fd2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationToolsTest.java @@ -0,0 +1,98 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MigrationToolsTest { + + private final MigrationTools tools; + + MigrationToolsTest() { + tools = new MigrationTools(); + tools.migrationData = new MigrationData(); + } + + private static final String CLEAN_POM = """ + + + 3.20.0 + 17 + + + + org.apache.camel + camel-core + ${camel.version} + + + + """; + + private static final String POM_WITH_SENSITIVE_DATA = """ + + + 3.20.0 + 17 + superSecret123 + tok_abc123xyz + + + + org.apache.camel + camel-core + ${camel.version} + + + + """; + + // ---- POM sanitization ---- + + @Test + void sanitizationMasksSensitiveData() { + MigrationTools.ProjectAnalysisResult result = tools.camel_migration_analyze(POM_WITH_SENSITIVE_DATA, null); + + assertThat(result.warnings()).anyMatch(w -> w.contains("db.password")); + assertThat(result.warnings()).anyMatch(w -> w.contains("api.token")); + } + + @Test + void sanitizationDisabledWhenFalse() { + MigrationTools.ProjectAnalysisResult result = tools.camel_migration_analyze(POM_WITH_SENSITIVE_DATA, false); + + // No sanitization warnings when disabled + assertThat(result.warnings()).noneMatch(w -> w.contains("Sensitive data detected")); + } + + @Test + void analysisWorksAfterSanitization() { + MigrationTools.ProjectAnalysisResult result = tools.camel_migration_analyze(POM_WITH_SENSITIVE_DATA, null); + + assertThat(result.camelVersion()).isEqualTo("3.20.0"); + assertThat(result.runtimeType()).isEqualTo("main"); + } + + @Test + void cleanPomHasNoSanitizationWarnings() { + MigrationTools.ProjectAnalysisResult result = tools.camel_migration_analyze(CLEAN_POM, null); + + assertThat(result.warnings()).noneMatch(w -> w.contains("Sensitive data detected")); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafToolsTest.java new file mode 100644 index 0000000000000..d60dccb98f561 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafToolsTest.java @@ -0,0 +1,111 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MigrationWildflyKarafToolsTest { + + private final MigrationWildflyKarafTools tools; + + MigrationWildflyKarafToolsTest() { + tools = new MigrationWildflyKarafTools(); + tools.migrationData = new MigrationData(); + } + + // A WildFly POM with sensitive data + private static final String WILDFLY_POM_WITH_SENSITIVE_DATA = """ + + + 2.25.0 + superSecret123 + + + + org.apache.camel + camel-core + ${camel.version} + + + org.apache.camel + camel-cdi + ${camel.version} + + + + """; + + // A clean WildFly POM + private static final String WILDFLY_POM_CLEAN = """ + + + 2.25.0 + + + + org.apache.camel + camel-core + ${camel.version} + + + org.apache.camel + camel-cdi + ${camel.version} + + + + """; + + // ---- POM sanitization ---- + + @Test + void sanitizationMasksSensitiveData() { + MigrationWildflyKarafTools.WildflyKarafMigrationResult result + = tools.camel_migration_wildfly_karaf(WILDFLY_POM_WITH_SENSITIVE_DATA, null, "4.18.0", null); + + assertThat(result.warnings()).anyMatch(w -> w.contains("db.password")); + } + + @Test + void sanitizationDisabledWhenFalse() { + MigrationWildflyKarafTools.WildflyKarafMigrationResult result + = tools.camel_migration_wildfly_karaf(WILDFLY_POM_WITH_SENSITIVE_DATA, null, "4.18.0", false); + + // No sanitization warnings when disabled + assertThat(result.warnings()).noneMatch(w -> w.contains("Sensitive data detected")); + } + + @Test + void analysisWorksAfterSanitization() { + MigrationWildflyKarafTools.WildflyKarafMigrationResult result + = tools.camel_migration_wildfly_karaf(WILDFLY_POM_WITH_SENSITIVE_DATA, null, "4.18.0", null); + + assertThat(result.sourceRuntime()).isEqualTo("wildfly"); + assertThat(result.sourceCamelVersion()).isEqualTo("2.25.0"); + assertThat(result.targetRuntime()).isEqualTo("quarkus"); + } + + @Test + void cleanPomHasNoSanitizationWarnings() { + MigrationWildflyKarafTools.WildflyKarafMigrationResult result + = tools.camel_migration_wildfly_karaf(WILDFLY_POM_CLEAN, null, "4.18.0", null); + + assertThat(result.warnings()).noneMatch(w -> w.contains("Sensitive data detected")); + } +} 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 index 6f2758d728045..d77e5a3d8e057 100644 --- 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 @@ -74,49 +74,6 @@ class PomSanitizerTest { """; - // A POM with servers section - private static final String POM_WITH_SERVERS = """ - - - 4.10.0 - - - - my-repo - admin - repoPassword - - - - - org.apache.camel - camel-core - - - - """; - - // A POM with distributionManagement section - private static final String POM_WITH_DIST_MGMT = """ - - - 4.10.0 - - - - internal-releases - https://private.repo.example.com/releases - - - - - org.apache.camel - camel-core - - - - """; - // A POM with multiple sensitive patterns private static final String POM_WITH_MULTIPLE_SENSITIVE = """ @@ -126,18 +83,6 @@ class PomSanitizerTest { key_12345 AKIA1234567890 - - - repo - pass - - - - - releases - https://repo.example.com/releases - - org.apache.camel @@ -190,18 +135,6 @@ void ignoresPropertyPlaceholders() { assertThat(findings).noneMatch(f -> f.equals("api.token")); } - @Test - void detectsServersSection() { - List findings = PomSanitizer.detectSensitiveContent(POM_WITH_SERVERS); - assertThat(findings).anyMatch(f -> f.contains("")); - } - - @Test - void detectsDistributionManagementSection() { - List findings = PomSanitizer.detectSensitiveContent(POM_WITH_DIST_MGMT); - assertThat(findings).anyMatch(f -> f.contains("")); - } - @Test void noDetectionForCleanPom() { List findings = PomSanitizer.detectSensitiveContent(CLEAN_POM); @@ -237,27 +170,10 @@ void preservesPropertyPlaceholders() { assertThat(result.pomContent()).contains("${env.API_TOKEN}"); } - @Test - void stripsServersSection() { - PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(POM_WITH_SERVERS); - assertThat(result.pomContent()).doesNotContain(""); - 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(""); - 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(); } @@ -275,25 +191,9 @@ void sanitizesMultiplePatterns() { assertThat(result.pomContent()).doesNotContain("myAppSecret"); assertThat(result.pomContent()).doesNotContain("key_12345"); assertThat(result.pomContent()).doesNotContain("AKIA1234567890"); - assertThat(result.pomContent()).doesNotContain(""); - assertThat(result.pomContent()).doesNotContain(""); - 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 = "secret"; @@ -314,4 +214,28 @@ void detectsPassphraseElement() { List findings = PomSanitizer.detectSensitiveContent(pom); assertThat(findings).anyMatch(f -> f.contains("passphrase")); } + + // ---- Process helper tests ---- + + @Test + void processReturnsSingleSummaryWarning() { + PomSanitizer.ProcessedPom result = PomSanitizer.process(POM_WITH_CREDENTIALS, null); + assertThat(result.warnings()).hasSize(1); + assertThat(result.warnings().get(0)).contains("db.password"); + assertThat(result.warnings().get(0)).contains("api.token"); + } + + @Test + void processSkipsSanitizationWhenFalse() { + PomSanitizer.ProcessedPom result = PomSanitizer.process(POM_WITH_CREDENTIALS, false); + assertThat(result.warnings()).isEmpty(); + assertThat(result.content()).isEqualTo(POM_WITH_CREDENTIALS); + } + + @Test + void processNoWarningsForCleanPom() { + PomSanitizer.ProcessedPom result = PomSanitizer.process(CLEAN_POM, null); + assertThat(result.warnings()).isEmpty(); + assertThat(result.content()).isEqualTo(CLEAN_POM); + } }