diff --git a/src/macaron/slsa_analyzer/checks/provenance_witness_l1_check.py b/src/macaron/slsa_analyzer/checks/provenance_witness_l1_check.py index 8b274d8ef..c1eaff4e6 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_witness_l1_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_witness_l1_check.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import Mapped, mapped_column from macaron.database.table_definitions import CheckFacts +from macaron.errors import MacaronError from macaron.slsa_analyzer.analyze_context import AnalyzeContext from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType @@ -27,6 +28,10 @@ logger: logging.Logger = logging.getLogger(__name__) +class WitnessProvenanceException(MacaronError): + """When there is an error while processing a Witness provenance.""" + + class WitnessProvenanceAvailableFacts(CheckFacts): """The ORM mapping for justifications in provenance l3 check.""" @@ -66,6 +71,11 @@ def verify_artifact_assets( ------- bool True if verification succeeds and False otherwise. + + Raises + ------ + WitnessProvenanceException + If a subject is not a file attested by the Witness product attestor. """ # A look-up table to verify: # 1. if the name of the artifact appears in any subject of the witness provenance, then @@ -73,13 +83,20 @@ def verify_artifact_assets( look_up: dict[str, dict[str, InTotoV01Subject]] = {} for subject in subjects: - if subject["name"] not in look_up: - look_up[subject["name"]] = {} - look_up[subject["name"]][subject["digest"]["sha256"]] = subject + if not subject["name"].startswith("https://witness.dev/attestations/product/v0.1/file:"): + raise WitnessProvenanceException( + f"{subject['name']} is not a file attested by the Witness product attestor." + ) + + # Get the artifact name, which should be the last part of the artifact subject value. + _, _, artifact_filename = subject["name"].rpartition("/") + if artifact_filename not in look_up: + look_up[artifact_filename] = {} + look_up[artifact_filename][subject["digest"]["sha256"]] = subject for asset in artifact_assets: if asset.name not in look_up: - message = f"Could not find subject with name {asset.name} in the provenance." + message = f"Could not find subject for asset {asset.name} in the provenance." logger.info(message) return False @@ -169,7 +186,16 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: ) subjects = extract_build_artifacts_from_witness_subjects(provenance.payload) - if not verify_artifact_assets(artifact_assets, subjects): + try: + verify_status = verify_artifact_assets(artifact_assets, subjects) + except WitnessProvenanceException as err: + logger.error(err) + return CheckResultData( + result_tables=result_tables, + result_type=CheckResultType.UNKNOWN, + ) + + if not verify_status: return CheckResultData( result_tables=result_tables, result_type=CheckResultType.FAILED, diff --git a/src/macaron/slsa_analyzer/provenance/witness/__init__.py b/src/macaron/slsa_analyzer/provenance/witness/__init__.py index cbd1c7b1b..68d84ccc2 100644 --- a/src/macaron/slsa_analyzer/provenance/witness/__init__.py +++ b/src/macaron/slsa_analyzer/provenance/witness/__init__.py @@ -82,21 +82,6 @@ def is_witness_provenance_payload( return isinstance(payload, InTotoV01Payload) and payload.statement["predicateType"] in predicate_types -class WitnessProvenanceSubject(NamedTuple): - """A helper class to store elements of the ``subject`` list in the provenances.""" - - #: The ``"name"`` field of each ``subject``. - subject_name: str - #: The SHA256 digest of the corresponding asset to the subject. - sha256_digest: str - - @property - def artifact_name(self) -> str: - """Get the artifact name, which should be the last part of the subject.""" - _, _, artifact_name = self.subject_name.rpartition("/") - return artifact_name - - def extract_repo_url(witness_payload: InTotoPayload) -> str | None: """Extract the repo URL from the witness provenance payload. diff --git a/tests/integration/cases/behnazh-w_example-maven-app/policy.dl b/tests/integration/cases/behnazh-w_example-maven-app/policy.dl index 5684c680e..2804959dc 100644 --- a/tests/integration/cases/behnazh-w_example-maven-app/policy.dl +++ b/tests/integration/cases/behnazh-w_example-maven-app/policy.dl @@ -7,7 +7,11 @@ Policy("example_maven_app_policy", component_id, "Policy for github Maven projec check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_build_script_1"), check_passed(component_id, "mcn_provenance_available_1"), - check_passed(component_id, "mcn_provenance_expectation_1"). + check_passed(component_id, "mcn_provenance_expectation_1"), + // We expect mcn_provenance_witness_level_one_1 to fail because at the moment + // it tries to discover the witness provenance even when the provenance is provided as input. + // TODO: address this policy once the issue with mcn_provenance_witness_level_one_1 is addressed. + check_failed(component_id, "mcn_provenance_witness_level_one_1"). apply_policy_to("example_maven_app_policy", component_id) :- is_repo( diff --git a/tests/integration/cases/behnazh-w_example-maven-app/policy_report.json b/tests/integration/cases/behnazh-w_example-maven-app/policy_report.json index 19fe98188..5838e5c2e 100644 --- a/tests/integration/cases/behnazh-w_example-maven-app/policy_report.json +++ b/tests/integration/cases/behnazh-w_example-maven-app/policy_report.json @@ -1,7 +1,7 @@ { "component_satisfies_policy": [ [ - "176", + "1", "pkg:maven/io.github.behnazh-w.demo/example-maven-app@1.0-SNAPSHOT?type=jar", "example_maven_app_policy" ], @@ -11,11 +11,11 @@ "example_maven_app_policy" ] ], - "component_violates_policy": [], - "failed_policies": [], "passed_policies": [ [ "example_maven_app_policy" ] - ] + ], + "component_violates_policy": [], + "failed_policies": [] } diff --git a/tests/integration/cases/behnazh-w_example-maven-app/vsa_payload.json b/tests/integration/cases/behnazh-w_example-maven-app/vsa_payload.json index f8c6ce09f..3dd63a548 100644 --- a/tests/integration/cases/behnazh-w_example-maven-app/vsa_payload.json +++ b/tests/integration/cases/behnazh-w_example-maven-app/vsa_payload.json @@ -1,33 +1,33 @@ { - "_type": "https://in-toto.io/Statement/v1", - "subject": [ - { - "uri": "pkg:maven/io.github.behnazh-w.demo/example-maven-app@1.0-SNAPSHOT?type=jar", - "digest": { - "sha256": "19986144a60f3d16d1e8d96bc1807c42bb7c91068ab3018b85033f62c2845921" - } - }, - { - "uri": "pkg:maven/io.github.behnazh-w.demo/example-maven-app@1.0?type=jar", - "digest": { - "sha256": "759a3c7f76ef2c467cedb814ed3fd38cb9125126664f66f9a62d1cfa0e54b6b7" - } + "_type": "https://in-toto.io/Statement/v1", + "subject": [ + { + "uri": "pkg:maven/io.github.behnazh-w.demo/example-maven-app@1.0-SNAPSHOT?type=jar", + "digest": { + "sha256": "19986144a60f3d16d1e8d96bc1807c42bb7c91068ab3018b85033f62c2845921" + } + }, + { + "uri": "pkg:maven/io.github.behnazh-w.demo/example-maven-app@1.0?type=jar", + "digest": { + "sha256": "759a3c7f76ef2c467cedb814ed3fd38cb9125126664f66f9a62d1cfa0e54b6b7" + } + } + ], + "predicateType": "https://slsa.dev/verification_summary/v1", + "predicate": { + "verifier": { + "id": "https://github.com/oracle/macaron", + "version": { + "macaron": "0.11.0" + } + }, + "timeVerified": "2024-07-23T05:34:41.564563+00:00", + "resourceUri": "", + "policy": { + "content": "/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */\n/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */\n\n#include \"prelude.dl\"\n\nPolicy(\"example_maven_app_policy\", component_id, \"Policy for github Maven project with Witness and GitHub provenances\") :-\n check_passed(component_id, \"mcn_build_service_1\"),\n check_passed(component_id, \"mcn_build_script_1\"),\n check_passed(component_id, \"mcn_provenance_available_1\"),\n check_passed(component_id, \"mcn_provenance_expectation_1\"),\n // We expect mcn_provenance_witness_level_one_1 to fail because at the moment\n // it tries to discover the witness provenance even when the provenance is provided as input.\n // TODO: address this policy once the issue with mcn_provenance_witness_level_one_1 is addressed.\n check_failed(component_id, \"mcn_provenance_witness_level_one_1\").\n\napply_policy_to(\"example_maven_app_policy\", component_id) :-\n is_repo(\n _, // repo_id\n \"github.com/behnazh-w/example-maven-app\", // http URL to the repo but without the \"http://\"\n component_id\n ).\n" + }, + "verificationResult": "PASSED", + "verifiedLevels": [] } - ], - "predicateType": "https://slsa.dev/verification_summary/v1", - "predicate": { - "verifier": { - "id": "https://github.com/oracle/macaron", - "version": { - "macaron": "0.9.0" - } - }, - "timeVerified": "2024-05-07T05:32:42.105941+00:00", - "resourceUri": "", - "policy": { - "content": "/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */\n/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */\n\n#include \"prelude.dl\"\n\nPolicy(\"example_maven_app_policy\", component_id, \"Policy for github Maven project with Witness and GitHub provenances\") :-\n check_passed(component_id, \"mcn_build_service_1\"),\n check_passed(component_id, \"mcn_build_script_1\"),\n check_passed(component_id, \"mcn_provenance_available_1\"),\n check_passed(component_id, \"mcn_provenance_expectation_1\").\n\napply_policy_to(\"example_maven_app_policy\", component_id) :-\n is_repo(\n _, // repo_id\n \"github.com/behnazh-w/example-maven-app\", // http URL to the repo but without the \"http://\"\n component_id\n ).\n" - }, - "verificationResult": "PASSED", - "verifiedLevels": [] - } } diff --git a/tests/slsa_analyzer/checks/test_provenance_witness_l1_check.py b/tests/slsa_analyzer/checks/test_provenance_witness_l1_check.py index ed13760b8..de1e7aae7 100644 --- a/tests/slsa_analyzer/checks/test_provenance_witness_l1_check.py +++ b/tests/slsa_analyzer/checks/test_provenance_witness_l1_check.py @@ -1,4 +1,241 @@ -# Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """Test the check ``provenance_witness_l1_check``.""" + +import pytest + +from macaron.slsa_analyzer.checks.provenance_witness_l1_check import WitnessProvenanceException, verify_artifact_assets +from macaron.slsa_analyzer.package_registry.jfrog_maven_registry import ( + JFrogMavenAsset, + JFrogMavenAssetMetadata, + JFrogMavenRegistry, +) +from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Subject + + +@pytest.fixture(name="subjects") +def subjects_() -> list[InTotoV01Subject]: + """Return the list of subjects in an example witness provenance.""" + return [ + { + "name": "https://witness.dev/attestations/product/v0.1/file:target/boo-1.0.0.jar", + "digest": { + "sha256": "cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + }, + }, + { + "name": "https://witness.dev/attestations/product/v0.1/file:sources/boo-1.0.0-sources.jar", + "digest": { + "sha256": "6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e", + }, + }, + { + "name": "https://witness.dev/attestations/product/v0.1/file:foo/bar/boo-1.0.0.jar", + "digest": { + "sha256": "d2238e45bb212fbe4a2e8e7dc160ffa123bacc5396899a039e973d2930eb4e41", + }, + }, + ] + + +@pytest.mark.parametrize( + ("artifact_assets"), + [ + pytest.param( + [ + JFrogMavenAsset( + name="boo-1.0.0.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=50, + sha256_digest="cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/target/boo-1.0.0.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + JFrogMavenAsset( + name="boo-1.0.0-sources.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=100, + sha256_digest="6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/sources/boo-1.0.0-sources.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + ], + id="The assets list can match only a subset of subjects, as long as all assets in that list are verified.", + ), + pytest.param( + [ + JFrogMavenAsset( + name="boo-1.0.0.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=50, + sha256_digest="cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/target/boo-1.0.0.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + JFrogMavenAsset( + name="boo-1.0.0.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=99, + sha256_digest="d2238e45bb212fbe4a2e8e7dc160ffa123bacc5396899a039e973d2930eb4e41", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/foo/bar/boo-1.0.0.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + ], + id="Two assets can share the same file name but have different digests.", + ), + ], +) +def test_verify_artifact_assets( + artifact_assets: list[JFrogMavenAsset], + subjects: list[InTotoV01Subject], +) -> None: + """Test the verify_artifact_assets function.""" + assert verify_artifact_assets( + artifact_assets=artifact_assets, + subjects=subjects, + ) + + +@pytest.mark.parametrize( + ("artifact_assets"), + [ + pytest.param( + [ + JFrogMavenAsset( + name="boo-1.0.0.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=50, + sha256_digest="cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/target/boo-1.0.0.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + JFrogMavenAsset( + name="this-does-not-exist", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=50, + sha256_digest="cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/this-does-not-exist", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + ], + id="An asset that is not attested will lead to a failed verification.", + ), + pytest.param( + [ + JFrogMavenAsset( + name="boo-1.0.0.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=50, + sha256_digest="aaa123", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/target/boo-1.0.0.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + ], + id="An asset with mismatched sha256_digest will lead to a failed verification.", + ), + ], +) +def test_failed_verify_artifact_assets( + artifact_assets: list[JFrogMavenAsset], + subjects: list[InTotoV01Subject], +) -> None: + """Test the verify_artifact_assets function with invalid assets.""" + assert not verify_artifact_assets( + artifact_assets=artifact_assets, + subjects=subjects, + ) + + +@pytest.mark.parametrize( + ("artifact_assets", "non_product_subjects"), + [ + pytest.param( + [ + JFrogMavenAsset( + name="boo-1.0.0.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=50, + sha256_digest="cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/target/boo-1.0.0.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + JFrogMavenAsset( + name="boo-1.0.0-sources.jar", + group_id="io.oracle.macaron", + artifact_id="boo", + version="1.0.0", + metadata=JFrogMavenAssetMetadata( + size_in_bytes=100, + sha256_digest="6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e", + download_uri="https://artifactory.com/repo/io/oracle/macaron/boo/1.0.0/sources/boo-1.0.0-sources.jar", + ), + jfrog_maven_registry=JFrogMavenRegistry(), + ), + ], + [ + { + "name": "https://witness.dev/attestations/product/v0.1/file:target/boo-1.0.0.jar", + "digest": { + "sha256": "cbc8f554dbfa17e5c5873c425a09cb1488c2f784ac52340747a92b7ec0aaefba", + }, + }, + { + "name": "foo/bar/boo-1.0.0.jar", + "digest": { + "sha256": "d2238e45bb212fbe4a2e8e7dc160ffa123bacc5396899a039e973d2930eb4e41", + }, + }, + { + "name": "https://witness.dev/attestations/gitlab/v0.1/projecturl:https://gitlab.com/boo/foo", + "digest": { + "sha256": "24521ae8555394253bdcab117183cc4f96cb5b1a23fee0f81d8cb7221595b536", + }, + }, + ], + id="Any subject that is not a Witness Product attestator file type will cause an exception.", + ) + ], +) +def test_non_product_witness_subject( + artifact_assets: list[JFrogMavenAsset], + non_product_subjects: list[InTotoV01Subject], +) -> None: + """A subject that is not a file attested by the Witness product attestator should raise an exception.""" + with pytest.raises(WitnessProvenanceException): + verify_artifact_assets( + artifact_assets=artifact_assets, + subjects=non_product_subjects, + )