diff --git a/vulnerabilities/importers/__init__.py b/vulnerabilities/importers/__init__.py
index f0d9532ab..42024ef3c 100644
--- a/vulnerabilities/importers/__init__.py
+++ b/vulnerabilities/importers/__init__.py
@@ -35,6 +35,7 @@
from vulnerabilities.importers import xen
from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
from vulnerabilities.pipelines import alpine_linux_importer
+from vulnerabilities.pipelines import apache_camel_importer
from vulnerabilities.pipelines import github_importer
from vulnerabilities.pipelines import gitlab_importer
from vulnerabilities.pipelines import nginx_importer
@@ -44,6 +45,7 @@
from vulnerabilities.pipelines import pysec_importer
IMPORTERS_REGISTRY = [
+ apache_camel_importer.ApacheCamelImporterPipeline,
nvd_importer.NVDImporterPipeline,
github_importer.GitHubAPIImporterPipeline,
gitlab_importer.GitLabImporterPipeline,
diff --git a/vulnerabilities/pipelines/apache_camel_importer.py b/vulnerabilities/pipelines/apache_camel_importer.py
new file mode 100644
index 000000000..a9c9386dc
--- /dev/null
+++ b/vulnerabilities/pipelines/apache_camel_importer.py
@@ -0,0 +1,343 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/aboutcode-org/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
+import json
+import logging
+import re
+from datetime import timezone
+from typing import Iterable
+
+import dateparser
+from bs4 import BeautifulSoup
+from packageurl import PackageURL
+from univers.version_constraint import VersionConstraint
+from univers.version_range import MavenVersionRange
+from univers.versions import MavenVersion
+
+from vulnerabilities.importer import AdvisoryData
+from vulnerabilities.importer import AffectedPackage
+from vulnerabilities.importer import Reference
+from vulnerabilities.importer import VulnerabilitySeverity
+from vulnerabilities.pipelines import VulnerableCodeBaseImporterPipeline
+from vulnerabilities.severity_systems import SCORING_SYSTEMS
+from vulnerabilities.utils import fetch_response
+from vulnerabilities.utils import get_item
+
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+
+class ApacheCamelImporterPipeline(VulnerableCodeBaseImporterPipeline):
+ """Collect Advisories from Apache Camel"""
+
+ pipeline_id = "apache_camel_importer"
+ spdx_license_expression = "Apache-2.0"
+ license_url = "https://www.apache.org/licenses/LICENSE-2.0"
+ root_url = "https://camel.apache.org/security/"
+ importer_name = "Apache Camel Importter"
+
+ def __init__(self):
+ super().__init__()
+
+ @classmethod
+ def steps(cls):
+ return (
+ cls.collect_and_store_advisories,
+ cls.import_new_advisories,
+ )
+
+ def advisories_count(self) -> int:
+ return fetch_count_advisories(self.root_url)
+
+ def collect_advisories(self) -> Iterable[AdvisoryData]:
+ adv_data = fetch_advisory_data(self.root_url)
+ for data in adv_data:
+ yield to_advisory_data(data)
+
+
+def fetch_html_response(url):
+ """
+ Fetch and parse the HTML content of a given URL.
+ This function sends a request to the URL, retrieves the HTML content,
+ and parses it using BeautifulSoup.
+ Args:
+ url (str): The URL to fetch the HTML content from.
+ Returns:
+ A BeautifulSoup object representing the parsed HTML content.
+ """
+ try:
+ response = fetch_response(url).content
+ soup = BeautifulSoup(response, "html.parser")
+ return soup
+ except:
+ logger.error(f"Failed to fetch URL {url}")
+
+
+def fetch_count_advisories(url):
+ """
+ Gives the number of advisories from the given URL.
+ Advisories are identified by
tags.
+
+ Args:
+ url (str): The URL to fetch the advisories from.
+
+ Returns:
+ int: The number of advisories found on the page.
+
+ Doctests:
+ >>> from unittest.mock import patch
+ >>> from bs4 import BeautifulSoup
+ >>> from vulnerabilities.pipelines.apache_camel_importer import fetch_count_advisories
+ >>> mock_html = '''
+ ...
+ ...
+ ...
+ ...
+ ... Advisory 1 |
+ ... Advisory 2 |
+ ... Advisory 3 |
+ ...
+ ...
+ ...
+ ...
+ ... '''
+ >>> with patch('vulnerabilities.pipelines.apache_camel_importer.fetch_html_response') as mock_fetch:
+ ... mock_fetch.return_value = BeautifulSoup(mock_html, "html.parser")
+ ... count = fetch_count_advisories("http://example.com")
+ >>> count
+ 3
+ """
+
+ soup = fetch_html_response(url)
+ table = soup.find("tbody")
+ advisory_len = len(table.find_all("tr"))
+
+ return advisory_len
+
+
+def fetch_advisory_data(url):
+ """
+ Fetch advisory data from the given URL.
+
+ Args:
+ url (str): The URL to fetch the advisory data from.
+
+ Returns:
+ list: A list of dictionaries, where each dictionary contains advisory details.
+
+ Doctests:
+ >>> from unittest.mock import patch
+ >>> mock_html = '''
+ ...
+ ...
+ ...
+ ...
+ ...
+ ... CVE-2025-30177 |
+ ... Apache Camel 4.10.0 before 4.10.3 |
+ ... 4.10.3 |
+ ... MEDIUM |
+ ... Camel-Undertow Message Header Injection |
+ ...
+ ...
+ ... CVE-2025-30178 |
+ ... Apache Camel 4.8.0 before 4.8.6 |
+ ... 4.8.6 |
+ ... HIGH |
+ ... Another vulnerability description |
+ ...
+ ...
+ ...
+ ...
+ ...
+ ... '''
+ >>> with patch('vulnerabilities.pipelines.apache_camel_importer.fetch_html_response') as mock_fetch:
+ ... mock_fetch.return_value = BeautifulSoup(mock_html, "html.parser")
+ ... advisories = fetch_advisory_data("http://example.com")
+ >>> len(advisories)
+ 2
+ >>> advisories[0]['Reference']
+ 'CVE-2025-30177'
+ >>> advisories[0]['Affected']
+ 'Apache Camel 4.10.0 before 4.10.3'
+ """
+
+ soup = fetch_html_response(url)
+ table = soup.find("tbody")
+
+ advisories = []
+
+ for row in table.find_all("tr"):
+ columns = row.find_all("td")
+ if len(columns) == 5:
+ reference = columns[0].text.strip()
+ affected = columns[1].text.strip()
+ fixed = columns[2].text.strip()
+ score = columns[3].text.strip()
+ description = columns[4].text.strip()
+
+ advisories.append(
+ {
+ "Reference": reference,
+ "Affected": affected,
+ "Fixed": fixed,
+ "Score": score,
+ "Description": description,
+ }
+ )
+
+ return advisories
+
+
+"""
+{
+ 'Reference': 'CVE-2025-30177',
+ 'Affected': 'Apache Camel 4.10.0 before 4.10.3. Apache Camel 4.8.0 before 4.8.6.',
+ 'Fixed': '4.8.6 and 4.10.3',
+ 'Score': 'MEDIUM',
+ 'Description': 'Camel-Undertow Message Header Injection via Improper Filtering'
+}
+"""
+
+
+def to_advisory_data(raw_data) -> AdvisoryData:
+ """
+ Convert raw advisory data into an AdvisoryData object.
+
+ Args:
+ raw_data (dict): A dictionary containing raw advisory data.
+
+ Returns:
+ AdvisoryData: An object containing structured advisory information.
+
+ Doctests:
+ >>> from unittest.mock import patch
+ >>> from vulnerabilities.pipelines.apache_camel_importer import fetch_date_published
+ >>> from vulnerabilities.pipelines.apache_camel_importer import to_advisory_data
+ >>> from vulnerabilities.importer import AdvisoryData
+ >>> raw_data = {
+ ... 'Reference': 'CVE-2025-30177',
+ ... 'Affected': 'Apache Camel 4.10.0 before 4.10.3. Apache Camel 4.8.0 before 4.8.6.',
+ ... 'Fixed': '4.8.6 and 4.10.3',
+ ... 'Score': 'MEDIUM',
+ ... 'Description': 'Camel-Undertow Message Header Injection via Improper Filtering'
+ ... }
+ >>> with patch('vulnerabilities.pipelines.apache_camel_importer.fetch_date_published') as mock_fetch_date_published:
+ ... mock_fetch_date_published.return_value = "2025-04-01T11:56:30.484000+00:00"
+ ... advisory = to_advisory_data(raw_data)
+ >>> advisory.aliases
+ ['CVE-2025-30177']
+ >>> advisory.summary
+ 'Camel-Undertow Message Header Injection via Improper Filtering'
+ >>> len(advisory.affected_packages)
+ 1
+ >>> advisory.affected_packages[0].package.name
+ 'camel'
+ """
+
+ alias = get_item(raw_data, "Reference")
+
+ version_pattern = re.compile(r"\b\d+\.\d+\.\d+\b")
+ fixed_version_out = get_item(raw_data, "Fixed")
+ fixed_versions = []
+ for fixed_version in version_pattern.findall(fixed_version_out):
+ fixed_versions.append(MavenVersion(fixed_version))
+
+ affected_packages = []
+ affected_package_string = get_item(raw_data, "Affected")
+ affected_package = parse_apache_camel_versions(affected_package_string)
+ affected_packages.append(
+ AffectedPackage(
+ package=PackageURL(
+ type="maven",
+ namespace="org.apache.camel",
+ name="camel",
+ ),
+ affected_version_range=affected_package,
+ )
+ )
+
+ score = get_item(raw_data, "Score")
+ severity = VulnerabilitySeverity(system=SCORING_SYSTEMS["generic_textual"], value=score)
+
+ references = []
+ references.append(
+ Reference(
+ severities=[severity],
+ reference_id=alias,
+ url=f"https://camel.apache.org/security/{alias}.html",
+ )
+ )
+
+ description = get_item(raw_data, "Description")
+
+ date_published = fetch_date_published(alias)
+ parsed_date_published = dateparser.parse(date_published).replace(tzinfo=timezone.utc)
+
+ return AdvisoryData(
+ aliases=[alias],
+ summary=description,
+ affected_packages=affected_packages,
+ references=references,
+ url=f"https://camel.apache.org/security/{alias}.html",
+ date_published=parsed_date_published,
+ )
+
+
+def fetch_date_published(cve):
+ """Fetches Date of a CVE"""
+
+ url = f"https://cveawg.mitre.org/api/cve/{cve}"
+ response = fetch_response(url).content
+ response = json.loads(response)
+ return response["cveMetadata"]["datePublished"]
+
+
+def parse_apache_camel_versions(version_string):
+ """Parse version strings from Apache Camel advisories into version constraints"""
+
+ version_ranges = []
+
+ # Handle "from X before Y"
+ for match in re.finditer(r"from ([\d\w.-]+) before ([\d\w.-]+)", version_string):
+ start_version, end_version = match.groups()
+ version_ranges.extend(
+ [
+ VersionConstraint(comparator=">=", version=MavenVersion(start_version)),
+ VersionConstraint(comparator="<", version=MavenVersion(end_version)),
+ ]
+ )
+
+ # Handle "from X up to Y"
+ for match in re.finditer(r"from ([\d\w.-]+) up to ([\d\w.-]+)", version_string):
+ start_version, end_version = match.groups()
+ version_ranges.extend(
+ [
+ VersionConstraint(comparator=">=", version=MavenVersion(start_version)),
+ VersionConstraint(comparator="<=", version=MavenVersion(end_version)),
+ ]
+ )
+
+ # Handle isolated versions like `3.19.0`
+ for match in re.finditer(r"(\d+\.\d+\.\d+)", version_string):
+ version = match.group(1)
+ version_ranges.append(VersionConstraint(comparator="=", version=MavenVersion(version)))
+
+ # Handle X.x style like 2.22.x
+ for match in re.finditer(r"(\d+\.\d+)\.x", version_string):
+ version_prefix = match.group(1)
+ start_version = f"{version_prefix}.0"
+ end_version = f"{version_prefix}.99999" # To cover all patch versions
+ version_ranges.extend(
+ [
+ VersionConstraint(comparator=">=", version=MavenVersion(start_version)),
+ VersionConstraint(comparator="<=", version=MavenVersion(end_version)),
+ ]
+ )
+
+ return MavenVersionRange(constraints=version_ranges)
diff --git a/vulnerabilities/tests/pipelines/test_apache_camel_importer_pipeline.py b/vulnerabilities/tests/pipelines/test_apache_camel_importer_pipeline.py
new file mode 100644
index 000000000..38eaffb69
--- /dev/null
+++ b/vulnerabilities/tests/pipelines/test_apache_camel_importer_pipeline.py
@@ -0,0 +1,148 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# VulnerableCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: Apache-2.0
+# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
+# See https://github.com/aboutcode-org/vulnerablecode for support or download.
+# See https://aboutcode.org for more information about nexB OSS projects.
+#
+
+import datetime
+import json
+from pathlib import Path
+from unittest import mock
+
+from packageurl import PackageURL
+from univers.version_constraint import VersionConstraint
+from univers.version_range import MavenVersionRange
+from univers.versions import MavenVersion
+
+from vulnerabilities.importer import AdvisoryData
+from vulnerabilities.importer import AffectedPackage
+from vulnerabilities.importer import Reference
+from vulnerabilities.importer import VulnerabilitySeverity
+from vulnerabilities.pipelines import apache_camel_importer
+from vulnerabilities.severity_systems import ScoringSystem
+
+EXPECTED_DATA = (
+ Path(__file__).parent.parent / "test_data" / "apache_camel" / "apache_camel_expected.json"
+)
+ADVISORY_HTML_DATA = (
+ Path(__file__).parent.parent / "test_data" / "apache_camel" / "apache_camel_test.html"
+)
+
+
+def load_test_data(file):
+ with open(file) as f:
+ return json.load(f)
+
+
+@mock.patch("requests.get")
+def test_fetch_advisory_data(mock_get):
+ """Test fetching and parsing of advisory data"""
+
+ expected_data = load_test_data(EXPECTED_DATA)
+
+ with open(ADVISORY_HTML_DATA, "r", encoding="utf-8") as file:
+ mock_html_content = file.read()
+
+ mock_response = mock.Mock()
+ mock_response.status_code = 200
+ mock_response.text = mock_html_content
+ mock_response.content = mock_html_content.encode("utf-8")
+
+ mock_get.return_value = mock_response
+
+ advisory_data = apache_camel_importer.fetch_advisory_data("https://camel.apache.org/security/")
+
+ assert advisory_data[0]["Reference"] == expected_data[0]["aliases"][0]
+ assert advisory_data[0]["Description"] == expected_data[0]["summary"]
+
+
+@mock.patch("vulnerabilities.pipelines.apache_camel_importer.fetch_date_published")
+@mock.patch("vulnerabilities.pipelines.apache_camel_importer.fetch_advisory_data")
+def test_apache_camel_importer_pipeline_collect_advisories(
+ mock_fetch_advisory_data, mock_fetch_date_published
+):
+ """Test the collect_advisories method in ApacheCamelImporterPipeline"""
+
+ with open(ADVISORY_HTML_DATA, "r", encoding="utf-8") as file:
+ mock_html_content = file.read()
+
+ mock_response = mock.Mock()
+ mock_response.status_code = 200
+ mock_response.text = mock_html_content
+ mock_response.content = mock_html_content.encode("utf-8")
+
+ mock_fetch_advisory_data.return_value = [
+ {
+ "Reference": "CVE-2025-30177",
+ "Affected": "Apache Camel 4.10.0 before 4.10.3. Apache Camel 4.8.0 before 4.8.6.",
+ "Fixed": "4.8.6 and 4.10.3",
+ "Score": "MEDIUM",
+ "Description": "Camel-Undertow Message Header Injection via Improper Filtering",
+ }
+ ]
+
+ mock_fetch_date_published.return_value = "2025-04-01T11:56:30.484000+00:00"
+
+ pipeline = apache_camel_importer.ApacheCamelImporterPipeline()
+
+ generator = pipeline.collect_advisories()
+ advisories = list(generator)
+
+ assert len(advisories) == 1
+ assert advisories[0].aliases == advisory_data.aliases
+ assert advisories[0].date_published == advisory_data.date_published
+ assert advisories[0].summary == advisory_data.summary
+ assert advisories[0].affected_packages == advisory_data.affected_packages
+
+
+advisory_data = AdvisoryData(
+ aliases=["CVE-2025-30177"],
+ summary="Camel-Undertow Message Header Injection via Improper Filtering",
+ affected_packages=[
+ AffectedPackage(
+ package=PackageURL(
+ type="maven",
+ namespace="org.apache.camel",
+ name="camel",
+ version=None,
+ qualifiers={},
+ subpath=None,
+ ),
+ affected_version_range=MavenVersionRange(
+ constraints=(
+ VersionConstraint(comparator="=", version=MavenVersion(string="4.8.0")),
+ VersionConstraint(comparator="=", version=MavenVersion(string="4.8.6")),
+ VersionConstraint(comparator="=", version=MavenVersion(string="4.10.0")),
+ VersionConstraint(comparator="=", version=MavenVersion(string="4.10.3")),
+ )
+ ),
+ fixed_version=None,
+ )
+ ],
+ references=[
+ Reference(
+ reference_id="CVE-2025-30177",
+ reference_type="",
+ url="https://camel.apache.org/security/CVE-2025-30177.html",
+ severities=[
+ VulnerabilitySeverity(
+ system=ScoringSystem(
+ identifier="generic_textual",
+ name="Generic textual severity rating",
+ url="",
+ notes="Severity for generic scoring systems. Contains generic textual values like High, Low etc",
+ ),
+ value="MEDIUM",
+ scoring_elements="",
+ published_at=None,
+ )
+ ],
+ )
+ ],
+ date_published=datetime.datetime(2025, 4, 1, 11, 56, 30, 484000, tzinfo=datetime.timezone.utc),
+ weaknesses=[],
+ url="https://camel.apache.org/security/CVE-2025-30177.html",
+)
diff --git a/vulnerabilities/tests/test_data/apache_camel/apache_camel_expected.json b/vulnerabilities/tests/test_data/apache_camel/apache_camel_expected.json
new file mode 100644
index 000000000..646d6c817
--- /dev/null
+++ b/vulnerabilities/tests/test_data/apache_camel/apache_camel_expected.json
@@ -0,0 +1,37 @@
+[
+ {
+ "aliases": ["CVE-2025-30177"],
+ "summary": "Camel-Undertow Message Header Injection via Improper Filtering",
+ "affected_packages": [
+ {
+ "package": {
+ "type": "maven",
+ "namespace": "org.apache.camel",
+ "name": "camel",
+ "version": "",
+ "qualifiers": "",
+ "subpath": ""
+ },
+ "affected_version_range": "vers:maven/4.8.0|4.8.6|4.10.0|4.10.3",
+ "fixed_version": null
+ }
+ ],
+ "references": [
+ {
+ "reference_id": "CVE-2025-30177",
+ "reference_type": "",
+ "url": "https://camel.apache.org/security/CVE-2025-30177.html",
+ "severities": [
+ {
+ "system": "generic_textual",
+ "value": "MEDIUM",
+ "scoring_elements": ""
+ }
+ ]
+ }
+ ],
+ "date_published": "2025-04-01T11:56:30.484000+00:00",
+ "weaknesses": [],
+ "url": "https://camel.apache.org/security/CVE-2025-30177.html"
+ }
+]
\ No newline at end of file
diff --git a/vulnerabilities/tests/test_data/apache_camel/apache_camel_test.html b/vulnerabilities/tests/test_data/apache_camel/apache_camel_test.html
new file mode 100644
index 000000000..e39f7a8f7
--- /dev/null
+++ b/vulnerabilities/tests/test_data/apache_camel/apache_camel_test.html
@@ -0,0 +1,15 @@
+
+
+
+ CVE-2025-30177 |
+ Apache Camel 4.10.0 before 4.10.3. Apache Camel 4.8.0 before 4.8.6. |
+ 4.8.6 and 4.10.3 |
+ MEDIUM |
+ Camel-Undertow Message Header Injection via Improper Filtering |
+
+
+ CVE-2025-29891 |
+ 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. |
+ 3.22.4, 4.8.5 and 4.10.2 | HIGH | Camel Message Header Injection through request parameters |
+
+
\ No newline at end of file