"""Unit tests.""" import json import subprocess import tarfile from datetime import datetime, timedelta from unittest.mock import mock_open import botocore import pytest from botocore.stub import Stubber import main @pytest.fixture def mock_inspector_sbomgen(mocker): mocker.patch("subprocess.run") mocker.patch( "builtins.open", mock_open( read_data=json.dumps( { "components": [ { "bom-ref": "comp-2", "type": "operating-system", "name": "Debian GNU/Linux", "version": "11", }, { "bom-ref": "comp-3", "type": "library", "name": "pip", "version": "9.0.3", "purl": "pkg:pypi/pip@9.0.3", }, ] } ) ), ) @pytest.fixture def mock_inspector_sbomgen_failure(mocker): mocker.patch("subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd")) @pytest.fixture def mock_image_config(mocker): mocker.patch( "main.get_image_config", return_value={"Labels": {"parent-images": "debian11,debian11-test"}}, ) @pytest.fixture def mock_image_tar(tmpdir, monkeypatch): tar_dir = tmpdir.mkdir("image") config = tar_dir.join("config.json") config.write(json.dumps({"config": {"Labels": {"foo": "bar"}}})) manifest = tar_dir.join("manifest.json") manifest.write(json.dumps([{"Config": "config.json"}])) tar = tmpdir.join("image.tar") with tarfile.open(tar, "w") as t: t.add(tar_dir, arcname=".") monkeypatch.setattr("config.IMAGE_PATH", tar) @pytest.fixture def inspector_stubber(): with Stubber(main.inspector_scan_client) as stubber: yield stubber @pytest.fixture def ecr_stubber(): with Stubber(main.ecr_client) as stubber: yield stubber def test_no_vulnerabilities( mock_inspector_sbomgen, inspector_stubber, mock_image_config ): inspector_stubber.add_response("scan_sbom", {"sbom": {}}) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_sbom_generation_failure(mock_inspector_sbomgen_failure): with pytest.raises(subprocess.CalledProcessError): main.main() def test_scan_sbom_errors(mock_inspector_sbomgen, inspector_stubber): inspector_stubber.add_client_error("scan_sbom") with pytest.raises(botocore.exceptions.ClientError): main.main() def test_failed_findings( mock_inspector_sbomgen, inspector_stubber, ecr_stubber, monkeypatch, mock_image_config, ): monkeypatch.setattr("config.ECR_REPOSITORY_NAME", "dummy") monkeypatch.setattr("config.IMAGE_TAG", "latest") inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-07-13T08:15:07Z", "affects": [{"fixed_version": "1.21"}], } ] } }, ) ecr_stubber.add_response( "describe_images", { "imageDetails": [ {"imageDigest": "sha256:abcdef", "registryId": "123456789012"} ] }, {"repositoryName": "dummy", "imageIds": [{"imageTag": "latest"}]}, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 1 def test_warning_finding_due_to_low_severity( mock_inspector_sbomgen, inspector_stubber, monkeypatch, mock_image_config ): monkeypatch.setattr("config.VULNERABILITY_SEVERITIES_TO_FAIL", ["HIGH"]) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-003", "severity": "high", "published": (datetime.now() - timedelta(days=10)).strftime( "%Y-%m-%dT%H:%M:%SZ" ), } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 2 def test_block_finding_due_to_age( mock_inspector_sbomgen, inspector_stubber, ecr_stubber, monkeypatch, mock_image_config, ): monkeypatch.setattr("config.ECR_REPOSITORY_NAME", "dummy") monkeypatch.setattr("config.IMAGE_TAG", "latest") monkeypatch.setattr("config.DAYS_FOR_ERROR", {"HIGH": 30}) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-004", "severity": "high", "published": "2021-06-01T08:15:07Z", "affects": [{"fixed_version": "1.1.0"}], } ] } }, ) ecr_stubber.add_response( "describe_images", { "imageDetails": [ {"imageDigest": "sha256:abcdef", "registryId": "123456789012"} ] }, {"repositoryName": "dummy", "imageIds": [{"imageTag": "latest"}]}, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 1 def test_mixed_block_and_warning_findings( mock_inspector_sbomgen, inspector_stubber, ecr_stubber, monkeypatch, mock_image_config, ): monkeypatch.setattr("config.ECR_REPOSITORY_NAME", "dummy") monkeypatch.setattr("config.IMAGE_TAG", "latest") monkeypatch.setattr("config.VULNERABILITY_SEVERITIES_TO_FAIL", ["HIGH"]) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-005", "severity": "high", "published": datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"), "affects": [{"fixed_version": "1.2.0"}], }, { "id": "CVE-006", "severity": "critical", "published": (datetime.now() - timedelta(days=10)).strftime( "%Y-%m-%dT%H:%M:%SZ" ), }, ] } }, ) ecr_stubber.add_response( "describe_images", { "imageDetails": [ {"imageDigest": "sha256:abcdef", "registryId": "123456789012"} ] }, {"repositoryName": "dummy", "imageIds": [{"imageTag": "latest"}]}, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 2 def test_fixed_not_available_findings( mock_inspector_sbomgen, inspector_stubber, ecr_stubber, monkeypatch, mock_image_config, ): monkeypatch.setattr("config.ECR_REPOSITORY_NAME", "dummy") monkeypatch.setattr("config.IMAGE_TAG", "latest") monkeypatch.setattr("config.DAYS_FOR_ERROR", {"NON_BLOCKING": 90}) # Add a response where the vulnerability has no fixed version inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-2023-7104", "severity": "high", "published": "2023-12-29T10:15:13Z", "affects": [ { "installed_version": "pkg:deb/debian/sqlite3@3.40.1?arch=amd64&distro=bookworm&epoch=0" } ], } ] } }, ) # Stub the ECR response ecr_stubber.add_response( "describe_images", { "imageDetails": [ {"imageDigest": "sha256:abcdef", "registryId": "123456789012"} ] }, {"repositoryName": "dummy", "imageIds": [{"imageTag": "latest"}]}, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 1 def test_fixed_not_available_below_threshold( mock_inspector_sbomgen, inspector_stubber, ecr_stubber, monkeypatch, mock_image_config, ): monkeypatch.setattr("config.ECR_REPOSITORY_NAME", "dummy") monkeypatch.setattr("config.IMAGE_TAG", "latest") monkeypatch.setattr("config.DAYS_FOR_ERROR", {"NON_BLOCKING": 90}) # Vulnerability created within the threshold period (e.g., 45 days ago) non_block_date = (datetime.now() - timedelta(days=45)).strftime( "%Y-%m-%dT%H:%M:%SZ" ) # Add a response where the vulnerability has no fixed version, but threshold hasn't been exceeded inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-2023-7106", "severity": "high", "published": non_block_date, "affects": [ { "installed_version": "pkg:deb/debian/sqlite3@3.40.1?arch=amd64&distro=bookworm&epoch=0" } ], } ] } }, ) # Stub the ECR response ecr_stubber.add_response( "describe_images", { "imageDetails": [ {"imageDigest": "sha256:abcdef", "registryId": "123456789012"} ] }, {"repositoryName": "dummy", "imageIds": [{"imageTag": "latest"}]}, ) # Since the threshold hasn't been exceeded, the exit code should be 2 with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 2 def test_findings_ignored_by_severity( mock_inspector_sbomgen, inspector_stubber, monkeypatch, mock_image_config ): monkeypatch.setattr("config.VULNERABILITY_SEVERITIES_TO_FAIL", ["CRITICAL"]) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_findings_ignored_explicitly( mock_inspector_sbomgen, inspector_stubber, monkeypatch, mock_image_config ): monkeypatch.setattr("config.VULNERABILITIES_TO_IGNORE", ["CVE-001"]) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_findings_ignored_by_file_path( mock_inspector_sbomgen, inspector_stubber, mock_image_config ): inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [ {"path": "/usr/local/lib/ruby/gems/blah/Gemfile.lock"} ], } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_findings_ignored_by_nested_package_lock_path( mock_inspector_sbomgen, inspector_stubber, mock_image_config ): inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [ { "path": ( "/app/node_modules/@apollo/protobufjs/" "node_modules/foo/package-lock.json" ) } ], } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_findings_not_ignored_for_top_level_package_lock( mock_inspector_sbomgen, inspector_stubber, mock_image_config ): inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [{"path": "/app/package-lock.json"}], } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code != 0 def test_findings_ignored_by_os( mock_inspector_sbomgen, inspector_stubber, monkeypatch, mock_image_config ): monkeypatch.setattr( "config.EXCEPTIONS_BY_OS", {"Debian GNU/Linux": {"11": ["CVE-001"]}} ) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_findings_ignored_by_parent_image( mock_inspector_sbomgen, inspector_stubber, monkeypatch, mock_image_config ): monkeypatch.setattr( "config.EXCEPTIONS_BY_PARENT_IMAGE", {"debian11-test": ["CVE-001"]} ) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_kernel_findings_ignored_if_no_fix_available( mock_inspector_sbomgen, inspector_stubber, mock_image_config ): inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [ { "installed_version": "pkg:deb/debian/linux@6.1.76-1?arch=arm64&distro=bookworm&epoch=0" } ], } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 def test_kernel_findings_not_ignored_if_fix_available( mock_inspector_sbomgen, inspector_stubber, mock_image_config ): inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [ { "installed_version": "pkg:deb/debian/linux@6.1.76-1?arch=arm64&distro=bookworm&epoch=0", "fixed_version": "6.1.77-1", } ], } ] } }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 1 def test_ignored_vulnerability_no_longer_present( mock_inspector_sbomgen, inspector_stubber, monkeypatch, caplog, mock_image_config ): monkeypatch.setattr("config.VULNERABILITIES_TO_IGNORE", ["CVE-001"]) inspector_stubber.add_response( "scan_sbom", {"sbom": {"vulnerabilities": []}}, ) with caplog.at_level("WARNING"): with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 assert any( "Warning: Ignored vulnerability CVE-001 is no longer present. Consider removing it from the ignore list." in message for message in caplog.messages ) def test_os_exception_no_longer_present( mock_inspector_sbomgen, inspector_stubber, monkeypatch, caplog, mock_image_config ): monkeypatch.setattr( "config.EXCEPTIONS_BY_PARENT_IMAGE", {"debian11-test": ["CVE-001"]} ) inspector_stubber.add_response( "scan_sbom", {"sbom": {"vulnerabilities": []}}, ) with caplog.at_level("WARNING"): with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 assert any( "Warning: Vulnerability CVE-001 is not present but is defined as an exception for the " "following parent images: ['debian11-test']. Consider removing it from the ignore list." in message for message in caplog.messages ) def test_parent_image_exception_no_longer_present( mock_inspector_sbomgen, inspector_stubber, monkeypatch, caplog, mock_image_config ): monkeypatch.setattr( "config.EXCEPTIONS_BY_OS", {"Debian GNU/Linux": {"11": ["CVE-001"]}} ) inspector_stubber.add_response( "scan_sbom", {"sbom": {"vulnerabilities": []}}, ) with caplog.at_level("WARNING"): with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 0 assert any( "Warning: Vulnerability CVE-001 is not present but is defined as an OS-level exception " "for OS (Debian GNU/Linux 11). Consider removing it from the ignore list." in message for message in caplog.messages ) def test_findings_written_to_output_file(monkeypatch, tmp_path): output_file = tmp_path / "findings.txt" monkeypatch.setattr("config.FINDINGS_OUTPUT_FILE", str(output_file)) block_finding = { "id": "CVE-001", "severity": "high", "published": "2021-07-13T08:15:07Z", "affects": [{"fixed_version": "1.21"}], } main.print_findings([block_finding], []) written_contents = output_file.read_text() assert "CVE-001" in written_contents def test_findings_not_written_to_output_file_when_unset(monkeypatch, tmp_path): monkeypatch.setattr("config.FINDINGS_OUTPUT_FILE", None) block_finding = { "id": "CVE-001", "severity": "high", "published": "2021-07-13T08:15:07Z", "affects": [{"fixed_version": "1.21"}], } main.print_findings([block_finding], []) assert list(tmp_path.iterdir()) == [] def test_get_image_config(mock_image_tar): image_config = main.get_image_config() assert image_config == {"Labels": {"foo": "bar"}} def test_scan_sbom_in_batches( mock_inspector_sbomgen, inspector_stubber, mock_image_config, monkeypatch, caplog ): monkeypatch.setattr("config.MAXIMUM_NUMBER_OF_SBOM_COMPONENTS", 1) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [ { "installed_version": "pkg:pypi/pip@9.0.3", } ], } ] } }, { "outputFormat": "INSPECTOR", "sbom": json.dumps( { "components": [ { "bom-ref": "comp-3", "type": "library", "name": "pip", "version": "9.0.3", "purl": "pkg:pypi/pip@9.0.3", } ] } ), }, ) inspector_stubber.add_response( "scan_sbom", { "sbom": { "vulnerabilities": [ { "id": "CVE-001", "severity": "high", "published": "2021-08-01T08:15:07Z", "affects": [ { "installed_version": "pkg:deb/debian/sqlite@6.1.76", } ], } ] } }, { "outputFormat": "INSPECTOR", "sbom": json.dumps( { "components": [ { "bom-ref": "comp-2", "type": "operating-system", "name": "Debian GNU/Linux", "version": "11", } ] } ), }, ) with pytest.raises(SystemExit) as e: main.main() assert e.value.code == 1 assert any("Found 1 total findings" in message for message in caplog.messages)