mongo/buildscripts/tests/test_generate_sbom.py
mongo-pr-bot[bot] 6a2b1f1b74 SERVER-111072 Auto-generated SBOM files [master] (#50933)
Co-authored-by: mongo-pr-bot[bot] <230616009+mongo-pr-bot[bot]@users.noreply.github.com>
Co-authored-by: Jason Hills <jason.hills@mongodb.com>
GitOrigin-RevId: 44c068f7b52875085073eeac559ea560d7804430
2026-04-01 09:06:58 +00:00

237 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""
Tests for buildscripts/sbom/*.py
"""
import json
import logging
import os
import sys
import unittest
from buildscripts.sbom.config import get_semver_from_release_version, regex_semver
from buildscripts.sbom.endorctl_utils import EndorCtl
from buildscripts.sbom.sbom_utils import is_valid_purl
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
class TestEndorctl(unittest.TestCase):
"""Test cases for the EndorCtl class."""
def test_endorctl_init(self):
"""Tests the Endorctl constructor."""
e = EndorCtl(namespace="mongodb.10gen", retry_limit=1, sleep_duration=5)
self.assertEqual(e.namespace, "mongodb.10gen")
self.assertEqual(e.retry_limit, 1)
self.assertEqual(e.sleep_duration, 5)
def test_call_endorctl_missing(self):
"""Tests EndorCtl execution with endorctl not in path."""
logger = logging.getLogger("generate_sbom")
logger.setLevel(logging.INFO)
e = EndorCtl(namespace="mongodb.10gen", endorctl_path="this_path_does_not_exist")
result = e.get_sbom("https://github.com/10gen/mongo.git")
self.assertRaises(FileNotFoundError)
self.assertIsNone(result, None)
class TestConfigRegex(unittest.TestCase):
"""Test suite for configuration regex patterns and PURL validation.
This test class validates regex patterns used for semantic versioning,
version extraction from release strings, and Package URL (PURL) validation.
"""
def test_semver_regex(self):
"""Tests the regex_semver."""
# List of valid semantic version strings
valid_semvers = [
"0.0.1",
"1.2.3",
"10.20.30",
"1.2.3-alpha",
"1.2.3-alpha.1",
"1.2.3-0.beta",
"1.2.3+build.123",
"1.2.3-rc.1+build.456",
"1.0.0-beta+exp.sha.5114f85",
]
# List of invalid semantic version strings
invalid_semvers = [
"1.2", # Incomplete
"1", # Incomplete
"v1.2.3", # Has a 'v' prefix (regex is for the version part only)
"1.2.3-", # Trailing hyphen in pre-release
"1.2.3+", # Trailing plus in build
"1.02.3", # Leading zero in minor component
"1.2.03", # Leading zero in patch component
"alpha", # Not a valid version
"1.2.3.4", # Four components (SemVer is 3)
"1.2.3-alpha_beta", # Underscore in pre-release
]
print("\nTesting regex_semver:")
for v in valid_semvers:
with self.subTest(v=v):
self.assertIsNotNone(
regex_semver.fullmatch(v), f"Expected '{v}' to be a valid semver"
)
for v in invalid_semvers:
with self.subTest(v=v):
self.assertIsNone(
regex_semver.fullmatch(v), f"Expected '{v}' to be an invalid semver"
)
def test_get_semver_from_release_version(self):
"""Tests the transformation function that uses VERSION_PATTERN_REPL."""
# (input, expected_output)
test_cases = [
# Pattern 1: 'debian/1.28.1-1'
("debian/1.28.1-1", "1.28.1"),
("debian/1.2.3-rc.1-2", "1.2.3-rc.1"),
# Pattern 2: 'gperftools-2.9.1', 'mongo/v1.5.2', etc.
("gperftools-2.9.1", "2.9.1"),
("mongo/v1.5.2", "1.5.2"),
("mongodb-8.2.0-alpha2", "8.2.0-alpha2"),
("release-1.12.0", "1.12.0"),
("yaml-cpp-0.6.3", "0.6.3"),
("mongo/1.2.3-beta+build", "1.2.3-beta+build"),
# Pattern 3: 'asio-1-34-2', 'cares-1_27_0'
("asio-1-34-2", "1.34.2"),
("cares-1_27_0", "1.27.0"),
# Pattern 4: 'pcre2-10.40'
("pcre2-10.40", "10.40"),
("something-1.2", "1.2"),
# Pattern 5: 'icu-release-57-1'
("icu-release-57-1", "57.1"),
("foo-bar-12-3", "12.3"),
# Pattern 6: 'v2.6.0'
("v2.6.0", "2.6.0"),
("v1.2.3-alpha.1", "1.2.3-alpha.1"),
# Pattern 7: 'r2.5.1'
("r2.5.1", "2.5.1"),
("r1.2.3-alpha.1", "1.2.3-alpha.1"),
# Pattern 7: 'v2025.04.21.00' (non-semver but specific pattern)
("v2025.04.21.00", "2025.04.21.00"),
# --- Cases that should not match ---
("1.2.3", "1.2.3"), # Already clean
("latest", "latest"), # No match
("not-a-version", "not-a-version"), # No match
("v1.2", "v1.2"), # Not matched by any pattern
]
print("\nTesting get_semver_from_release_version():")
for input_str, expected_str in test_cases:
with self.subTest(input=input_str):
result = get_semver_from_release_version(input_str)
self.assertEqual(
result,
expected_str,
f"Input: '{input_str}', Expected: '{expected_str}', Got: '{result}'",
)
def test_purls_valid(self):
"""Tests valid PURLs."""
valid_purls = [
"pkg:github/gperftools/gperftools@gperftools-2.9.1",
"pkg:github/mongodb/mongo-c-driver@1.23.4",
"pkg:github/google/benchmark", # No version
"pkg:github/c-ares/c-ares@cares-1_27_0",
"pkg:github/apache/avro@release-1.12.0",
"pkg:github/jbeder/yaml-cpp@yaml-cpp-0.6.3",
"pkg:github/pcre2project/pcre2@pcre2-10.40",
"pkg:github/unicode-org/icu@icu-release-57-1",
"pkg:github/confluentinc/librdkafka@v2.6.0",
"pkg:github/facebook/folly@v2025.04.21.00?foo=bar#src/main", # With qualifiers/subpath
"pkg:generic/valgrind/valgrind@3.23.0", # namespace/name@version
"pkg:generic/intel/IntelRDFPMathLib@2.0u2",
"pkg:generic/openldap/openldap", # namespace/name
"pkg:generic/openssl@3.0.13", # name@version
"pkg:generic/my-package", # name only
"pkg:generic/my-package@1.2.3?arch=x86_64#README.md", # With qualifiers/subpath
"pkg:deb/debian/firefox-esr@128.11.0esr-1?arch=source",
"pkg:pypi/ocspbuilder@0.10.2",
]
print("\nTesting Valid PURLs:")
for purl in valid_purls:
with self.subTest(purl=purl):
self.assertTrue(is_valid_purl(purl), f"Expected '{purl}' to be valid")
def test_purls_invalid(self):
"""Tests invalid PURLs."""
invalid_purls = [
"pkg:github/gperftools", # Missing name
"pkg:github/", # Missing namespace and name
"pkg:c/github.com/abseil/abseil-cpp", # Wrong type (from your config.py)
"pkg:github/mongodb/mongo-c-driver@1.2.3@4.5.6", # Double version
"pkg:generic/github/mongodb/mongo", # Wrong type
"pkg:generic/", # Missing name
"pkg:github/valgrind/", # Missing name
"pkg:generic/my-package@1.2@3.4", # Double version
"pkg:generic/spaces in name", # Spaces not allowed (must be encoded)
"pkg:deb/firefox-esr@128.11.0esr-1?arch=source", # Missing vendor
"pkg:pypi/ocsp/ocspbuilder@0.10.2", # no namespace for PyPI
]
print("\nTesting Invalid PURLs:")
for purl in invalid_purls:
with self.subTest(purl=purl):
self.assertFalse(is_valid_purl(purl), f"Expected '{purl}' to be invalid")
class TestMetadataFile(unittest.TestCase):
"""Unit tests for SBOM metadata file validation and version tag consistency."""
TEST_DIR = os.path.join("buildscripts", "sbom")
VERSION_TAG = "{{VERSION}}"
def read_sbom_json_file(self, file_path: str) -> dict:
"""Load a JSON SBOM file (schema is not validated)"""
with open(file_path, "r", encoding="utf-8") as input_json:
sbom_json = input_json.read()
return json.loads(sbom_json)
def test_metadata_sbom_version_tags(self):
"""Test that SBOM metadata components have consistent version tags.
Verifies that each component in the metadata SBOM file contains required fields
(bom-ref and version) plus at least one of purl or cpe. Additionally ensures that
the VERSION_TAG is either present in all component properties or absent from all,
maintaining consistency across bom-ref, version, purl, and cpe fields.
"""
sbom_metadata_file = os.path.join(self.TEST_DIR, "metadata.cdx.json")
print(sbom_metadata_file)
meta_bom = self.read_sbom_json_file(sbom_metadata_file)
for component in meta_bom["components"]:
with self.subTest(component=component):
properties = []
properties.append(component["bom-ref"])
properties.append(component["version"])
if "purl" in component:
properties.append(component["purl"])
if "cpe" in component:
properties.append(component["cpe"])
# make sure component has a minimum of bom-ref, version and at least one of purl or cpe
self.assertGreater(
len(properties),
2,
f"Component must have a minimum of bom-ref, version and at least one of purl or cpe. {properties}",
)
# make sure all properites either have version tag or no version tags
self.assertTrue(
all(self.VERSION_TAG in p for p in properties)
or all(self.VERSION_TAG not in p for p in properties),
f"Component must have version tag '{self.VERSION_TAG}' in all or none of bom-ref, version and purl and/or cpe. {properties})",
)
if __name__ == "__main__":
unittest.main(verbosity=2)