Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion vulnerabilities/pipelines/v2_importers/gitlab_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import json
import logging
import re
import traceback
from pathlib import Path
from typing import Iterable
Expand Down Expand Up @@ -178,6 +179,37 @@ def get_purl(package_slug, purl_type_by_gitlab_scheme, logger):
return


def parse_native_version_range(vrc, native_range):
"""
Return a VersionRange parsed from a native version range string.

Some upstream ranges cannot be parsed in a single pass, for example Maven
ranges containing single-point segments such as ``[1.81.0,1.81.0]`` or
segments that are not listed in version order. In this case, parse each
segment separately and combine the resulting constraints in a single range.
"""
try:
return vrc.from_native(native_range)
except Exception:
pass

segments = re.findall(r"[\[\(][^\[\]()]*[\]\)]|[^,\[\]()]+", native_range)
constraints = []
for segment in segments:
segment = segment.strip()
single_point = re.fullmatch(r"\[\s*(.+?)\s*,\s*(.+?)\s*\]", segment)
if single_point and single_point.group(1) == single_point.group(2):
segment_range = vrc.from_versions([single_point.group(1)])
elif segment.startswith(("[", "(")):
segment_range = vrc.from_native(segment)
else:
segment_range = vrc.from_versions([segment])
constraints.extend(segment_range.constraints)

constraints = sorted(constraints, key=lambda constraint: constraint.version)
return vrc(constraints=tuple(constraints))


def parse_gitlab_advisory(
file, base_path, gitlab_scheme_by_purl_type, purl_type_by_gitlab_scheme, logger
):
Expand Down Expand Up @@ -266,7 +298,7 @@ def parse_gitlab_advisory(
gitlab_scheme=gitlab_scheme, string=affected_range
)
else:
affected_version_range = vrc.from_native(affected_range)
affected_version_range = parse_native_version_range(vrc, affected_range)
except Exception as e:
logger(
f"parse_yaml_file: affected_range is not parsable: {affected_range!r} for: {purl!s} error: {e!r}\n {traceback.format_exc()}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ def mock_gitlab_yaml(tmp_path):
advisory_dir.mkdir(parents=True)

advisory_file = advisory_dir / "CVE-2022-0001.yml"
advisory_file.write_text(
"""
advisory_file.write_text("""
identifier: "CVE-2022-0001"
package_slug: "pypi/package_name"
title: "Example vulnerability"
Expand All @@ -54,8 +53,7 @@ def mock_gitlab_yaml(tmp_path):
- "CWE-79"
identifiers:
- "CVE-2022-0001"
"""
)
""")
return tmp_path


Expand Down Expand Up @@ -206,3 +204,71 @@ def mock_get_purl(package_slug, purl_type_by_gitlab_scheme, logger):
assert isinstance(result.date_published, datetime)
assert result.date_published.year == 2018
assert result.affected_packages == [] # Because get_purl was mocked to return None


@pytest.fixture
def gitlab_maven_advisory_yaml(tmp_path):
content = {
"identifier": "CVE-2025-14813",
"package_slug": "maven/org.bouncycastle/bcprov-debug-jdk14",
"title": "Example maven vulnerability",
"description": "Example description",
"pubdate": "2025-01-15",
"affected_range": "[1.59.0,1.80.1],[1.82.0,1.84.0),[1.81.0,1.81.0]",
"fixed_versions": ["1.84.0"],
"urls": ["https://example.com/advisory"],
"identifiers": ["CVE-2025-14813"],
}

advisory_path = tmp_path / "CVE-2025-14813.yml"
advisory_path.write_text(saneyaml.dump(content))
return advisory_path, content


def parse_maven_advisory(file_path):
from vulnerabilities.pipelines.v2_importers.gitlab_importer import GitLabImporterPipeline

dummy_logger = lambda *args, **kwargs: None # Ignore logging in test

return parse_gitlab_advisory(
file=file_path,
base_path=file_path.parent,
gitlab_scheme_by_purl_type=GitLabImporterPipeline.gitlab_scheme_by_purl_type,
purl_type_by_gitlab_scheme=GitLabImporterPipeline.purl_type_by_gitlab_scheme,
logger=dummy_logger,
)


def test_parse_gitlab_advisory_maven_multirange(gitlab_maven_advisory_yaml):
file_path, _ = gitlab_maven_advisory_yaml

result = parse_maven_advisory(file_path)

assert isinstance(result, AdvisoryDataV2)
affected_package = result.affected_packages[0]
assert affected_package.package.name == "bcprov-debug-jdk14"
assert affected_package.package.namespace == "org.bouncycastle"
assert str(affected_package.affected_version_range) == (
"vers:maven/>=1.59.0|<=1.80.1|1.81.0|>=1.82.0|<1.84.0"
)
assert str(affected_package.fixed_version_range) == "vers:maven/1.84.0"

from univers.versions import MavenVersion

affected_range = affected_package.affected_version_range
for version in ["1.59.0", "1.70.0", "1.81.0", "1.83.0"]:
assert MavenVersion(version) in affected_range
for version in ["1.80.2", "1.81.5", "1.84.0", "1.90.0"]:
assert MavenVersion(version) not in affected_range


def test_parse_gitlab_advisory_maven_single_point_range(gitlab_maven_advisory_yaml):
file_path, content = gitlab_maven_advisory_yaml

content["affected_range"] = "[1.81.0,1.81.0]"
file_path.write_text(saneyaml.dump(content))

result = parse_maven_advisory(file_path)

assert isinstance(result, AdvisoryDataV2)
assert str(result.affected_packages[0].affected_version_range) == "vers:maven/1.81.0"