names = catalog.findBeansNames();
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 613624b8afe78..1002fcff0566c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -194,7 +194,8 @@ The assistant discovers components, looks up documentation, builds a YAML route,
=== Understanding, security, and testing
* _"Explain what this route does"_ — uses `camel_route_context` for catalog-enriched analysis
-* _"Analyze this route for security concerns"_ — uses `camel_route_harden_context` to detect hardcoded credentials, plain-text protocols, etc.
+* _"Analyze this route for security concerns"_ — uses `camel_route_harden_context` to detect hardcoded credentials, plain-text protocols, known CVE advisories, etc.
+* _"Is my Camel 4.10.1 project affected by known CVEs?"_ — uses `camel_security_advisories` to match the published Apache Camel security advisories against a Camel version or component
* _"Generate a JUnit 5 test for this route"_ — uses `camel_route_test_scaffold` to produce test class with mock endpoints and test-infra stubs
=== Error diagnosis
@@ -366,8 +367,28 @@ plus prompts that provide structured multi-step workflows.
| `camel_route_harden_context`
| Analyzes a route for security concerns. Identifies security-sensitive components, assigns risk levels, detects
issues like hardcoded credentials or plain-text protocols, and returns structured security findings alongside
- best practices.
-|===
+ best practices and the known published CVE advisories affecting the components used by the route at the given
+ Camel version.
+
+| `camel_security_advisories`
+| Lists the published Apache Camel CVE security advisories (the data behind
+ https://camel.apache.org/security/[camel.apache.org/security]), optionally filtered by Camel version, component
+ and severity. Each advisory includes the summary, affected and fixed versions, mitigation, and a best-effort
+ verdict on whether the given Camel version is affected. Use it to answer questions such as "is my Camel 4.10.1
+ project affected by known CVEs?".
+|===
+
+==== Security advisory data
+
+The advisory data ships with the Camel catalog bundled in the MCP server, where it is synced from the official
+published Apache Camel security advisories (the sources of
+https://camel.apache.org/security/[camel.apache.org/security]) when Camel is built — the same way the known
+releases are synced. Lookups are therefore fully offline, only published advisories are included, and the data
+is as fresh as the Camel version of the MCP server: advisories published after that release are not included,
+so check the web page for the very latest. When the catalog carries no advisory data the tools report it as
+unavailable rather than returning an empty list, so a missing data set is never mistaken for "no known CVEs".
+The advisories are also browseable as MCP resources: `camel://security/advisories` (full list) and
+`camel://security/advisory/\{cve}` (detail for one CVE).
=== Error Diagnosis
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryResources.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryResources.java
new file mode 100644
index 0000000000000..a56a1608f4e2d
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryResources.java
@@ -0,0 +1,127 @@
+/*
+ * 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.Optional;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.Resource;
+import io.quarkiverse.mcp.server.ResourceTemplate;
+import io.quarkiverse.mcp.server.ResourceTemplateArg;
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * MCP Resources exposing the published Apache Camel CVE security advisories from
+ * camel.apache.org/security (shipped with the Camel catalog).
+ *
+ * These resources provide browseable advisory data that clients can pull into context independently of route analysis.
+ * When the catalog carries no advisory data, the resources return an explicit {@code advisoryDataUnavailable} marker
+ * instead of an empty list, so a missing data set is never mistaken for "no known CVEs".
+ */
+@ApplicationScoped
+public class AdvisoryResources {
+
+ @Inject
+ AdvisoryService advisoryService;
+
+ /**
+ * All published security advisories (one summary entry per CVE).
+ */
+ @Resource(uri = "camel://security/advisories",
+ name = "camel_security_advisories_list",
+ title = "Published Camel CVE Security Advisories",
+ description = "List of all published Apache Camel CVE security advisories (the data behind "
+ + "https://camel.apache.org/security/) with severity, summary, fixed versions and link. "
+ + "The data ships with the Camel catalog bundled in this MCP server.",
+ mimeType = "application/json")
+ public TextResourceContents securityAdvisories() {
+ JsonObject result = new JsonObject();
+ try {
+ JsonArray advisories = new JsonArray();
+ for (SecurityAdvisoryModel advisory : advisoryService.advisories()) {
+ JsonObject json = new JsonObject();
+ json.put("cve", advisory.getCve());
+ json.put("severity", advisory.getSeverity());
+ json.put("summary", advisory.getSummary());
+ json.put("fixed", advisory.getFixed());
+ json.put("url", advisory.getUrl());
+ advisories.add(json);
+ }
+ result.put("advisories", advisories);
+ result.put("totalCount", advisories.size());
+ result.put("source", AdvisoryService.SECURITY_PAGE_URL);
+ } catch (Exception e) {
+ result.put("advisoryDataUnavailable", true);
+ result.put("message", e.getMessage());
+ }
+ return new TextResourceContents("camel://security/advisories", result.toJson(), "application/json");
+ }
+
+ /**
+ * Full detail for a single published security advisory.
+ */
+ @ResourceTemplate(uriTemplate = "camel://security/advisory/{cve}",
+ name = "camel_security_advisory_detail",
+ title = "Camel CVE Security Advisory Detail",
+ description = "Full detail for a single published Apache Camel security advisory (by CVE id, "
+ + "e.g. CVE-2025-27636) including affected and fixed versions, mitigation and "
+ + "affected components.",
+ mimeType = "application/json")
+ public TextResourceContents securityAdvisoryDetail(
+ @ResourceTemplateArg(name = "cve") String cve) {
+
+ String uri = "camel://security/advisory/" + cve;
+ String wanted = cve == null ? "" : cve.trim();
+
+ JsonObject result = new JsonObject();
+ try {
+ Optional found = advisoryService.advisories().stream()
+ .filter(advisory -> advisory.getCve().equalsIgnoreCase(wanted))
+ .findFirst();
+
+ if (found.isEmpty()) {
+ result.put("cve", cve);
+ result.put("found", false);
+ result.put("message", "No published Apache Camel security advisory found for '" + cve + "'. "
+ + "See " + AdvisoryService.SECURITY_PAGE_URL + " for the full list.");
+ } else {
+ SecurityAdvisoryModel advisory = found.get();
+ result.put("cve", advisory.getCve());
+ result.put("found", true);
+ result.put("severity", advisory.getSeverity());
+ result.put("summary", advisory.getSummary());
+ result.put("affected", advisory.getAffected());
+ result.put("fixed", advisory.getFixed());
+ result.put("mitigation", advisory.getMitigation());
+ result.put("date", advisory.getDate());
+ result.put("url", advisory.getUrl());
+ JsonArray components = new JsonArray();
+ components.addAll(advisory.getComponents());
+ result.put("components", components);
+ }
+ } catch (Exception e) {
+ result.put("advisoryDataUnavailable", true);
+ result.put("message", e.getMessage());
+ }
+ return new TextResourceContents(uri, result.toJson(), "application/json");
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryService.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryService.java
new file mode 100644
index 0000000000000..ad9ee7ffa3706
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryService.java
@@ -0,0 +1,218 @@
+/*
+ * 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.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
+
+/**
+ * Answers queries against the published Apache Camel CVE security advisories (the data behind
+ * camel.apache.org/security).
+ *
+ * The advisory data ships with the Camel catalog ({@link CamelCatalog#camelSecurityAdvisories()}), where it is synced
+ * from the camel-website sources by the {@code update-security-advisories} goal of the camel-package-maven-plugin, the
+ * same way the known releases are synced. Lookups are therefore fully offline; the data is as fresh as the catalog
+ * bundled with this MCP server.
+ *
+ * When the catalog contains no advisory data at all (which cannot happen for a correctly built catalog), the service
+ * fails with an explicit {@link AdvisoriesUnavailableException} rather than returning an empty result — an empty result
+ * must never be mistaken for "no known CVEs".
+ */
+@ApplicationScoped
+public class AdvisoryService {
+
+ static final String SECURITY_PAGE_URL = "https://camel.apache.org/security/";
+
+ private static final Pattern CVE_ID = Pattern.compile("CVE-(\\d{4})-(\\d+)");
+
+ // Version range grammars used by the advisory "affected" prose, in the order they are consumed:
+ // "4.10.0 before 4.10.2" / "4.10.0 up to but not including 4.10.2" -> affected range [from, to)
+ private static final Pattern RANGE_EXCLUSIVE = Pattern.compile(
+ "(\\d+(?:\\.\\d+){1,3})\\s+(?:before|up to but not including)\\s+(\\d+(?:\\.\\d+){1,3})");
+ // "2.9.0 up to 2.9.7" / "2.9.0 through 2.9.7" / "2.9.0 to 2.9.7" -> affected range [from, to]
+ private static final Pattern RANGE_INCLUSIVE = Pattern.compile(
+ "(\\d+(?:\\.\\d+){1,3})\\s+(?:up to|through|to)\\s+(\\d+(?:\\.\\d+){1,3})");
+ // "prior to 2.24.0" / "before 2.24.0" without a starting version -> everything below is affected
+ private static final Pattern BELOW = Pattern.compile(
+ "(?:prior to|before|until)\\s+(\\d+(?:\\.\\d+){1,3})");
+ // a remaining bare version is an exactly affected release, e.g. the trailing "2.12.0" in CVE-2013-4330
+ private static final Pattern VERSION_TOKEN = Pattern.compile("\\d+(?:\\.\\d+){1,3}");
+
+ private final CamelCatalog catalog;
+
+ public AdvisoryService() {
+ this(new DefaultCamelCatalog());
+ }
+
+ AdvisoryService(CamelCatalog catalog) {
+ this.catalog = catalog;
+ }
+
+ /**
+ * All published advisories from the catalog bundled with this MCP server.
+ *
+ * @throws AdvisoriesUnavailableException when the catalog carries no advisory data
+ */
+ public List advisories() {
+ List advisories = catalog.camelSecurityAdvisories();
+ if (advisories == null || advisories.isEmpty()) {
+ throw new AdvisoriesUnavailableException(
+ "Security advisory data is not available in the Camel catalog used by this MCP server. "
+ + "See " + SECURITY_PAGE_URL
+ + " for the published advisories.");
+ }
+ return advisories;
+ }
+
+ /**
+ * Filter advisories and flatten them to {@link AdvisoryView}, newest first. When {@code camelVersion} is given,
+ * advisories whose parsed affected ranges exclude that version are dropped; advisories whose ranges could not be
+ * parsed are kept with {@code affectsGivenVersion} unset so the caller can judge from the {@code affected} prose.
+ */
+ static List query(
+ List advisories, String camelVersion, String component, String severity) {
+ List result = new ArrayList<>();
+ for (SecurityAdvisoryModel advisory : advisories) {
+ if (!matchesComponent(advisory, component) || !matchesSeverity(advisory, severity)) {
+ continue;
+ }
+ AdvisoryView view = AdvisoryView.of(advisory, camelVersion);
+ if (Boolean.FALSE.equals(view.affectsGivenVersion())) {
+ continue;
+ }
+ result.add(view);
+ }
+ result.sort(Comparator.comparingLong((AdvisoryView view) -> cveOrdinal(view.cve())).reversed());
+ return result;
+ }
+
+ /**
+ * Whether the advisory names the given component (with or without the {@code camel-} prefix). Best-effort: the
+ * match is against the components named by the advisory text, and some (mostly older) advisories do not name
+ * components at all.
+ */
+ static boolean matchesComponent(SecurityAdvisoryModel advisory, String component) {
+ if (component == null || component.isBlank()) {
+ return true;
+ }
+ String name = component.trim().toLowerCase(Locale.ROOT);
+ if (!name.startsWith("camel-")) {
+ name = "camel-" + name;
+ }
+ return advisory.getComponents() != null && advisory.getComponents().contains(name);
+ }
+
+ static boolean matchesSeverity(SecurityAdvisoryModel advisory, String severity) {
+ if (severity == null || severity.isBlank()) {
+ return true;
+ }
+ return advisory.getSeverity() != null && advisory.getSeverity().equalsIgnoreCase(severity.trim());
+ }
+
+ /**
+ * Best-effort verdict on whether the given Camel version falls inside the affected ranges stated by the advisory
+ * {@code affected} prose. Returns {@code null} when the prose contains no parseable version ranges — the caller
+ * must then fall back to the prose itself.
+ */
+ static Boolean affectsVersion(String affected, String version) {
+ if (affected == null || affected.isBlank() || version == null || version.isBlank()) {
+ return null;
+ }
+ // strip qualifiers such as -SNAPSHOT or vendor suffixes before comparing
+ String plainVersion = version.trim();
+ int dash = plainVersion.indexOf('-');
+ if (dash > 0) {
+ plainVersion = plainVersion.substring(0, dash);
+ }
+
+ boolean parsedAny = false;
+ boolean affectedHit = false;
+ String text = affected;
+
+ Matcher matcher = RANGE_EXCLUSIVE.matcher(text);
+ while (matcher.find()) {
+ parsedAny = true;
+ affectedHit |= VersionHelper.isBetween(plainVersion, matcher.group(1), matcher.group(2));
+ }
+ text = matcher.replaceAll(" ");
+
+ matcher = RANGE_INCLUSIVE.matcher(text);
+ while (matcher.find()) {
+ parsedAny = true;
+ affectedHit |= VersionHelper.isGE(plainVersion, matcher.group(1))
+ && VersionHelper.isLE(plainVersion, matcher.group(2));
+ }
+ text = matcher.replaceAll(" ");
+
+ matcher = BELOW.matcher(text);
+ while (matcher.find()) {
+ parsedAny = true;
+ affectedHit |= VersionHelper.compare(plainVersion, matcher.group(1)) < 0;
+ }
+ text = matcher.replaceAll(" ");
+
+ matcher = VERSION_TOKEN.matcher(text);
+ while (matcher.find()) {
+ parsedAny = true;
+ affectedHit |= VersionHelper.compare(plainVersion, matcher.group()) == 0
+ && plainVersion.length() == matcher.group().length();
+ }
+
+ return parsedAny ? affectedHit : null;
+ }
+
+ private static long cveOrdinal(String cve) {
+ Matcher matcher = CVE_ID.matcher(cve == null ? "" : cve);
+ if (matcher.find()) {
+ return Long.parseLong(matcher.group(1)) * 1_000_000L + Long.parseLong(matcher.group(2));
+ }
+ return 0;
+ }
+
+ /** Flattened advisory returned by MCP tools, with the optional per-version verdict. */
+ public record AdvisoryView(
+ String cve, String severity, String summary, String affected, String fixed,
+ String mitigation, String url, List components, Boolean affectsGivenVersion) {
+
+ static AdvisoryView of(SecurityAdvisoryModel advisory, String camelVersion) {
+ Boolean affects = camelVersion == null || camelVersion.isBlank()
+ ? null : affectsVersion(advisory.getAffected(), camelVersion);
+ return new AdvisoryView(
+ advisory.getCve(), advisory.getSeverity(), advisory.getSummary(), advisory.getAffected(),
+ advisory.getFixed(), advisory.getMitigation(), advisory.getUrl(), advisory.getComponents(),
+ affects);
+ }
+ }
+
+ /** Signals that the catalog used by this MCP server carries no advisory data. */
+ public static class AdvisoriesUnavailableException extends RuntimeException {
+ public AdvisoriesUnavailableException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
new file mode 100644
index 0000000000000..454873b1942d0
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
@@ -0,0 +1,90 @@
+/*
+ * 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 jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
+
+/**
+ * MCP Tool exposing the published Apache Camel CVE security advisories from
+ * camel.apache.org/security (shipped with the Camel catalog).
+ *
+ * Lets an LLM answer questions such as "is my Camel 4.10.1 project affected by known CVEs?" or "which CVEs were
+ * published for camel-kafka and in which versions are they fixed?".
+ */
+@ApplicationScoped
+public class AdvisoryTools {
+
+ @Inject
+ AdvisoryService advisoryService;
+
+ @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false),
+ description = "List published Apache Camel CVE security advisories (the data behind "
+ + "https://camel.apache.org/security/), optionally filtered by Camel version, component and "
+ + "severity. When camelVersion is set, advisories whose parsed affected ranges exclude that "
+ + "version are dropped; advisories whose ranges cannot be parsed are kept with "
+ + "affectsGivenVersion unset - judge those from the 'affected' text. The advisory data ships "
+ + "with the Camel catalog (synced from the published advisories when Camel is released), so "
+ + "advisories published after this Camel version was released are not included - check the "
+ + "web page for the very latest.")
+ public AdvisoriesResult camel_security_advisories(
+ @ToolArg(description = "Camel version to check, e.g. 4.10.1 (optional)") String camelVersion,
+ @ToolArg(description = "Component to filter by, e.g. kafka or camel-kafka (optional; best-effort match "
+ + "against components named in the advisory text - older advisories may not name "
+ + "components)") String component,
+ @ToolArg(description = "Severity to filter by: LOW, MEDIUM, HIGH, or CRITICAL "
+ + "(optional)") String severity) {
+ try {
+ List advisories = advisoryService.advisories();
+ List matches
+ = AdvisoryService.query(advisories, camelVersion, component, severity);
+ return new AdvisoriesResult(
+ AdvisoryService.SECURITY_PAGE_URL,
+ advisories.size(),
+ matches.size(),
+ blankToNull(camelVersion),
+ blankToNull(component),
+ blankToNull(severity),
+ matches);
+ } catch (AdvisoryService.AdvisoriesUnavailableException e) {
+ throw new ToolCallException(e.getMessage(), e);
+ } catch (ToolCallException e) {
+ throw e;
+ } catch (Throwable e) {
+ throw new ToolCallException(
+ "Failed to load security advisories (" + e.getClass().getName() + "): " + e.getMessage(), null);
+ }
+ }
+
+ private static String blankToNull(String value) {
+ return value == null || value.isBlank() ? null : value.trim();
+ }
+
+ // Result records
+
+ public record AdvisoriesResult(
+ String source, int totalPublished, int matched, String camelVersion, String component, String severity,
+ List advisories) {
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
index 22116be69e33c..ef7768dcc4fa8 100644
--- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
@@ -17,7 +17,11 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -27,6 +31,7 @@
import io.quarkiverse.mcp.server.ToolCallException;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.tooling.model.ComponentModel;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
/**
* MCP Tool for providing security hardening context and analysis for Camel routes.
@@ -37,20 +42,28 @@
@ApplicationScoped
public class HardenTools {
+ /** Upper bound of known-CVE advisories embedded in the hardening context to keep the response focused. */
+ private static final int MAX_ADVISORIES = 20;
+
@Inject
CatalogService catalogService;
@Inject
SecurityData securityData;
+ @Inject
+ AdvisoryService advisoryService;
+
/**
* Tool to get security hardening context for a Camel route.
*/
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false),
description = "Get security hardening analysis context for a Camel route. " +
"Returns security-sensitive components, potential vulnerabilities, " +
- "and security best practices. Use this context to provide security " +
- "hardening recommendations for the route.")
+ "security best practices, and known published CVE advisories affecting " +
+ "the components used by the route at the given Camel version (advisory data " +
+ "from https://camel.apache.org/security/ shipped with the Camel catalog). " +
+ "Use this context to provide security hardening recommendations for the route.")
public HardenContextResult camel_route_harden_context(
@ToolArg(description = "The Camel route content (YAML, XML, or Java DSL)") String route,
@ToolArg(description = "Route format: yaml, xml, or java (default: yaml)") String format,
@@ -85,6 +98,27 @@ public HardenContextResult camel_route_harden_context(
// Best practices
List bestPractices = List.copyOf(securityData.getBestPractices());
+ // Known published CVE advisories affecting the components used by the route.
+ // Failures here must degrade to a note, never break the hardening analysis.
+ List knownAdvisories = null;
+ String advisoriesNote = null;
+ try {
+ String effectiveVersion = camelVersion != null && !camelVersion.isBlank()
+ ? camelVersion : catalog.getCatalogVersion();
+ knownAdvisories = matchAdvisories(
+ advisoryService.advisories(), catalog, securityComponentNames, effectiveVersion);
+ if (knownAdvisories.size() > MAX_ADVISORIES) {
+ advisoriesNote = "Showing " + MAX_ADVISORIES + " of " + knownAdvisories.size()
+ + " matched advisories; use the camel_security_advisories tool for the full list.";
+ knownAdvisories = knownAdvisories.subList(0, MAX_ADVISORIES);
+ }
+ if (knownAdvisories.isEmpty()) {
+ knownAdvisories = null;
+ }
+ } catch (Exception e) {
+ advisoriesNote = "Known-CVE advisory check skipped: " + e.getMessage();
+ }
+
// Summary
HardenSummary summary = new HardenSummary(
securityComponents.size(),
@@ -95,7 +129,8 @@ public HardenContextResult camel_route_harden_context(
usesTLS(route), hasAuthentication(route));
return new HardenContextResult(
- resolvedFormat, route, securityComponents, securityAnalysis, bestPractices, summary);
+ resolvedFormat, route, securityComponents, securityAnalysis, bestPractices,
+ knownAdvisories, advisoriesNote, summary);
} catch (ToolCallException e) {
throw e;
} catch (Throwable e) {
@@ -120,6 +155,34 @@ private List extractSecurityComponents(String route) {
return found;
}
+ /**
+ * Match published CVE advisories against the components used by the route, keeping only advisories that affect the
+ * given Camel version (advisories whose affected ranges cannot be parsed are kept for the LLM to judge). Components
+ * are matched by Maven artifact id where the catalog knows it (e.g. scheme {@code https} maps to
+ * {@code camel-http}), with the scheme-derived {@code camel-} as fallback.
+ */
+ private List matchAdvisories(
+ List advisories, CamelCatalog catalog,
+ List componentNames, String camelVersion) {
+
+ Set artifacts = new LinkedHashSet<>();
+ for (String name : componentNames) {
+ ComponentModel model = catalog.componentModel(name);
+ if (model != null && model.getArtifactId() != null) {
+ artifacts.add(model.getArtifactId());
+ }
+ artifacts.add("camel-" + name);
+ }
+
+ Map byCve = new LinkedHashMap<>();
+ for (String artifact : artifacts) {
+ for (AdvisoryService.AdvisoryView view : AdvisoryService.query(advisories, camelVersion, artifact, null)) {
+ byCve.putIfAbsent(view.cve(), view);
+ }
+ }
+ return new ArrayList<>(byCve.values());
+ }
+
/**
* Analyze security concerns in the route.
*/
@@ -282,6 +345,7 @@ private boolean hasAuthentication(String route) {
public record HardenContextResult(
String format, String route, List securitySensitiveComponents,
SecurityAnalysis securityAnalysis, List securityBestPractices,
+ List knownSecurityAdvisories, String advisoriesNote,
HardenSummary summary) {
}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryResourcesTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryResourcesTest.java
new file mode 100644
index 0000000000000..2953aacbf0c7b
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryResourcesTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.mcp;
+
+import io.quarkiverse.mcp.server.TextResourceContents;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for the {@code camel://security/advisories} MCP resources, backed by the advisory data shipped with the Camel
+ * catalog. Fully offline.
+ */
+class AdvisoryResourcesTest {
+
+ private AdvisoryResources resources() {
+ AdvisoryResources resources = new AdvisoryResources();
+ resources.advisoryService = new AdvisoryService();
+ return resources;
+ }
+
+ @Test
+ void advisoriesListReturnsValidJson() throws Exception {
+ TextResourceContents contents = resources().securityAdvisories();
+
+ assertThat(contents.uri()).isEqualTo("camel://security/advisories");
+ assertThat(contents.mimeType()).isEqualTo("application/json");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getInteger("totalCount")).isGreaterThan(70);
+ assertThat(result.getString("source")).isEqualTo("https://camel.apache.org/security/");
+ assertThat(result.containsKey("advisoryDataUnavailable")).isFalse();
+
+ JsonArray advisories = result.getCollection("advisories");
+ JsonObject known = null;
+ for (Object o : advisories) {
+ JsonObject json = (JsonObject) o;
+ if ("CVE-2025-27636".equals(json.getString("cve"))) {
+ known = json;
+ }
+ }
+ assertThat(known).isNotNull();
+ assertThat(known.getString("severity")).isEqualTo("MEDIUM");
+ assertThat(known.getString("fixed")).contains("4.10.2");
+ assertThat(known.getString("url")).isEqualTo("https://camel.apache.org/security/CVE-2025-27636.html");
+ }
+
+ @Test
+ void advisoryDetailReturnsAllFields() throws Exception {
+ TextResourceContents contents = resources().securityAdvisoryDetail("CVE-2013-4330");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getBoolean("found")).isTrue();
+ assertThat(result.getString("cve")).isEqualTo("CVE-2013-4330");
+ assertThat(result.getString("severity")).isEqualTo("CRITICAL");
+ assertThat(result.getString("affected")).contains("2.9.0 up to 2.9.7");
+ assertThat(result.getString("fixed")).contains("2.12.1");
+ assertThat(result.getString("mitigation")).isNotBlank();
+ assertThat(result.getString("date")).isNotBlank();
+ }
+
+ @Test
+ void advisoryDetailIsCaseInsensitive() throws Exception {
+ TextResourceContents contents = resources().securityAdvisoryDetail("cve-2025-27636");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getBoolean("found")).isTrue();
+ JsonArray components = result.getCollection("components");
+ assertThat(components).contains("camel-bean", "camel-undertow");
+ }
+
+ @Test
+ void advisoryDetailReportsUnknownCve() throws Exception {
+ TextResourceContents contents = resources().securityAdvisoryDetail("CVE-1999-0001");
+
+ JsonObject result = (JsonObject) Jsoner.deserialize(contents.text());
+ assertThat(result.getBoolean("found")).isFalse();
+ assertThat(result.getString("message")).contains("No published Apache Camel security advisory");
+ }
+
+ @Test
+ void unavailableAdvisoryDataIsExplicitNotEmpty() throws Exception {
+ AdvisoryResources resources = new AdvisoryResources();
+ resources.advisoryService = AdvisoryServiceTest.unavailableService();
+
+ JsonObject list = (JsonObject) Jsoner.deserialize(resources.securityAdvisories().text());
+ assertThat(list.getBoolean("advisoryDataUnavailable")).isTrue();
+ assertThat(list.containsKey("advisories")).isFalse();
+
+ JsonObject detail = (JsonObject) Jsoner.deserialize(resources.securityAdvisoryDetail("CVE-2025-27636").text());
+ assertThat(detail.getBoolean("advisoryDataUnavailable")).isTrue();
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryServiceTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryServiceTest.java
new file mode 100644
index 0000000000000..7403951be7f09
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryServiceTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for {@link AdvisoryService}: loading the published advisories from the Camel catalog, the best-effort affected
+ * version matching, and the query filtering. Fully offline - the advisory data ships with camel-catalog.
+ */
+class AdvisoryServiceTest {
+
+ /** Builds a synthetic advisory for deterministic filter tests. */
+ static SecurityAdvisoryModel advisory(String cve, String severity, String affected, String... components) {
+ SecurityAdvisoryModel model = new SecurityAdvisoryModel();
+ model.setCve(cve);
+ model.setSeverity(severity);
+ model.setAffected(affected);
+ model.setSummary("Synthetic advisory " + cve);
+ model.setFixed("n/a");
+ model.setUrl("https://camel.apache.org/security/" + cve + ".html");
+ model.getComponents().addAll(List.of(components));
+ return model;
+ }
+
+ /** An {@link AdvisoryService} whose catalog carries no advisory data. */
+ static AdvisoryService unavailableService() {
+ return new AdvisoryService(new DefaultCamelCatalog() {
+ @Override
+ public List camelSecurityAdvisories() {
+ return List.of();
+ }
+ });
+ }
+
+ // ---- loading from the catalog ----
+
+ @Test
+ void loadsPublishedAdvisoriesFromCatalog() {
+ List advisories = new AdvisoryService().advisories();
+
+ assertThat(advisories.size()).isGreaterThan(70);
+
+ SecurityAdvisoryModel known = advisories.stream()
+ .filter(a -> "CVE-2025-27636".equals(a.getCve()))
+ .findFirst().orElse(null);
+ assertThat(known).isNotNull();
+ assertThat(known.getSeverity()).isEqualTo("MEDIUM");
+ assertThat(known.getAffected()).contains("4.10.0 before 4.10.2");
+ assertThat(known.getFixed()).contains("4.10.2");
+ assertThat(known.getUrl()).isEqualTo("https://camel.apache.org/security/CVE-2025-27636.html");
+ assertThat(known.getComponents()).contains("camel-bean", "camel-undertow", "camel-kafka");
+ }
+
+ @Test
+ void emptyCatalogDataFailsExplicitly() {
+ assertThatThrownBy(() -> unavailableService().advisories())
+ .isInstanceOf(AdvisoryService.AdvisoriesUnavailableException.class)
+ .hasMessageContaining("not available");
+ }
+
+ // ---- affected version matching ----
+
+ @Test
+ void affectsVersionWithExclusiveRanges() {
+ String affected = "Apache Camel 4.10.0 before 4.10.2. Apache Camel 4.8.0 before 4.8.5. "
+ + "Apache Camel 3.10.0 before 3.22.4.";
+
+ assertThat(AdvisoryService.affectsVersion(affected, "4.10.1")).isTrue();
+ assertThat(AdvisoryService.affectsVersion(affected, "4.10.0")).isTrue();
+ assertThat(AdvisoryService.affectsVersion(affected, "4.8.4")).isTrue();
+ assertThat(AdvisoryService.affectsVersion(affected, "3.15.0")).isTrue();
+
+ assertThat(AdvisoryService.affectsVersion(affected, "4.10.2")).isFalse();
+ assertThat(AdvisoryService.affectsVersion(affected, "4.8.5")).isFalse();
+ assertThat(AdvisoryService.affectsVersion(affected, "4.11.0")).isFalse();
+ assertThat(AdvisoryService.affectsVersion(affected, "3.22.4")).isFalse();
+ }
+
+ @Test
+ void affectsVersionWithInclusiveRangesAndBareVersion() {
+ // the affected style used by the old advisories, e.g. CVE-2013-4330
+ String affected = "2.9.0 up to 2.9.7, 2.10.0 up to 2.10.6, 2.11.0 up to 2.11.1, 2.12.0";
+
+ assertThat(AdvisoryService.affectsVersion(affected, "2.9.0")).isTrue();
+ assertThat(AdvisoryService.affectsVersion(affected, "2.9.7")).isTrue();
+ assertThat(AdvisoryService.affectsVersion(affected, "2.12.0")).isTrue();
+
+ assertThat(AdvisoryService.affectsVersion(affected, "2.9.8")).isFalse();
+ assertThat(AdvisoryService.affectsVersion(affected, "2.12.1")).isFalse();
+ assertThat(AdvisoryService.affectsVersion(affected, "2.8.0")).isFalse();
+ }
+
+ @Test
+ void affectsVersionStripsQualifierSuffix() {
+ String affected = "Apache Camel 4.10.0 before 4.10.2.";
+
+ assertThat(AdvisoryService.affectsVersion(affected, "4.10.1-SNAPSHOT")).isTrue();
+ assertThat(AdvisoryService.affectsVersion(affected, "4.10.2-SNAPSHOT")).isFalse();
+ }
+
+ @Test
+ void affectsVersionIsNullWhenNotParseable() {
+ assertThat(AdvisoryService.affectsVersion("All versions of Apache Camel", "4.10.1")).isNull();
+ assertThat(AdvisoryService.affectsVersion(null, "4.10.1")).isNull();
+ assertThat(AdvisoryService.affectsVersion(" ", "4.10.1")).isNull();
+ assertThat(AdvisoryService.affectsVersion("Apache Camel 4.10.0 before 4.10.2.", null)).isNull();
+ assertThat(AdvisoryService.affectsVersion("Apache Camel 4.10.0 before 4.10.2.", " ")).isNull();
+ }
+
+ // ---- query filtering ----
+
+ @Test
+ void queryFiltersByComponentSeverityAndVersion() {
+ List advisories = List.of(
+ advisory("CVE-2013-4330", "CRITICAL", "2.9.0 up to 2.9.7, 2.12.0"),
+ advisory("CVE-2025-27636", "MEDIUM", "Apache Camel 4.10.0 before 4.10.2.", "camel-bean",
+ "camel-undertow"));
+
+ // no filters: everything, newest first, no version verdict
+ List views = AdvisoryService.query(advisories, null, null, null);
+ assertThat(views).hasSize(2);
+ assertThat(views.get(0).cve()).isEqualTo("CVE-2025-27636");
+ assertThat(views.get(0).affectsGivenVersion()).isNull();
+
+ // component filter accepts both bare and camel- prefixed names
+ assertThat(AdvisoryService.query(advisories, null, "bean", null)).hasSize(1);
+ assertThat(AdvisoryService.query(advisories, null, "camel-bean", null)).hasSize(1);
+ assertThat(AdvisoryService.query(advisories, null, "does-not-exist", null)).isEmpty();
+
+ // severity filter is case-insensitive
+ assertThat(AdvisoryService.query(advisories, null, null, "medium")).hasSize(1);
+ assertThat(AdvisoryService.query(advisories, null, null, "CRITICAL")).hasSize(1);
+ assertThat(AdvisoryService.query(advisories, null, null, "IMPORTANT")).isEmpty();
+
+ // version filter drops advisories whose parsed ranges exclude the version
+ List affected = AdvisoryService.query(advisories, "4.10.1", null, null);
+ assertThat(affected).hasSize(1);
+ assertThat(affected.get(0).cve()).isEqualTo("CVE-2025-27636");
+ assertThat(affected.get(0).affectsGivenVersion()).isTrue();
+
+ assertThat(AdvisoryService.query(advisories, "4.10.2", null, null)).isEmpty();
+ assertThat(AdvisoryService.query(advisories, "2.12.0", null, null))
+ .singleElement()
+ .satisfies(view -> assertThat(view.cve()).isEqualTo("CVE-2013-4330"));
+ }
+
+ @Test
+ void queryKeepsAdvisoriesWithUnparseableRanges() {
+ List advisories
+ = List.of(advisory("CVE-2099-11111", "MEDIUM", "All versions", "camel-bar"));
+
+ List views = AdvisoryService.query(advisories, "4.10.1", null, null);
+
+ assertThat(views).hasSize(1);
+ assertThat(views.get(0).affectsGivenVersion()).isNull();
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryToolsTest.java
new file mode 100644
index 0000000000000..034c7025bb90f
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryToolsTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.mcp;
+
+import io.quarkiverse.mcp.server.ToolCallException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for the {@code camel_security_advisories} MCP tool, backed by the advisory data shipped with the Camel catalog.
+ * Fully offline.
+ */
+class AdvisoryToolsTest {
+
+ private AdvisoryTools tools() {
+ AdvisoryTools tools = new AdvisoryTools();
+ tools.advisoryService = new AdvisoryService();
+ return tools;
+ }
+
+ @Test
+ void listsAllPublishedAdvisoriesWithoutFilters() {
+ AdvisoryTools.AdvisoriesResult result = tools().camel_security_advisories(null, null, null);
+
+ assertThat(result.totalPublished()).isGreaterThan(70);
+ assertThat(result.matched()).isEqualTo(result.totalPublished());
+ assertThat(result.source()).isEqualTo("https://camel.apache.org/security/");
+ // newest first: the very first published Camel advisory comes last
+ assertThat(result.advisories().get(result.advisories().size() - 1).cve()).isEqualTo("CVE-2013-4330");
+ assertThat(result.advisories().get(0).affectsGivenVersion()).isNull();
+ }
+
+ @Test
+ void filtersByVersionComponentAndSeverity() {
+ AdvisoryTools.AdvisoriesResult result = tools().camel_security_advisories("4.10.1", "bean", "MEDIUM");
+
+ assertThat(result.camelVersion()).isEqualTo("4.10.1");
+ assertThat(result.component()).isEqualTo("bean");
+ assertThat(result.severity()).isEqualTo("MEDIUM");
+ assertThat(result.matched()).isGreaterThanOrEqualTo(1);
+
+ AdvisoryService.AdvisoryView view = result.advisories().stream()
+ .filter(a -> "CVE-2025-27636".equals(a.cve()))
+ .findFirst().orElse(null);
+ assertThat(view).isNotNull();
+ assertThat(view.affectsGivenVersion()).isTrue();
+ assertThat(view.fixed()).contains("4.10.2");
+ assertThat(view.mitigation()).isNotBlank();
+ }
+
+ @Test
+ void blankArgumentsAreTreatedAsNoFilter() {
+ AdvisoryTools.AdvisoriesResult result = tools().camel_security_advisories(" ", "", " ");
+
+ assertThat(result.matched()).isEqualTo(result.totalPublished());
+ assertThat(result.camelVersion()).isNull();
+ assertThat(result.component()).isNull();
+ assertThat(result.severity()).isNull();
+ }
+
+ @Test
+ void unavailableAdvisoryDataThrowsExplicitToolError() {
+ AdvisoryTools tools = new AdvisoryTools();
+ tools.advisoryService = AdvisoryServiceTest.unavailableService();
+
+ assertThatThrownBy(() -> tools.camel_security_advisories(null, null, null))
+ .isInstanceOf(ToolCallException.class)
+ .hasMessageContaining("not available");
+ }
+}
diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenToolsAdvisoryTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenToolsAdvisoryTest.java
new file mode 100644
index 0000000000000..896c4e1176595
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenToolsAdvisoryTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.apache.camel.tooling.model.SecurityAdvisoryModel;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the known-CVE advisory integration of the {@code camel_route_harden_context} tool: matched advisories are
+ * embedded in the hardening context, unaffected versions report nothing, and advisory lookup failures degrade to a note
+ * without breaking the hardening analysis.
+ */
+class HardenToolsAdvisoryTest {
+
+ private static final String KAFKA_ROUTE = """
+ - route:
+ from:
+ uri: kafka:orders
+ steps:
+ - to: log:done
+ """;
+
+ private HardenTools hardenTools(AdvisoryService advisoryService) {
+ HardenTools tools = new HardenTools();
+ tools.catalogService = new CatalogService();
+ tools.securityData = new SecurityData();
+ tools.advisoryService = advisoryService;
+ return tools;
+ }
+
+ private static AdvisoryService stubService(SecurityAdvisoryModel... advisories) {
+ return new AdvisoryService() {
+ @Override
+ public List advisories() {
+ return List.of(advisories);
+ }
+ };
+ }
+
+ @Test
+ void matchedAdvisoriesAreEmbeddedInHardenContext() {
+ // an advisory naming camel-kafka whose affected range cannot be parsed is kept for the LLM to judge
+ AdvisoryService stub = stubService(
+ AdvisoryServiceTest.advisory("CVE-2099-11111", "MEDIUM", "All versions", "camel-kafka"),
+ AdvisoryServiceTest.advisory("CVE-2099-22222", "HIGH", "All versions", "camel-ftp"));
+
+ HardenTools.HardenContextResult result = hardenTools(stub)
+ .camel_route_harden_context(KAFKA_ROUTE, "yaml", null, null, null);
+
+ assertThat(result.knownSecurityAdvisories()).hasSize(1);
+ assertThat(result.knownSecurityAdvisories().get(0).cve()).isEqualTo("CVE-2099-11111");
+ assertThat(result.knownSecurityAdvisories().get(0).affectsGivenVersion()).isNull();
+ assertThat(result.advisoriesNote()).isNull();
+ }
+
+ @Test
+ void unaffectedVersionReportsNoAdvisories() {
+ // the advisory affected range excludes the current catalog version, so nothing is reported
+ AdvisoryService stub = stubService(
+ AdvisoryServiceTest.advisory("CVE-2025-27636", "MEDIUM",
+ "Apache Camel 4.10.0 before 4.10.2.", "camel-kafka"));
+
+ HardenTools.HardenContextResult result = hardenTools(stub)
+ .camel_route_harden_context(KAFKA_ROUTE, "yaml", null, null, null);
+
+ assertThat(result.knownSecurityAdvisories()).isNull();
+ assertThat(result.advisoriesNote()).isNull();
+ }
+
+ @Test
+ void advisoryLookupFailureDegradesToNote() {
+ HardenTools.HardenContextResult result = hardenTools(AdvisoryServiceTest.unavailableService())
+ .camel_route_harden_context(KAFKA_ROUTE, "yaml", null, null, null);
+
+ assertThat(result).isNotNull();
+ assertThat(result.knownSecurityAdvisories()).isNull();
+ assertThat(result.advisoriesNote()).contains("Known-CVE advisory check skipped");
+ // the hardening analysis itself is unaffected by the advisory failure
+ assertThat(result.securitySensitiveComponents()).extracting(HardenTools.SecurityComponent::name)
+ .contains("kafka");
+ }
+}
diff --git a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
index 1f8d15deb97ef..40723b4e4acbe 100644
--- a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
+++ b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
@@ -1053,6 +1053,55 @@ public static ReleaseModel generateReleaseModel(JsonObject obj) {
return model;
}
+ public static JsonObject asJsonObject(SecurityAdvisoryModel model) {
+ JsonObject json = new JsonObject();
+ json.put("cve", model.getCve());
+ if (model.getDate() != null) {
+ json.put("date", model.getDate());
+ }
+ if (model.getSeverity() != null) {
+ json.put("severity", model.getSeverity());
+ }
+ if (model.getSummary() != null) {
+ json.put("summary", model.getSummary());
+ }
+ if (model.getAffected() != null) {
+ json.put("affected", model.getAffected());
+ }
+ if (model.getFixed() != null) {
+ json.put("fixed", model.getFixed());
+ }
+ if (model.getMitigation() != null) {
+ json.put("mitigation", model.getMitigation());
+ }
+ if (model.getUrl() != null) {
+ json.put("url", model.getUrl());
+ }
+ if (model.getComponents() != null && !model.getComponents().isEmpty()) {
+ json.put("components", new JsonArray(model.getComponents()));
+ }
+ return json;
+ }
+
+ public static SecurityAdvisoryModel generateSecurityAdvisoryModel(JsonObject obj) {
+ SecurityAdvisoryModel model = new SecurityAdvisoryModel();
+ model.setCve(obj.getString("cve"));
+ model.setDate(obj.getString("date"));
+ model.setSeverity(obj.getString("severity"));
+ model.setSummary(obj.getString("summary"));
+ model.setAffected(obj.getString("affected"));
+ model.setFixed(obj.getString("fixed"));
+ model.setMitigation(obj.getString("mitigation"));
+ model.setUrl(obj.getString("url"));
+ JsonArray components = (JsonArray) obj.get("components");
+ if (components != null) {
+ for (Object component : components) {
+ model.getComponents().add(String.valueOf(component));
+ }
+ }
+ return model;
+ }
+
public static String createJsonSchema(MainModel model) {
JsonObject wrapper = asJsonObject(model);
return serialize(wrapper);
diff --git a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/SecurityAdvisoryModel.java b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/SecurityAdvisoryModel.java
new file mode 100644
index 0000000000000..62f8632dd3b41
--- /dev/null
+++ b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/SecurityAdvisoryModel.java
@@ -0,0 +1,122 @@
+/*
+ * 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.tooling.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A published Apache Camel CVE security advisory, as listed on
+ * camel.apache.org/security.
+ */
+public class SecurityAdvisoryModel {
+
+ protected String cve;
+ protected String date;
+ protected String severity;
+ protected String summary;
+ protected String affected;
+ protected String fixed;
+ protected String mitigation;
+ protected String url;
+ protected List components = new ArrayList<>();
+
+ public String getCve() {
+ return cve;
+ }
+
+ public void setCve(String cve) {
+ this.cve = cve;
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public void setDate(String date) {
+ this.date = date;
+ }
+
+ public String getSeverity() {
+ return severity;
+ }
+
+ public void setSeverity(String severity) {
+ this.severity = severity;
+ }
+
+ public String getSummary() {
+ return summary;
+ }
+
+ public void setSummary(String summary) {
+ this.summary = summary;
+ }
+
+ /**
+ * The affected Camel versions as stated by the advisory, e.g. "Apache Camel 4.10.0 before 4.10.2".
+ */
+ public String getAffected() {
+ return affected;
+ }
+
+ public void setAffected(String affected) {
+ this.affected = affected;
+ }
+
+ /**
+ * The Camel versions that fix the vulnerability.
+ */
+ public String getFixed() {
+ return fixed;
+ }
+
+ public void setFixed(String fixed) {
+ this.fixed = fixed;
+ }
+
+ public String getMitigation() {
+ return mitigation;
+ }
+
+ public void setMitigation(String mitigation) {
+ this.mitigation = mitigation;
+ }
+
+ /**
+ * Link to the published advisory page.
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ /**
+ * The Camel components named by the advisory text (Maven artifact ids such as camel-kafka). May be empty when the
+ * advisory does not name specific components.
+ */
+ public List getComponents() {
+ return components;
+ }
+
+ public void setComponents(List components) {
+ this.components = components;
+ }
+}
diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/UpdateSecurityAdvisoriesMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/UpdateSecurityAdvisoriesMojo.java
new file mode 100644
index 0000000000000..16d3b64c3d52d
--- /dev/null
+++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/UpdateSecurityAdvisoriesMojo.java
@@ -0,0 +1,307 @@
+/*
+ * 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.maven.packaging;
+
+import java.io.File;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+
+import org.apache.camel.tooling.model.JsonMapper;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProjectHelper;
+import org.codehaus.plexus.build.BuildContext;
+import org.snakeyaml.engine.v2.api.Load;
+import org.snakeyaml.engine.v2.api.LoadSettings;
+
+/**
+ * Syncs the published Apache Camel CVE security advisories (the sources behind
+ * camel.apache.org/security, maintained as Markdown files with YAML
+ * front matter in the camel-website git repository) into a JSON file shipped with camel-catalog, the same way the known
+ * releases are synced by {@code update-camel-releases}.
+ */
+@Mojo(name = "update-security-advisories", threadSafe = true, defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
+public class UpdateSecurityAdvisoriesMojo extends AbstractGeneratorMojo {
+
+ private static final String GIT_SECURITY_URL
+ = "https://api.github.com/repos/apache/camel-website/contents/content/security";
+ private static final String WEBSITE_BASE_URL = "https://camel.apache.org";
+
+ private static final Pattern ADVISORY_FILE = Pattern.compile("CVE-\\d{4}-\\d{4,}\\.md");
+ // the first segment must start with a letter so that lowercased JIRA ids ("camel-12444") never match
+ private static final Pattern COMPONENT_TOKEN = Pattern.compile("camel-[a-z][a-z0-9]+(?:-[a-z0-9]+)*");
+ private static final Pattern CVE_ID = Pattern.compile("CVE-(\\d{4})-(\\d+)");
+
+ /**
+ * English prose that follows "Camel-" in advisory texts (e.g. "Camel-internal headers", "non-Camel-prefixed names")
+ * and therefore must not be mistaken for component artifact ids. Deliberately not validated against the current
+ * catalog instead: old advisories legitimately name components that have since been removed or renamed
+ * (camel-xstream, camel-hessian, camel-castor, camel-cxfrs, ...), which must remain filterable.
+ */
+ private static final Set COMPONENT_TOKEN_DENYLIST = Set.of(
+ "camel-internal", "camel-prefixed", "camel-specific", "camel-side", "camel-namespace",
+ "camel-case", "camel-cased", "camel-based", "camel-related", "camel-style");
+
+ /**
+ * The output directory for the generated catalog advisories file
+ */
+ @Parameter(defaultValue = "${project.basedir}/src/generated/resources/org/apache/camel/catalog/advisories")
+ protected File outDir;
+
+ @Inject
+ public UpdateSecurityAdvisoriesMojo(MavenProjectHelper projectHelper, BuildContext buildContext) {
+ super(projectHelper, buildContext);
+ }
+
+ @Override
+ public void execute() throws MojoExecutionException, MojoFailureException {
+ if (outDir == null) {
+ outDir = new File(project.getBasedir(), "src/generated/resources");
+ }
+
+ try {
+ getLog().info("Updating Camel security advisories from camel-website");
+ List links = fetchAdvisoryLinks();
+ List advisories = processAdvisories(links);
+ advisories.sort(Comparator.comparingLong(UpdateSecurityAdvisoriesMojo::cveOrdinal));
+ getLog().info("Found " + advisories.size() + " published security advisories");
+
+ JsonArray arr = new JsonArray();
+ for (SecurityAdvisoryModel advisory : advisories) {
+ arr.add(JsonMapper.asJsonObject(advisory));
+ }
+ String json = Jsoner.serialize(arr);
+ json = Jsoner.prettyPrint(json, 4);
+
+ Path path = outDir.toPath();
+ updateResource(path, "camel-security-advisories.json", json);
+ addResourceDirectory(path);
+ } catch (Exception e) {
+ throw new MojoExecutionException(e);
+ }
+ }
+
+ private List processAdvisories(List urls) throws Exception {
+ List answer = new ArrayList<>();
+
+ try (CloseableHttpClient hc = new CloseableHttpClient()) {
+ for (String url : urls) {
+ HttpResponse res
+ = hc.send(HttpRequest.newBuilder(new URI(url)).timeout(Duration.ofSeconds(20)).build(),
+ HttpResponse.BodyHandlers.ofString());
+
+ if (res.statusCode() == 200) {
+ SecurityAdvisoryModel model = parseAdvisory(res.body());
+ if (model != null) {
+ answer.add(model);
+ }
+ }
+ }
+ }
+
+ return answer;
+ }
+
+ /**
+ * Parse one advisory Markdown file (YAML front matter). Returns {@code null} for drafts, non-advisory pages and
+ * files without parseable front matter, so only published advisories are included.
+ */
+ static SecurityAdvisoryModel parseAdvisory(String content) {
+ Map frontMatter = frontMatter(content);
+ if (frontMatter == null) {
+ return null;
+ }
+ if (!"security-advisory".equals(str(frontMatter.get("type")))) {
+ return null;
+ }
+ Object draft = frontMatter.get("draft");
+ if (Boolean.TRUE.equals(draft) || "true".equalsIgnoreCase(str(draft))) {
+ return null;
+ }
+ String cve = str(frontMatter.get("cve"));
+ if (cve == null || cve.isBlank()) {
+ return null;
+ }
+
+ SecurityAdvisoryModel model = new SecurityAdvisoryModel();
+ model.setCve(cve.trim());
+ model.setDate(trimmed(frontMatter.get("date")));
+ String severity = str(frontMatter.get("severity"));
+ if (severity != null) {
+ model.setSeverity(severity.trim().toUpperCase(Locale.ROOT));
+ }
+ model.setSummary(trimmed(frontMatter.get("summary")));
+ model.setAffected(trimmed(frontMatter.get("affected")));
+ model.setFixed(trimmed(frontMatter.get("fixed")));
+ model.setMitigation(trimmed(frontMatter.get("mitigation")));
+
+ String url = str(frontMatter.get("url"));
+ if (url == null || url.isBlank()) {
+ url = "/security/" + model.getCve() + ".html";
+ }
+ if (url.startsWith("/")) {
+ url = WEBSITE_BASE_URL + url;
+ }
+ model.setUrl(url.trim());
+
+ model.setComponents(extractComponents(str(frontMatter.get("title")), model.getSummary(),
+ str(frontMatter.get("description")), model.getMitigation()));
+ return model;
+ }
+
+ @SuppressWarnings("unchecked")
+ static Map frontMatter(String content) {
+ if (content == null) {
+ return null;
+ }
+ String trimmedContent = content.stripLeading();
+ if (!trimmedContent.startsWith("---")) {
+ return null;
+ }
+ int start = trimmedContent.indexOf('\n');
+ if (start < 0) {
+ return null;
+ }
+ int end = trimmedContent.indexOf("\n---", start);
+ if (end < 0) {
+ return null;
+ }
+ String yaml = trimmedContent.substring(start + 1, end);
+ try {
+ Object parsed = new Load(LoadSettings.builder().build()).loadFromString(yaml);
+ return parsed instanceof Map ? (Map) parsed : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * The Camel components named by the advisory text, as {@code camel-*} tokens (best-effort: some older advisories do
+ * not name components at all). Lowercased JIRA ids and common English prose such as "Camel-internal" are filtered
+ * out.
+ */
+ static List extractComponents(String... texts) {
+ TreeSet found = new TreeSet<>();
+ for (String text : texts) {
+ if (text == null) {
+ continue;
+ }
+ Matcher matcher = COMPONENT_TOKEN.matcher(text.toLowerCase(Locale.ROOT));
+ while (matcher.find()) {
+ if (!COMPONENT_TOKEN_DENYLIST.contains(matcher.group())) {
+ found.add(matcher.group());
+ }
+ }
+ }
+ return new ArrayList<>(found);
+ }
+
+ private List fetchAdvisoryLinks() throws Exception {
+ List answer = new ArrayList<>();
+
+ // use JDK http client to call github api
+ try (CloseableHttpClient hc = new CloseableHttpClient()) {
+ HttpResponse res = hc.send(
+ HttpRequest.newBuilder(new URI(GIT_SECURITY_URL)).timeout(Duration.ofSeconds(20)).build(),
+ HttpResponse.BodyHandlers.ofString());
+
+ // follow redirect
+ if (res.statusCode() == 302) {
+ String loc = res.headers().firstValue("location").orElse(null);
+ if (loc != null) {
+ res = hc.send(HttpRequest.newBuilder(new URI(loc)).timeout(Duration.ofSeconds(20)).build(),
+ HttpResponse.BodyHandlers.ofString());
+ }
+ }
+
+ if (res.statusCode() == 200) {
+ JsonArray root = (JsonArray) Jsoner.deserialize(res.body());
+ for (Object o : root) {
+ JsonObject jo = (JsonObject) o;
+ String name = jo.getString("name");
+ if (name != null && ADVISORY_FILE.matcher(name).matches()) {
+ String url = jo.getString("download_url");
+ if (url != null) {
+ answer.add(url);
+ }
+ }
+ }
+ }
+ }
+
+ return answer;
+ }
+
+ private static long cveOrdinal(SecurityAdvisoryModel advisory) {
+ Matcher matcher = CVE_ID.matcher(advisory.getCve() == null ? "" : advisory.getCve());
+ if (matcher.find()) {
+ return Long.parseLong(matcher.group(1)) * 1_000_000L + Long.parseLong(matcher.group(2));
+ }
+ return 0;
+ }
+
+ private static String str(Object value) {
+ return value == null ? null : String.valueOf(value);
+ }
+
+ private static String trimmed(Object value) {
+ String text = str(value);
+ return text == null ? null : text.trim();
+ }
+
+ /**
+ * Wrapper that makes {@link HttpClient} usable in try-with-resources. On Java 21+ HttpClient implements
+ * AutoCloseable natively; the instanceof check future-proofs us for when the minimum JDK is raised.
+ */
+ private static final class CloseableHttpClient implements AutoCloseable {
+ private final HttpClient httpClient = HttpClient.newHttpClient();
+
+ HttpResponse send(HttpRequest request, HttpResponse.BodyHandler responseBodyHandler)
+ throws java.io.IOException, InterruptedException {
+ return httpClient.send(request, responseBodyHandler);
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (httpClient instanceof AutoCloseable closeable) {
+ closeable.close();
+ }
+ }
+ }
+}
diff --git a/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/UpdateSecurityAdvisoriesMojoTest.java b/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/UpdateSecurityAdvisoriesMojoTest.java
new file mode 100644
index 0000000000000..4cf0d166f3bc4
--- /dev/null
+++ b/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/UpdateSecurityAdvisoriesMojoTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.maven.packaging;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.camel.tooling.model.JsonMapper;
+import org.apache.camel.tooling.model.SecurityAdvisoryModel;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests the advisory front-matter parsing of {@link UpdateSecurityAdvisoriesMojo} against canned copies of two real
+ * published advisories: CVE-2025-27636 (recent front matter style) and CVE-2013-4330 (old style).
+ */
+class UpdateSecurityAdvisoriesMojoTest {
+
+ private static String fixture(String name) throws IOException {
+ try (InputStream in = UpdateSecurityAdvisoriesMojoTest.class
+ .getResourceAsStream("/security-advisories/" + name)) {
+ assertThat(in).as("fixture /security-advisories/" + name).isNotNull();
+ return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+
+ @Test
+ void parsesRecentAdvisoryStyle() throws Exception {
+ SecurityAdvisoryModel model = UpdateSecurityAdvisoriesMojo.parseAdvisory(fixture("CVE-2025-27636.md"));
+
+ assertThat(model).isNotNull();
+ assertThat(model.getCve()).isEqualTo("CVE-2025-27636");
+ assertThat(model.getSeverity()).isEqualTo("MEDIUM");
+ assertThat(model.getSummary()).contains("Header Injection");
+ assertThat(model.getAffected()).contains("4.10.0 before 4.10.2");
+ assertThat(model.getFixed()).contains("4.10.2");
+ assertThat(model.getMitigation()).isNotBlank();
+ assertThat(model.getUrl()).isEqualTo("https://camel.apache.org/security/CVE-2025-27636.html");
+ assertThat(model.getComponents())
+ .contains("camel-bean", "camel-undertow", "camel-kafka", "camel-platform-http");
+ }
+
+ @Test
+ void parsesOldAdvisoryStyle() throws Exception {
+ SecurityAdvisoryModel model = UpdateSecurityAdvisoriesMojo.parseAdvisory(fixture("CVE-2013-4330.md"));
+
+ assertThat(model).isNotNull();
+ assertThat(model.getCve()).isEqualTo("CVE-2013-4330");
+ assertThat(model.getSeverity()).isEqualTo("CRITICAL");
+ assertThat(model.getAffected()).startsWith("2.9.0 up to 2.9.7");
+ assertThat(model.getFixed()).contains("2.12.1");
+ assertThat(model.getUrl()).isEqualTo("https://camel.apache.org/security/CVE-2013-4330.html");
+ // the old advisory does not name camel-* components
+ assertThat(model.getComponents()).isEmpty();
+ }
+
+ @Test
+ void skipsDraftsAndNonAdvisories() {
+ String draft = """
+ ---
+ type: security-advisory
+ draft: true
+ cve: CVE-2099-99999
+ severity: LOW
+ ---
+ body
+ """;
+ assertThat(UpdateSecurityAdvisoriesMojo.parseAdvisory(draft)).isNull();
+
+ String wrongType = """
+ ---
+ type: blog-post
+ draft: false
+ cve: CVE-2099-99999
+ ---
+ body
+ """;
+ assertThat(UpdateSecurityAdvisoriesMojo.parseAdvisory(wrongType)).isNull();
+
+ assertThat(UpdateSecurityAdvisoriesMojo.parseAdvisory("no front matter")).isNull();
+ assertThat(UpdateSecurityAdvisoriesMojo.parseAdvisory(null)).isNull();
+ }
+
+ @Test
+ void componentExtractionSkipsJiraIdsAndProse() {
+ // lowercased JIRA ids and English prose following "Camel-" must not become component names
+ assertThat(UpdateSecurityAdvisoriesMojo.extractComponents(
+ "See CAMEL-12444 and CAMEL-23200 for the fix.",
+ "Camel-internal headers with non-Camel-prefixed names are Camel-specific.",
+ "The camel-side filter uses the Camel-namespace and camel-case naming on camel-kafka and camel-aws2-sqs."))
+ .containsExactly("camel-aws2-sqs", "camel-kafka");
+ }
+
+ @Test
+ void modelRoundTripsThroughJsonMapper() throws Exception {
+ SecurityAdvisoryModel model = UpdateSecurityAdvisoriesMojo.parseAdvisory(fixture("CVE-2025-27636.md"));
+
+ JsonObject json = JsonMapper.asJsonObject(model);
+ SecurityAdvisoryModel copy = JsonMapper.generateSecurityAdvisoryModel(json);
+
+ assertThat(copy.getCve()).isEqualTo(model.getCve());
+ assertThat(copy.getSeverity()).isEqualTo(model.getSeverity());
+ assertThat(copy.getSummary()).isEqualTo(model.getSummary());
+ assertThat(copy.getAffected()).isEqualTo(model.getAffected());
+ assertThat(copy.getFixed()).isEqualTo(model.getFixed());
+ assertThat(copy.getMitigation()).isEqualTo(model.getMitigation());
+ assertThat(copy.getUrl()).isEqualTo(model.getUrl());
+ assertThat(copy.getComponents()).isEqualTo(model.getComponents());
+ }
+}
diff --git a/tooling/maven/camel-package-maven-plugin/src/test/resources/security-advisories/CVE-2013-4330.md b/tooling/maven/camel-package-maven-plugin/src/test/resources/security-advisories/CVE-2013-4330.md
new file mode 100644
index 0000000000000..0568ebc756a48
--- /dev/null
+++ b/tooling/maven/camel-package-maven-plugin/src/test/resources/security-advisories/CVE-2013-4330.md
@@ -0,0 +1,23 @@
+---
+title: "Apache Camel Security Advisory - CVE-2013-4330"
+url: /security/CVE-2013-4330.html
+date: 2013-10-04T13:55:09.853000
+draft: false
+type: security-advisory
+cve: CVE-2013-4330
+severity: CRITICAL
+summary: "Writing files using FILE or FTP components, can potentially be exploited by a malicious user."
+description: "When sending an Exchange with the in Message Header 'CamelFileName' with a value of '$simple{...}' to a FILE or FTP producer, it will interpret the value as simple language expression which can be exploited by a malicious user."
+mitigation: "2.9.x users should upgrade to 2.9.8, 2.10.x users should upgrade to 2.10.7, 2.11.x users should upgrade to 2.11.2 and 2.12.0 users should upgrade to 2.12.1. This patch will be included from Camel 2.13.0: https://git-wip-us.apache.org/repos/asf?p=camel.git;a=commitdiff;h=27a9752a565fbef436bac4fcf22d339e3295b2a0"
+credit: "This issue was discovered by Grégory Draperi"
+affected: 2.9.0 up to 2.9.7, 2.10.0 up to 2.10.6, 2.11.0 up to 2.11.1, 2.12.0
+fixed: 2.9.8, 2.10.7, 2.11.2, 2.12.1 and newer
+---
+
+Example: Create a simple route which moves files from one directory to another, e.g.:
+
+ from("file:c:/tmp/in")
+ .to("file:/c:/tmp/out");
+
+If you are using Windows, create an file with a name like `"$simple{}"` (without the quotes) and drop it into the "c:/tmp/in" directory. The file consumer will read and process this file. It will also set the Exchange in Message Header '`CamelFileName`' with the value `"$simple{}"`. In the next step, the file producer will interpreted the value of this header as simple language expression and execute the malicious code.
+
diff --git a/tooling/maven/camel-package-maven-plugin/src/test/resources/security-advisories/CVE-2025-27636.md b/tooling/maven/camel-package-maven-plugin/src/test/resources/security-advisories/CVE-2025-27636.md
new file mode 100644
index 0000000000000..c561508b7948c
--- /dev/null
+++ b/tooling/maven/camel-package-maven-plugin/src/test/resources/security-advisories/CVE-2025-27636.md
@@ -0,0 +1,53 @@
+---
+title: "Apache Camel Security Advisory - CVE-2025-27636"
+date: 2025-03-09T04:30:42+02:00
+url: /security/CVE-2025-27636.html
+draft: false
+type: security-advisory
+cve: CVE-2025-27636
+severity: MEDIUM
+summary: "Camel Message Header Injection via Improper Filtering"
+description: "This vulnerability is only present in the following situation. The user is using one of the following HTTP Servers via one the of the following Camel components: camel-servlet, camel-jetty, camel-undertow, camel-platform-http and camel-netty-http and in the route, the exchange will be routed to a camel-bean producer. So ONLY camel-bean component is affected. In particular: The bean invocation (is only affected if you use any of the above together with camel-bean component) and the bean that can be called, has more than 1 method implemented. In these, limited and particular, conditions an attacker could be able to forge a Camel header name and make the bean component invoking other methods in the SAME bean. The vulnerability arises due to a bug in the default filtering mechanism that only blocks headers starting with 'Camel', 'camel', or 'org.apache.camel.'. This vulnerability is present in Camel's default incoming header filter, that allows an attacker to include Camel specific
+headers that for some Camel components can alter the behaviours such as the camel-bean component, to call another method
+on the bean, than was coded in the application. In the camel-jms component, then a mallicous header can be used to send
+the message to another queue (on the same broker) than was coded in the application.
+
+The attacker would need to inject custom headers, such as HTTP protocols. So if you have Camel applications that are
+directly connected to the internet via HTTP, then an attacker could include malicious HTTP headers in the HTTP requests
+that are send to the Camel application.
+
+All the known Camel HTTP component such as camel-servlet, camel-jetty, camel-undertow, camel-platform-http, and camel-netty-http would be vulnerable out of the box.
+
+In terms of usage of the default header filter strategy the list of components using that is:
+
+camel-activemq
+camel-activemq6
+camel-amqp
+camel-aws2-sqs
+camel-azure-servicebus
+camel-cxf-rest
+camel-cxf-soap
+camel-http
+camel-jetty
+camel-jms
+camel-kafka
+camel-knative
+camel-mail
+camel-nats
+camel-netty-http
+camel-platform-http
+camel-rest
+camel-servlet
+camel-sjms
+camel-spring-rabbitmq
+camel-stomp
+camel-tahu
+camel-undertow
+camel-xmpp"
+mitigation: "Users are recommended to upgrade to version 4.10.2 for 4.10.x LTS, 4.8.5 for 4.8.x LTS and 3.22.4 for 3.x releases. Also, users could use removeHeaders EIP, to filter out anything like 'cAmel, cAMEL' etc, or in general everything not starting with 'Camel', 'camel' or 'org.apache.camel.'."
+credit: "This issue was discovered by Mark Thorson of AT&T"
+affected: Apache Camel 4.10.0 before 4.10.2. Apache Camel 4.8.0 before 4.8.5. Apache Camel 3.10.0 before 3.22.4.
+fixed: 3.22.4, 4.8.5 and 4.10.2
+---
+
+The JIRA ticket: https://issues.apache.org/jira/browse/CAMEL-21828 refers to the various commits that resolved the issue, and have more details.