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..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 @@ -51,27 +51,41 @@ 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 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") 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") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { + PomSanitizer.ProcessedPom processed = PomSanitizer.process(pomContent, sanitizePom); + CamelCatalog catalog = catalogService.loadCatalog(runtime, camelVersion, platformBom); - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processed.content()); JsonObject result = new JsonObject(); + // Add sanitization warnings if any + if (!processed.warnings().isEmpty()) { + JsonArray sanitizationArr = new JsonArray(); + processed.warnings().forEach(sanitizationArr::add); + result.put("sanitizationWarnings", sanitizationArr); + } + // Project info JsonObject projectInfo = new JsonObject(); projectInfo.put("camelVersion", pom.camelVersion()); @@ -92,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(pomContent, 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 f62bc97157e2d..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 @@ -49,21 +49,28 @@ 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 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") 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") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + PomSanitizer.ProcessedPom processed = PomSanitizer.process(pomContent, sanitizePom); + + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processed.content()); String runtimeType = pom.runtimeType(); int majorVersion = pom.majorVersion(); - List warnings = new ArrayList<>(); + 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 17f938204dc2b..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 @@ -48,18 +48,25 @@ 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 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") 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") Boolean sanitizePom) { if (pomContent == null || pomContent.isBlank()) { throw new ToolCallException("pomContent is required", null); } try { - MigrationData.PomAnalysis pom = MigrationData.parsePomContent(pomContent); + PomSanitizer.ProcessedPom processed = PomSanitizer.process(pomContent, sanitizePom); + + MigrationData.PomAnalysis pom = MigrationData.parsePomContent(processed.content()); String sourceRuntime = pom.isWildfly() ? "wildfly" : pom.isKaraf() ? "karaf" : "unknown"; String resolvedTarget = targetRuntime != null && !targetRuntime.isBlank() @@ -83,7 +90,7 @@ public WildflyKarafMigrationResult camel_migration_wildfly_karaf( .collect(Collectors.toList()); // Warnings specific to the source runtime - List warnings = new ArrayList<>(); + 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 new file mode 100644 index 0000000000000..ecb50eddb2d4a --- /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,146 @@ +/* + * 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 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) 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 { + + 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); + + private PomSanitizer() { + } + + /** + * Detect sensitive content patterns in POM content. + * + * @return list of element names that contain sensitive values + */ + 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)); + } + } + + return new ArrayList<>(findings); + } + + /** + * 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. + * + * @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***"); + }); + + if (!detected.isEmpty()) { + LOG.warnf("Sensitive data detected in pomContent: %s. Content was sanitized before processing.", detected); + } + + 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) { + } + + 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/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/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 new file mode 100644 index 0000000000000..d77e5a3d8e057 --- /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,241 @@ +/* + * 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 multiple sensitive patterns + private static final String POM_WITH_MULTIPLE_SENSITIVE = """ + + + 4.10.0 + myAppSecret + key_12345 + AKIA1234567890 + + + + 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 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 cleanPomUnchanged() { + PomSanitizer.SanitizationResult result = PomSanitizer.sanitize(CLEAN_POM); + assertThat(result.pomContent()).isEqualTo(CLEAN_POM); + 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.detectedPatterns().size()).isGreaterThanOrEqualTo(3); + } + + @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")); + } + + // ---- 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); + } +}