diff --git a/docs/source/pages/cli_usage/command_analyze.rst b/docs/source/pages/cli_usage/command_analyze.rst index 961ebbc1f..a04f88bd2 100644 --- a/docs/source/pages/cli_usage/command_analyze.rst +++ b/docs/source/pages/cli_usage/command_analyze.rst @@ -80,6 +80,11 @@ Options The path to the local .m2 directory. If this option is not used, Macaron will use the default location at $HOME/.m2 +.. option:: --verify-provenance + + Allow the analysis to attempt to verify provenance files as part of its normal operations. + + ----------- Environment ----------- diff --git a/docs/source/pages/developers_guide/apidoc/macaron.provenance.rst b/docs/source/pages/developers_guide/apidoc/macaron.provenance.rst new file mode 100644 index 000000000..d2b68aa0b --- /dev/null +++ b/docs/source/pages/developers_guide/apidoc/macaron.provenance.rst @@ -0,0 +1,34 @@ +macaron.provenance package +========================== + +.. automodule:: macaron.provenance + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +macaron.provenance.provenance\_extractor module +----------------------------------------------- + +.. automodule:: macaron.provenance.provenance_extractor + :members: + :undoc-members: + :show-inheritance: + +macaron.provenance.provenance\_finder module +-------------------------------------------- + +.. automodule:: macaron.provenance.provenance_finder + :members: + :undoc-members: + :show-inheritance: + +macaron.provenance.provenance\_verifier module +---------------------------------------------- + +.. automodule:: macaron.provenance.provenance_verifier + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst b/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst index 724c2614f..c75a00e3d 100644 --- a/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst +++ b/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst @@ -17,22 +17,6 @@ macaron.repo\_finder.commit\_finder module :undoc-members: :show-inheritance: -macaron.repo\_finder.provenance\_extractor module -------------------------------------------------- - -.. automodule:: macaron.repo_finder.provenance_extractor - :members: - :undoc-members: - :show-inheritance: - -macaron.repo\_finder.provenance\_finder module ----------------------------------------------- - -.. automodule:: macaron.repo_finder.provenance_finder - :members: - :undoc-members: - :show-inheritance: - macaron.repo\_finder.repo\_finder module ---------------------------------------- diff --git a/docs/source/pages/developers_guide/apidoc/macaron.rst b/docs/source/pages/developers_guide/apidoc/macaron.rst index d8fdf3e64..b1910f0aa 100644 --- a/docs/source/pages/developers_guide/apidoc/macaron.rst +++ b/docs/source/pages/developers_guide/apidoc/macaron.rst @@ -20,6 +20,7 @@ Subpackages macaron.output_reporter macaron.parsers macaron.policy_engine + macaron.provenance macaron.repo_finder macaron.repo_verifier macaron.slsa_analyzer diff --git a/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst b/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst index 75e216b56..d843de46a 100644 --- a/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst +++ b/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst @@ -89,14 +89,6 @@ macaron.slsa\_analyzer.checks.provenance\_commit\_check module :undoc-members: :show-inheritance: -macaron.slsa\_analyzer.checks.provenance\_l3\_check module ----------------------------------------------------------- - -.. automodule:: macaron.slsa_analyzer.checks.provenance_l3_check - :members: - :undoc-members: - :show-inheritance: - macaron.slsa\_analyzer.checks.provenance\_l3\_content\_check module ------------------------------------------------------------------- diff --git a/docs/source/pages/tutorials/npm_provenance.rst b/docs/source/pages/tutorials/npm_provenance.rst index bbd8ec4b4..6a591e716 100644 --- a/docs/source/pages/tutorials/npm_provenance.rst +++ b/docs/source/pages/tutorials/npm_provenance.rst @@ -42,7 +42,7 @@ To perform an analysis on the latest version of semver (when this tutorial was w .. code-block:: shell - ./run_macaron.sh analyze -purl pkg:npm/semver@7.6.2 + ./run_macaron.sh analyze -purl pkg:npm/semver@7.6.2 --verify-provenance The analysis involves Macaron downloading the contents of the target repository to the configured, or default, ``output`` folder. Results from the analysis, including checks, are stored in the database found at ``output/macaron.db`` (See :ref:`Output Files Guide `). Once the analysis is complete, Macaron will also produce a report in the form of a HTML file. @@ -52,7 +52,7 @@ During this analysis, Macaron will retrieve two provenance files from the npm re .. note:: Most of the details from the two provenance files can be found through the links provided on the artifacts page on the npm website. In particular: `Sigstore Rekor `_. The provenance file itself can be found at: `npm registry `_. -Of course to reliably say the above does what is claimed here, proof is needed. For this we can rely on the check results produced from the analysis run. In particular, we want to know the results of three checks: ``mcn_provenance_derived_repo_1``, ``mcn_provenance_derived_commit_1``, and ``mcn_provenance_verified_1``. The first two to ensure that the commit and the repository being analyzed match those found in the provenance file, and the last check to ensure that the provenance file has been verified. +Of course to reliably say the above does what is claimed here, proof is needed. For this we can rely on the check results produced from the analysis run. In particular, we want to know the results of three checks: ``mcn_provenance_derived_repo_1``, ``mcn_provenance_derived_commit_1``, and ``mcn_provenance_verified_1``. The first two to ensure that the commit and the repository being analyzed match those found in the provenance file, and the last check to ensure that the provenance file has been verified. For the third check to succeed, you need to enable provenance verification in Macaron by using the ``--verify-provenance`` command-line argument, as demonstrated above. This verification is disabled by default because it can be slow in some cases due to I/O-bound operations. .. _fig_semver_7.6.2_report: diff --git a/src/macaron/__main__.py b/src/macaron/__main__.py index da727cdaa..52c441914 100644 --- a/src/macaron/__main__.py +++ b/src/macaron/__main__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This is the main entrypoint to run Macaron.""" @@ -32,7 +32,6 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None: """Run the SLSA checks against a single target repository.""" - deps_depth = None if analyzer_single_args.deps_depth == "inf": deps_depth = -1 else: @@ -173,7 +172,8 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None analyzer_single_args.sbom_path, deps_depth, provenance_payload=prov_payload, - validate_malware_switch=analyzer_single_args.validate_malware_switch, + validate_malware=analyzer_single_args.validate_malware, + verify_provenance=analyzer_single_args.verify_provenance, ) sys.exit(status_code) @@ -360,7 +360,7 @@ def main(argv: list[str] | None = None) -> None: help="The directory where Macaron looks for already cloned repositories.", ) - # Add sub parsers for each action + # Add sub parsers for each action. sub_parser = main_parser.add_subparsers(dest="action", help="Run macaron --help for help") # Use Macaron to analyze one single repository. @@ -470,12 +470,19 @@ def main(argv: list[str] | None = None) -> None: ) single_analyze_parser.add_argument( - "--validate-malware-switch", + "--validate-malware", required=False, action="store_true", help=("Enable malware validation."), ) + single_analyze_parser.add_argument( + "--verify-provenance", + required=False, + action="store_true", + help=("Allow the analysis to attempt to verify provenance files as part of its normal operations."), + ) + # Dump the default values. sub_parser.add_parser(name="dump-defaults", description="Dumps the defaults.ini file to the output directory.") diff --git a/src/macaron/config/defaults.ini b/src/macaron/config/defaults.ini index 5faab65fb..93c403baf 100644 --- a/src/macaron/config/defaults.ini +++ b/src/macaron/config/defaults.ini @@ -46,11 +46,6 @@ validate = True # The CycloneDX schema version used for validation. schema = 1.6 -# This is the Analyzer section used as part of Macaron's analysis. -[analyzer] -# This enables or disables attempts at verification of provenance. -verify_provenance = True - # This is the repo finder script. [repofinder] find_repos = True @@ -569,7 +564,7 @@ purl_endpoint = v3alpha/purl # [analysis.checks] # exclude = # mcn_build_as_code_1 -# mcn_provenance_level_three_1 +# mcn_provenance_verified_1 # include = * # ``` # 3. Exclude multiple checks that start with `mcn_provenance`: diff --git a/src/macaron/database/db_custom_types.py b/src/macaron/database/db_custom_types.py index f40256099..e67d22b3a 100644 --- a/src/macaron/database/db_custom_types.py +++ b/src/macaron/database/db_custom_types.py @@ -1,13 +1,21 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. -"""This module implements SQLAlchemy type for converting date format to RFC3339 string representation.""" +"""This module implements SQLAlchemy types for Python data types that cannot be automatically stored.""" import datetime +import json from typing import Any from sqlalchemy import JSON, String, TypeDecorator +from macaron.slsa_analyzer.provenance.intoto import ( + InTotoPayload, + InTotoV01Payload, + InTotoV1Payload, + validate_intoto_payload, +) + class RFC3339DateTime(TypeDecorator): # pylint: disable=W0223 """ @@ -36,7 +44,7 @@ def process_bind_param(self, value: None | Any, dialect: Any) -> None | str: if the provided ``datetime`` is a naive ``datetime`` object then UTC is added. value: None | datetime.datetime - The value being stored + The value being stored. """ if value is None: return None @@ -52,7 +60,7 @@ def process_result_value(self, value: None | str, dialect: Any) -> None | dateti If the deserialized ``datetime`` has a timezone then return it, otherwise add UTC as its timezone. value: None | str - The value being loaded + The value being loaded. """ if value is None: return None @@ -76,7 +84,7 @@ def process_bind_param(self, value: None | dict, dialect: Any) -> None | dict: """Process when storing a dict object to the SQLite db. value: None | dict - The value being stored + The value being stored. """ if not isinstance(value, dict): raise TypeError("DBJsonDict type expects a dict.") @@ -87,8 +95,63 @@ def process_result_value(self, value: None | dict, dialect: Any) -> None | dict: """Process when loading a dict object from the SQLite db. value: None | dict - The value being loaded + The value being loaded. """ if not isinstance(value, dict): raise TypeError("DBJsonDict type expects a dict.") return value + + +class ProvenancePayload(TypeDecorator): # pylint: disable=W0223 + """SQLAlchemy column type to serialize InTotoProvenance.""" + + # It is stored in the database as a String value. + impl = String + + # To prevent Sphinx from rendering the docstrings for `cache_ok`, make this docstring private. + #: :meta private: + cache_ok = True + + def process_bind_param(self, value: InTotoPayload | None, dialect: Any) -> str | None: + """Process when storing an InTotoPayload object to the SQLite db. + + value: InTotoPayload | None + The value being stored. + """ + if value is None: + return None + + if not isinstance(value, InTotoPayload): + raise TypeError("ProvenancePayload type expects an InTotoPayload.") + + payload_type = value.__class__.__name__ + payload_dict = {"payload_type": payload_type, "payload": value.statement} + return json.dumps(payload_dict) + + def process_result_value(self, value: str | None, dialect: Any) -> InTotoPayload | None: + """Process when loading an InTotoPayload object from the SQLite db. + + value: str | None + The value being loaded. + """ + if value is None: + return None + + try: + payload_dict = json.loads(value) + except ValueError as error: + raise TypeError(f"Error parsing str as JSON: {error}") from error + + if not isinstance(payload_dict, dict): + raise TypeError("Parsed data is not a dict.") + + if "payload_type" not in payload_dict or "payload" not in payload_dict: + raise TypeError("Missing keys in dict for ProvenancePayload type.") + + payload = payload_dict["payload"] + if payload_dict["payload_type"] == "InTotoV01Payload": + return InTotoV01Payload(statement=payload) + if payload_dict["payload_type"] == "InTotoV1Payload": + return InTotoV1Payload(statement=payload) + + return validate_intoto_payload(payload) diff --git a/src/macaron/database/table_definitions.py b/src/macaron/database/table_definitions.py index d9e1d7f17..2a7f1e95a 100644 --- a/src/macaron/database/table_definitions.py +++ b/src/macaron/database/table_definitions.py @@ -34,7 +34,7 @@ from macaron.artifact.maven import MavenSubjectPURLMatcher from macaron.database.database_manager import ORMBase -from macaron.database.db_custom_types import RFC3339DateTime +from macaron.database.db_custom_types import ProvenancePayload, RFC3339DateTime from macaron.errors import InvalidPURLError from macaron.repo_finder.repo_finder_enums import CommitFinderInfo, RepoFinderInfo from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, ProvenanceSubjectPURLMatcher @@ -491,7 +491,10 @@ class Provenance(ORMBase): component: Mapped["Component"] = relationship(back_populates="provenance") #: The SLSA version. - version: Mapped[str] = mapped_column(String, nullable=False) + slsa_version: Mapped[str] = mapped_column(String, nullable=True) + + #: The SLSA level. + slsa_level: Mapped[int] = mapped_column(Integer, default=0) #: The release tag commit sha. release_commit_sha: Mapped[str] = mapped_column(String, nullable=True) @@ -499,8 +502,17 @@ class Provenance(ORMBase): #: The release tag. release_tag: Mapped[str] = mapped_column(String, nullable=True) - #: The provenance payload content in JSON format. - provenance_json: Mapped[str] = mapped_column(String, nullable=False) + #: The repository URL from the provenance. + repository_url: Mapped[str] = mapped_column(String, nullable=True) + + #: The commit sha from the provenance. + commit_sha: Mapped[str] = mapped_column(String, nullable=True) + + #: The provenance payload. + provenance_payload: Mapped[InTotoPayload] = mapped_column(ProvenancePayload, nullable=False) + + #: The verified status of the provenance. + verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) #: A one-to-many relationship with the release artifacts. artifact: Mapped[list["ReleaseArtifact"]] = relationship(back_populates="provenance") diff --git a/src/macaron/provenance/__init__.py b/src/macaron/provenance/__init__.py new file mode 100644 index 000000000..a99afa31c --- /dev/null +++ b/src/macaron/provenance/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024 - 2025, 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/. + +"""This package contains the provenance tools for software components.""" diff --git a/src/macaron/repo_finder/provenance_extractor.py b/src/macaron/provenance/provenance_extractor.py similarity index 95% rename from src/macaron/repo_finder/provenance_extractor.py rename to src/macaron/provenance/provenance_extractor.py index 84c57ed3b..29078c9f3 100644 --- a/src/macaron/repo_finder/provenance_extractor.py +++ b/src/macaron/provenance/provenance_extractor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module contains methods for extracting repository and commit metadata from provenance files.""" @@ -7,16 +7,11 @@ from abc import ABC, abstractmethod from packageurl import PackageURL -from pydriller import Git from macaron.errors import ProvenanceError from macaron.json_tools import JsonType, json_extract from macaron.repo_finder import to_domain_from_known_purl_types -from macaron.repo_finder.commit_finder import ( - AbstractPurlType, - determine_abstract_purl_type, - extract_commit_from_version, -) +from macaron.repo_finder.commit_finder import AbstractPurlType, determine_abstract_purl_type from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV1Payload, InTotoV01Payload from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Statement from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1Statement @@ -48,10 +43,10 @@ def extract_repo_and_commit_from_provenance(payload: InTotoPayload) -> tuple[str If the extraction process fails for any reason. """ predicate_type = payload.statement.get("predicateType") - if isinstance(payload, InTotoV1Payload): - if predicate_type == "https://slsa.dev/provenance/v1": - return _extract_from_slsa_v1(payload) - elif isinstance(payload, InTotoV01Payload): + if isinstance(payload, InTotoV1Payload) and predicate_type == "https://slsa.dev/provenance/v1": + return _extract_from_slsa_v1(payload) + + if isinstance(payload, InTotoV01Payload): if predicate_type == "https://slsa.dev/provenance/v0.2": return _extract_from_slsa_v02(payload) if predicate_type == "https://slsa.dev/provenance/v0.1": @@ -61,12 +56,40 @@ def extract_repo_and_commit_from_provenance(payload: InTotoPayload) -> tuple[str msg = ( f"Extraction from provenance not supported for versions: " - f"predicate_type {predicate_type}, in-toto {str(type(payload))}." + f"predicate_type {payload.statement.get('predicateType')}, in-toto {str(type(payload))}." ) logger.debug(msg) raise ProvenanceError(msg) +def extract_predicate_version(payload: InTotoPayload) -> str | None: + """Extract and return the SLSA version from the passed payload. + + Parameters + ---------- + payload: InTotoPayload + The payload to extract from. + + Returns + ------- + str | None + The SLSA version, or None if the payload does contain a supported version number. + """ + predicate_type = payload.statement.get("predicateType") + if isinstance(payload, InTotoV1Payload) and predicate_type == "https://slsa.dev/provenance/v1": + return "SLSA-1.0" + + if isinstance(payload, InTotoV01Payload): + if predicate_type == "https://slsa.dev/provenance/v0.2": + return "SLSA-0.2" + if predicate_type == "https://slsa.dev/provenance/v0.1": + return "SLSA-0.1" + if predicate_type == "https://witness.testifysec.com/attestation-collection/v0.1": + return "WITNESS-0.1" + + return None + + def _extract_from_slsa_v01(payload: InTotoV01Payload) -> tuple[str | None, str | None]: """Extract the repository and commit metadata from the slsa v01 provenance payload.""" predicate: dict[str, JsonType] | None = payload.statement.get("predicate") @@ -278,27 +301,18 @@ def check_if_input_repo_provenance_conflict( def check_if_input_purl_provenance_conflict( - git_obj: Git, repo_path_input: bool, - digest_input: bool, provenance_repo_url: str | None, - provenance_commit_digest: str | None, purl: PackageURL, ) -> bool: """Test if the input repository type PURL's repo and commit match the contents of the provenance. Parameters ---------- - git_obj: Git - The Git object. repo_path_input: bool True if there is a repo as input. - digest_input: str - True if there is a commit as input. provenance_repo_url: str | None The repo url from provenance. - provenance_commit_digest: str | None - The commit digest from provenance. purl: PackageURL The input repository PURL. @@ -321,18 +335,6 @@ def check_if_input_purl_provenance_conflict( ) return True - # Check the PURL commit against the provenance. - if not digest_input and provenance_commit_digest and purl.version: - purl_commit, _ = extract_commit_from_version(git_obj, purl.version) - if purl_commit and purl_commit != provenance_commit_digest: - logger.debug( - "The commit digest passed via purl input does not match what exists in the " - "provenance. Purl Commit: %s, Provenance Commit: %s.", - purl_commit, - provenance_commit_digest, - ) - return True - return False diff --git a/src/macaron/repo_finder/provenance_finder.py b/src/macaron/provenance/provenance_finder.py similarity index 75% rename from src/macaron/repo_finder/provenance_finder.py rename to src/macaron/provenance/provenance_finder.py index aaf6a312a..b02423eec 100644 --- a/src/macaron/repo_finder/provenance_finder.py +++ b/src/macaron/provenance/provenance_finder.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module contains methods for finding provenance files.""" @@ -107,38 +107,6 @@ def _find_provenance(self, discovery_functions: list[partial[list[InTotoPayload] logger.debug("No provenance found.") return [] - def verify_provenance(self, purl: PackageURL, provenance: list[InTotoPayload]) -> bool: - """Verify the passed provenance. - - Parameters - ---------- - purl: PackageURL - The PURL of the analysis target. - provenance: list[InTotoPayload] - The list of provenance. - - Returns - ------- - bool - True if the provenance could be verified, or False otherwise. - """ - if determine_abstract_purl_type(purl) == AbstractPurlType.REPOSITORY: - # Do not perform default verification for repository type targets. - return False - - verification_function = None - - if purl.type == "npm": - verification_function = partial(verify_npm_provenance, purl, provenance) - - # TODO other verification functions go here. - - if verification_function: - return verification_function() - - logger.debug("Provenance verification not supported for PURL type: %s", purl.type) - return False - def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoPayload]: """Find and download the NPM based provenance for the passed PURL. @@ -213,72 +181,6 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoP return [] -def verify_npm_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool: - """Compare the unsigned payload subject digest with the signed payload digest, if available. - - Parameters - ---------- - purl: PackageURL - The PURL of the analysis target. - provenance: list[InTotoPayload] - The provenances to verify. - - Returns - ------- - bool - True if the provenance was verified, or False otherwise. - """ - if len(provenance) != 2: - logger.debug("Expected unsigned and signed provenance.") - return False - - signed_subjects = provenance[1].statement.get("subject") - if not signed_subjects: - return False - - unsigned_subjects = provenance[0].statement.get("subject") - if not unsigned_subjects: - return False - - found_signed_subject = None - for signed_subject in signed_subjects: - name = signed_subject.get("name") - if name and name == str(purl): - found_signed_subject = signed_subject - break - - if not found_signed_subject: - return False - - found_unsigned_subject = None - for unsigned_subject in unsigned_subjects: - name = unsigned_subject.get("name") - if name and name == str(purl): - found_unsigned_subject = unsigned_subject - break - - if not found_unsigned_subject: - return False - - signed_digest = found_signed_subject.get("digest") - unsigned_digest = found_unsigned_subject.get("digest") - if not (signed_digest and unsigned_digest): - return False - - # For signed and unsigned to match, the digests must be identical. - if signed_digest != unsigned_digest: - return False - - key = list(signed_digest.keys())[0] - logger.debug( - "Verified provenance against signed companion. Signed: %s, Unsigned: %s.", - signed_digest[key][:7], - unsigned_digest[key][:7], - ) - - return True - - def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[InTotoPayload]: """Find and download the GAV based provenance for the passed PURL. @@ -373,7 +275,9 @@ def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[ return provenances[:1] -def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> InTotoPayload | None: +def find_provenance_from_ci( + analyze_ctx: AnalyzeContext, git_obj: Git | None, download_path: str +) -> InTotoPayload | None: """Try to find provenance from CI services of the repository. Note that we stop going through the CI services once we encounter a CI service @@ -385,9 +289,11 @@ def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> Parameters ---------- analyze_ctx: AnalyzeContext - The contenxt of the ongoing analysis. + The context of the ongoing analysis. git_obj: Git | None The Pydriller Git object representing the repository, if any. + download_path: str + The pre-existing location to download discovered files to. Returns ------- @@ -463,9 +369,7 @@ def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> ci_info["provenance_assets"].extend(provenance_assets) # Download the provenance assets and load the provenance payloads. - download_provenances_from_github_actions_ci_service( - ci_info, - ) + download_provenances_from_ci_service(ci_info, download_path) # TODO consider how to handle multiple payloads here. return ci_info["provenances"][0].payload if ci_info["provenances"] else None @@ -476,56 +380,60 @@ def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> return None -def download_provenances_from_github_actions_ci_service(ci_info: CIInfo) -> None: +def download_provenances_from_ci_service(ci_info: CIInfo, download_path: str) -> None: """Download provenances from GitHub Actions. Parameters ---------- ci_info: CIInfo, A ``CIInfo`` instance that holds a GitHub Actions git service object. + download_path: str + The pre-existing location to download discovered files to. """ ci_service = ci_info["service"] prov_assets = ci_info["provenance_assets"] - + if not os.path.isdir(download_path): + logger.debug("Download location is not a valid directory.") + return try: - with tempfile.TemporaryDirectory() as temp_path: - downloaded_provs = [] - for prov_asset in prov_assets: - # Check the size before downloading. - if prov_asset.size_in_bytes > defaults.getint( - "slsa.verifier", - "max_download_size", - fallback=1000000, - ): - logger.info( - "Skip verifying the provenance %s: asset size too large.", - prov_asset.name, - ) - continue + downloaded_provs = [] + for prov_asset in prov_assets: + # Check the size before downloading. + if prov_asset.size_in_bytes > defaults.getint( + "slsa.verifier", + "max_download_size", + fallback=1000000, + ): + logger.info( + "Skip verifying the provenance %s: asset size too large.", + prov_asset.name, + ) + continue - provenance_filepath = os.path.join(temp_path, prov_asset.name) + provenance_filepath = os.path.join(download_path, prov_asset.name) - if not ci_service.api_client.download_asset( - prov_asset.url, - provenance_filepath, - ): - logger.debug( - "Could not download the provenance %s. Skip verifying...", - prov_asset.name, - ) - continue + if not ci_service.api_client.download_asset( + prov_asset.url, + provenance_filepath, + ): + logger.debug( + "Could not download the provenance %s. Skip verifying...", + prov_asset.name, + ) + continue - # Read the provenance. - try: - payload = load_provenance_payload(provenance_filepath) - except LoadIntotoAttestationError as error: - logger.error("Error logging provenance: %s", error) - continue + # Read the provenance. + try: + payload = load_provenance_payload(provenance_filepath) + except LoadIntotoAttestationError as error: + logger.error("Error logging provenance: %s", error) + continue - # Add the provenance file. - downloaded_provs.append(SLSAProvenanceData(payload=payload, asset=prov_asset)) + # Add the provenance file. + downloaded_provs.append(SLSAProvenanceData(payload=payload, asset=prov_asset)) # Persist the provenance payloads into the CIInfo object. ci_info["provenances"] = downloaded_provs + except OSError as error: logger.error("Error while storing provenance in the temporary directory: %s", error) diff --git a/src/macaron/provenance/provenance_verifier.py b/src/macaron/provenance/provenance_verifier.py new file mode 100644 index 000000000..98cc42fc6 --- /dev/null +++ b/src/macaron/provenance/provenance_verifier.py @@ -0,0 +1,398 @@ +# Copyright (c) 2024 - 2025, 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/. + +"""This module contains methods for verifying provenance files.""" +import glob +import hashlib +import logging +import os +import subprocess # nosec B404 +import tarfile +import zipfile +from functools import partial +from pathlib import Path + +from packageurl import PackageURL + +from macaron.config.defaults import defaults +from macaron.config.global_config import global_config +from macaron.provenance.provenance_extractor import ProvenancePredicate, SLSAGithubGenericBuildDefinitionV01 +from macaron.repo_finder.commit_finder import AbstractPurlType, determine_abstract_purl_type +from macaron.slsa_analyzer.analyze_context import AnalyzeContext +from macaron.slsa_analyzer.asset import AssetLocator +from macaron.slsa_analyzer.ci_service import BaseCIService +from macaron.slsa_analyzer.git_url import get_repo_dir_name +from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV01Payload, v01 +from macaron.slsa_analyzer.specs.ci_spec import CIInfo + +logger: logging.Logger = logging.getLogger(__name__) + + +def verify_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool: + """Verify the passed provenance. + + Parameters + ---------- + purl: PackageURL + The PURL of the analysis target. + provenance: list[InTotoPayload] + The list of provenance. + + Returns + ------- + bool + True if the provenance could be verified, or False otherwise. + """ + if determine_abstract_purl_type(purl) == AbstractPurlType.REPOSITORY: + # Do not perform default verification for repository type targets. + return False + + verification_function = None + + if purl.type == "npm": + verification_function = partial(verify_npm_provenance, purl, provenance) + + # TODO other verification functions go here. + + if verification_function: + return verification_function() + + logger.debug("Provenance verification not supported for PURL type: %s", purl.type) + return False + + +def verify_npm_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool: + """Compare the unsigned payload subject digest with the signed payload digest, if available. + + Parameters + ---------- + purl: PackageURL + The PURL of the analysis target. + provenance: list[InTotoPayload] + The provenances to verify. + + Returns + ------- + bool + True if the provenance was verified, or False otherwise. + """ + if len(provenance) != 2: + logger.debug("Expected unsigned and signed provenance.") + return False + + signed_subjects = provenance[1].statement.get("subject") + if not signed_subjects: + return False + + unsigned_subjects = provenance[0].statement.get("subject") + if not unsigned_subjects: + return False + + found_signed_subject = None + for signed_subject in signed_subjects: + name = signed_subject.get("name") + if not name or not check_purls_equivalent(purl, PackageURL.from_string(name)): + continue + found_signed_subject = signed_subject + break + + if not found_signed_subject: + return False + + found_unsigned_subject = None + for unsigned_subject in unsigned_subjects: + name = unsigned_subject.get("name") + if not name or not check_purls_equivalent(purl, PackageURL.from_string(name)): + continue + found_unsigned_subject = unsigned_subject + break + + if not found_unsigned_subject: + return False + + signed_digest = found_signed_subject.get("digest") + unsigned_digest = found_unsigned_subject.get("digest") + if not (signed_digest and unsigned_digest): + return False + + # For signed and unsigned to match, the digests must be identical. + if signed_digest != unsigned_digest: + return False + + key = list(signed_digest.keys())[0] + logger.debug( + "Verified provenance against signed companion. Signed: %s, Unsigned: %s.", + signed_digest[key][:7], + unsigned_digest[key][:7], + ) + + return True + + +def check_purls_equivalent(original_purl: PackageURL, new_purl: PackageURL) -> bool: + """Check if `new_purl` is equivalent to `original_purl`, excluding versions if the original has none.""" + if ( + original_purl.type != new_purl.type + or original_purl.name != new_purl.name + or original_purl.namespace != new_purl.namespace + ): + return False + if original_purl.version and original_purl.version != new_purl.version: + return False + return True + + +def verify_ci_provenance(analyze_ctx: AnalyzeContext, ci_info: CIInfo, download_path: str) -> bool: + """Try to verify the CI provenance in terms of SLSA level 3 requirements. + + Involves running the SLSA verifier. + + Parameters + ---------- + analyze_ctx: AnalyzeContext + The context of the analysis. + ci_info: CIInfo + A ``CIInfo`` instance that holds a GitHub Actions git service object. + download_path: str + The location to search for downloaded files. + + Returns + ------- + bool + True if the provenance could be verified. + """ + # TODO: During verification, we need to fetch the workflow and verify that it's not + # using self-hosted runners, custom containers or services, etc. + ci_service = ci_info["service"] + for provenance in ci_info["provenances"]: + if not isinstance(provenance.payload, InTotoV01Payload): + logger.debug("Cannot verify provenance type: %s", type(provenance.payload)) + continue + + all_assets = ci_info["release"]["assets"] + + # Iterate through the subjects and verify. + for subject in provenance.payload.statement["subject"]: + sub_asset = _find_subject_asset(subject, all_assets, download_path, ci_service) + + if not sub_asset: + logger.debug("Sub asset not found for: %s.", provenance.payload.statement["subject"]) + return False + if not Path(download_path, sub_asset["name"]).is_file(): + if "size" in sub_asset and sub_asset["size"] > defaults.getint( + "slsa.verifier", "max_download_size", fallback=1000000 + ): + logger.debug("Sub asset too large to verify: %s", sub_asset["name"]) + return False + if "url" in sub_asset and not ci_service.api_client.download_asset( + sub_asset["url"], os.path.join(download_path, sub_asset["name"]) + ): + logger.debug("Sub asset not found: %s", sub_asset["name"]) + return False + + sub_verified = _verify_slsa( + analyze_ctx.macaron_path, + download_path, + provenance.asset, + sub_asset["name"], + analyze_ctx.component.repository.remote_path, + ) + + if not sub_verified: + logger.info("Sub asset not verified: %s", sub_asset["name"]) + return False + + if sub_verified: + logger.info("Successfully verified sub asset: %s", sub_asset["name"]) + + return True + + +def _find_subject_asset( + subject: v01.InTotoV01Subject, + all_assets: list[dict[str, str]], + download_path: str, + ci_service: BaseCIService, +) -> dict | None: + """Find the artifacts that appear in the provenance subject. + + The artifacts can be directly found as a release asset or in an archive file. + """ + sub_asset = next( + (item for item in all_assets if item["name"] == os.path.basename(subject["name"])), + None, + ) + + if sub_asset: + return sub_asset + + extracted_artifact = glob.glob(os.path.join(download_path, "**", os.path.basename(subject["name"])), recursive=True) + for artifact_path in extracted_artifact: + try: + with open(artifact_path, "rb") as file: + if hashlib.sha256(file.read()).hexdigest() == subject["digest"]["sha256"]: + return {"name": str(Path(artifact_path).relative_to(download_path))} + except OSError as error: + logger.error("Error in check: %s", error) + continue + + for item in all_assets: + item_path = os.path.join(download_path, item["name"]) + # Make sure to download an archive just once. + if not Path(item_path).is_file(): + # TODO: check that it's not too large. + if not ci_service.api_client.download_asset(item["url"], item_path): + logger.info("Could not download artifact %s. Skip verifying...", os.path.basename(item_path)) + break + + if _extract_archive(file_path=item_path, temp_path=download_path): + return _find_subject_asset(subject, all_assets, download_path, ci_service) + + return None + + +def _extract_archive(file_path: str, temp_path: str) -> bool: + """Extract the archive file to the temporary path. + + Returns + ------- + bool + Returns True if successful. + """ + + def _validate_path_traversal(path: str) -> bool: + """Check for path traversal attacks.""" + if path.startswith("/") or ".." in path: + logger.debug("Found suspicious path in the archive file: %s.", path) + return False + try: + # Check if there are any symbolic links. + if os.path.realpath(path): + return True + except OSError as error: + logger.debug("Failed to extract artifact from archive file: %s", error) + return False + return False + + try: + if zipfile.is_zipfile(file_path): + with zipfile.ZipFile(file_path, "r") as zip_file: + members = (path for path in zip_file.namelist() if _validate_path_traversal(path)) + zip_file.extractall(temp_path, members=members) # nosec B202:tarfile_unsafe_members + return True + elif tarfile.is_tarfile(file_path): + with tarfile.open(file_path, mode="r:gz") as tar_file: + members_tarinfo = ( + tarinfo for tarinfo in tar_file.getmembers() if _validate_path_traversal(tarinfo.name) + ) + tar_file.extractall(temp_path, members=members_tarinfo) # nosec B202:tarfile_unsafe_members + return True + except (tarfile.TarError, zipfile.BadZipFile, zipfile.LargeZipFile, OSError, ValueError) as error: + logger.info(error) + + return False + + +def _verify_slsa( + macaron_path: str, download_path: str, prov_asset: AssetLocator, asset_name: str, repository_url: str +) -> bool: + """Run SLSA verifier to verify the artifact.""" + source_path = get_repo_dir_name(repository_url, sanitize=False) + if not source_path: + logger.error("Invalid repository source path to verify: %s.", repository_url) + return False + + errors: list[str] = [] + verified = False + cmd = [ + os.path.join(macaron_path, "bin/slsa-verifier"), + "verify-artifact", + os.path.join(download_path, asset_name), + "--provenance-path", + os.path.join(download_path, prov_asset.name), + "--source-uri", + source_path, + ] + + try: + verifier_output = subprocess.run( # nosec B603 + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + cwd=download_path, + timeout=defaults.getint("slsa.verifier", "timeout", fallback=120), + ) + output = verifier_output.stdout.decode("utf-8") + verified = "PASSED: SLSA verification passed" in output + log_path = os.path.join(global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.log") + with open(log_path, mode="a", encoding="utf-8") as log_file: + logger.info("Storing SLSA verifier output for %s to %s", asset_name, log_path) + log_file.writelines( + [f"SLSA verifier output for cmd: {' '.join(cmd)}\n", output, "--------------------------------\n"] + ) + + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: + logger.error(error) + errors.append(error.output.decode("utf-8")) + except OSError as error: + logger.error(error) + errors.append(str(error)) + + if errors: + verified = False + try: + error_log_path = os.path.join( + global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.errors" + ) + with open(error_log_path, mode="a", encoding="utf-8") as log_file: + logger.info("Storing SLSA verifier log for%s to %s", asset_name, error_log_path) + log_file.write(f"SLSA verifier output for cmd: {' '.join(cmd)}\n") + log_file.writelines(errors) + log_file.write("--------------------------------\n") + except OSError as error: + logger.error(error) + + return verified + + +def determine_provenance_slsa_level( + ctx: AnalyzeContext, provenance_payload: InTotoPayload | None, verified: bool, verified_l3: bool +) -> int: + """Implement the check in this method. + + Parameters + ---------- + ctx : AnalyzeContext + The object containing processed data for the target repo. + provenance_payload: dict | None + The provenance payload. + verified: bool + True if the provenance content is verified. + verified_l3: bool + True if the provenance content is level 3 verified. + + Returns + ------- + int + The SLSA level. + """ + if not provenance_payload or ctx.dynamic_data["is_inferred_prov"]: + # 0. Provenance is not available. + return 0 + + predicate = provenance_payload.statement.get("predicate") + build_type = None + if predicate: + build_type = ProvenancePredicate.get_build_type(provenance_payload.statement) + + if build_type in {SLSAGithubGenericBuildDefinitionV01.expected_build_type} and verified_l3: + # 3. Provenance is created by the SLSA GitHub generator and verified. + return 3 + + if verified: + # 2. Provenance is verified. + return 2 + + # 1. Provenance is not verified. + return 1 diff --git a/src/macaron/repo_finder/__init__.py b/src/macaron/repo_finder/__init__.py index dfccaa6a9..6221b357c 100644 --- a/src/macaron/repo_finder/__init__.py +++ b/src/macaron/repo_finder/__init__.py @@ -1,7 +1,7 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. -"""This package contains the dependency resolvers for Java projects.""" +"""This package contains the repository and commit finding tools for software components.""" def to_domain_from_known_purl_types(purl_type: str) -> str | None: diff --git a/src/macaron/slsa_analyzer/analyze_context.py b/src/macaron/slsa_analyzer/analyze_context.py index c2f6a0042..84d8151f2 100644 --- a/src/macaron/slsa_analyzer/analyze_context.py +++ b/src/macaron/slsa_analyzer/analyze_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the Analyze Context class. @@ -11,7 +11,7 @@ from collections import defaultdict from typing import Any, TypedDict -from macaron.database.table_definitions import Component, SLSALevel +from macaron.database.table_definitions import Component, Provenance, SLSALevel from macaron.repo_verifier.repo_verifier import RepositoryVerificationResult from macaron.slsa_analyzer.checks.check_result import CheckResult, CheckResultType from macaron.slsa_analyzer.ci_service.base_ci_service import BaseCIService @@ -19,7 +19,7 @@ from macaron.slsa_analyzer.git_service.base_git_service import NoneGitService from macaron.slsa_analyzer.levels import SLSALevels from macaron.slsa_analyzer.provenance.expectations.expectation import Expectation -from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV01Payload +from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Statement from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1Statement from macaron.slsa_analyzer.slsa_req import ReqName, SLSAReqStatus, create_requirement_status_dict @@ -47,17 +47,11 @@ class ChecksOutputs(TypedDict): """The expectation to verify the provenance for the target software component.""" package_registries: list[PackageRegistryInfo] """The package registries for the target software component.""" - provenance: InTotoPayload | None - """The provenance payload for the target software component.""" - provenance_repo_url: str | None - """The repository URL extracted from provenance, if applicable.""" - provenance_commit_digest: str | None - """The commit digest extracted from provenance, if applicable.""" - provenance_verified: bool - """True if the provenance exists and has been verified against a signed companion provenance.""" + provenance_info: Provenance | None + """The provenance and related information.""" local_artifact_paths: list[str] """The local artifact absolute paths.""" - validate_malware_switch: bool + validate_malware: bool """True when the malware validation is enabled.""" @@ -110,12 +104,9 @@ def __init__( package_registries=[], is_inferred_prov=True, expectation=None, - provenance=None, - provenance_repo_url=None, - provenance_commit_digest=None, - provenance_verified=False, + provenance_info=None, local_artifact_paths=[], - validate_malware_switch=False, + validate_malware=False, ) @property diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index fb3adb33b..c1587a558 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -8,6 +8,7 @@ import os import re import sys +import tempfile from collections.abc import Mapping from datetime import datetime, timezone from pathlib import Path @@ -20,11 +21,17 @@ from macaron import __version__ from macaron.artifact.local_artifact import get_local_artifact_dirs -from macaron.config.defaults import defaults from macaron.config.global_config import global_config from macaron.config.target_config import Configuration from macaron.database.database_manager import DatabaseManager, get_db_manager, get_db_session -from macaron.database.table_definitions import Analysis, Component, ProvenanceSubject, RepoFinderMetadata, Repository +from macaron.database.table_definitions import ( + Analysis, + Component, + Provenance, + ProvenanceSubject, + RepoFinderMetadata, + Repository, +) from macaron.dependency_analyzer.cyclonedx import DependencyAnalyzer, DependencyInfo from macaron.errors import ( DuplicateError, @@ -36,13 +43,16 @@ ) from macaron.output_reporter.reporter import FileReporter from macaron.output_reporter.results import Record, Report, SCMStatus -from macaron.repo_finder import repo_finder -from macaron.repo_finder.provenance_extractor import ( +from macaron.provenance import provenance_verifier +from macaron.provenance.provenance_extractor import ( check_if_input_purl_provenance_conflict, check_if_input_repo_provenance_conflict, + extract_predicate_version, extract_repo_and_commit_from_provenance, ) -from macaron.repo_finder.provenance_finder import ProvenanceFinder, find_provenance_from_ci +from macaron.provenance.provenance_finder import ProvenanceFinder, find_provenance_from_ci +from macaron.provenance.provenance_verifier import determine_provenance_slsa_level, verify_ci_provenance +from macaron.repo_finder import repo_finder from macaron.repo_finder.repo_finder import prepare_repo from macaron.repo_finder.repo_finder_enums import CommitFinderInfo, RepoFinderInfo from macaron.repo_finder.repo_utils import get_git_service @@ -65,7 +75,7 @@ from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.registry import registry from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo logger: logging.Logger = logging.getLogger(__name__) @@ -126,7 +136,8 @@ def run( sbom_path: str = "", deps_depth: int = 0, provenance_payload: InTotoPayload | None = None, - validate_malware_switch: bool = False, + validate_malware: bool = False, + verify_provenance: bool = False, ) -> int: """Run the analysis and write results to the output path. @@ -143,6 +154,10 @@ def run( The depth of dependency resolution. Default: 0. provenance_payload : InToToPayload | None The provenance intoto payload for the main software component. + validate_malware: bool + Enable malware validation if True. + verify_provenance: bool + Enable provenance verification if True. Returns ------- @@ -175,7 +190,8 @@ def run( main_config, analysis, provenance_payload=provenance_payload, - validate_malware_switch=validate_malware_switch, + validate_malware=validate_malware, + verify_provenance=verify_provenance, ) if main_record.status != SCMStatus.AVAILABLE or not main_record.context: @@ -293,7 +309,8 @@ def run_single( analysis: Analysis, existing_records: dict[str, Record] | None = None, provenance_payload: InTotoPayload | None = None, - validate_malware_switch: bool = False, + validate_malware: bool = False, + verify_provenance: bool = False, ) -> Record: """Run the checks for a single repository target. @@ -310,6 +327,10 @@ def run_single( The mapping of existing records that the analysis has run successfully. provenance_payload : InToToPayload | None The provenance intoto payload for the analyzed software component. + validate_malware: bool + Enable malware validation if True. + verify_provenance: bool + Enable provenance verification if True. Returns ------- @@ -339,12 +360,11 @@ def run_single( provenances = provenance_finder.find_provenance(parsed_purl) if provenances: provenance_payload = provenances[0] - if defaults.getboolean("analyzer", "verify_provenance"): - provenance_is_verified = provenance_finder.verify_provenance(parsed_purl, provenances) + if verify_provenance: + provenance_is_verified = provenance_verifier.verify_provenance(parsed_purl, provenances) # Try to extract the repository URL and commit digest from the Provenance, if it exists. repo_path_input: str | None = config.get_value("path") - digest_input: str | None = config.get_value("digest") provenance_repo_url = provenance_commit_digest = None if provenance_payload: try: @@ -352,10 +372,8 @@ def run_single( provenance_payload ) except ProvenanceError as error: - logger.debug("Failed to extract repo or commit from provenance: %s", error) - - # Try to validate the input repo against provenance contents. - if provenance_repo_url and check_if_input_repo_provenance_conflict(repo_path_input, provenance_repo_url): + logger.debug("Failed to extract from provenance: %s", error) + if check_if_input_repo_provenance_conflict(repo_path_input, provenance_repo_url): return Record( record_id=repo_id, description="Input mismatch between repo and provenance.", @@ -400,18 +418,15 @@ def run_single( ) # Check if only one of the repo or digest came from direct input. - if git_obj and (provenance_repo_url or provenance_commit_digest) and parsed_purl: + if parsed_purl: if check_if_input_purl_provenance_conflict( - git_obj, bool(repo_path_input), - bool(digest_input), provenance_repo_url, - provenance_commit_digest, parsed_purl, ): return Record( record_id=repo_id, - description="Input mismatch between repo/commit (purl) and provenance.", + description="Input mismatch between repo (purl) and provenance.", pre_config=config, status=SCMStatus.ANALYSIS_FAILED, ) @@ -461,42 +476,62 @@ def run_single( self._verify_repository_link(parsed_purl, analyze_ctx) self._determine_package_registries(analyze_ctx) + provenance_l3_verified = False if not provenance_payload: # Look for provenance using the CI. - provenance_payload = find_provenance_from_ci(analyze_ctx, git_obj) - # If found, verify analysis target against new provenance - if provenance_payload: - # If repository URL was not provided as input, check the one found during analysis. - if not repo_path_input and component.repository: - repo_path_input = component.repository.remote_path - - # Extract the digest and repository URL from provenance. - provenance_repo_url = provenance_commit_digest = None - try: - provenance_repo_url, provenance_commit_digest = extract_repo_and_commit_from_provenance( - provenance_payload - ) - except ProvenanceError as error: - logger.debug("Failed to extract repo or commit from provenance: %s", error) - - # Try to validate the input repo against provenance contents. - if provenance_repo_url and check_if_input_repo_provenance_conflict( - repo_path_input, provenance_repo_url - ): - return Record( - record_id=repo_id, - description="Input mismatch between repo/commit and provenance.", - pre_config=config, - status=SCMStatus.ANALYSIS_FAILED, - ) + with tempfile.TemporaryDirectory() as temp_dir: + provenance_payload = find_provenance_from_ci(analyze_ctx, git_obj, temp_dir) + # If found, validate analysis target against new provenance. + if provenance_payload: + # If repository URL was not provided as input, check the one found during analysis. + if not repo_path_input and component.repository: + repo_path_input = component.repository.remote_path + provenance_repo_url = provenance_commit_digest = None + try: + provenance_repo_url, provenance_commit_digest = extract_repo_and_commit_from_provenance( + provenance_payload + ) + except ProvenanceError as error: + logger.debug("Failed to extract from provenance: %s", error) + + if check_if_input_repo_provenance_conflict(repo_path_input, provenance_repo_url): + return Record( + record_id=repo_id, + description="Input mismatch between repo/commit and provenance.", + pre_config=config, + status=SCMStatus.ANALYSIS_FAILED, + ) + + # Also try to verify CI provenance contents. + if verify_provenance: + verified = [] + for ci_info in analyze_ctx.dynamic_data["ci_services"]: + verified.append(verify_ci_provenance(analyze_ctx, ci_info, temp_dir)) + if not verified: + break + if verified and all(verified): + provenance_l3_verified = True - analyze_ctx.dynamic_data["provenance"] = provenance_payload if provenance_payload: analyze_ctx.dynamic_data["is_inferred_prov"] = False - analyze_ctx.dynamic_data["provenance_verified"] = provenance_is_verified - analyze_ctx.dynamic_data["provenance_repo_url"] = provenance_repo_url - analyze_ctx.dynamic_data["provenance_commit_digest"] = provenance_commit_digest - analyze_ctx.dynamic_data["validate_malware_switch"] = validate_malware_switch + slsa_version = extract_predicate_version(provenance_payload) + + slsa_level = determine_provenance_slsa_level( + analyze_ctx, provenance_payload, provenance_is_verified, provenance_l3_verified + ) + + analyze_ctx.dynamic_data["provenance_info"] = Provenance( + component=component, + repository_url=provenance_repo_url, + commit_sha=provenance_commit_digest, + verified=provenance_is_verified, + provenance_payload=provenance_payload, + slsa_level=slsa_level, + slsa_version=slsa_version, + # TODO Add release tag, release digest. + ) + + analyze_ctx.dynamic_data["validate_malware"] = validate_malware if parsed_purl and parsed_purl.type in self.local_artifact_repo_mapper: local_artifact_repo_path = self.local_artifact_repo_mapper[parsed_purl.type] @@ -971,11 +1006,11 @@ def _determine_ci_services(self, analyze_ctx: AnalyzeContext, git_service: BaseG release={}, provenances=[ SLSAProvenanceData( - payload=InTotoV01Payload(statement=Provenance().payload), + payload=InTotoV01Payload(statement=InferredProvenance().payload), asset=VirtualReleaseAsset(name="No_ASSET", url="NO_URL", size_in_bytes=0), ) ], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ) diff --git a/src/macaron/slsa_analyzer/checks/build_as_code_check.py b/src/macaron/slsa_analyzer/checks/build_as_code_check.py index df00ef2b3..494adf345 100644 --- a/src/macaron/slsa_analyzer/checks/build_as_code_check.py +++ b/src/macaron/slsa_analyzer/checks/build_as_code_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the BuildAsCodeCheck class.""" @@ -15,7 +15,7 @@ from macaron.errors import CallGraphError, ProvenanceError from macaron.parsers.bashparser import BashNode from macaron.parsers.github_workflow_model import ActionStep -from macaron.repo_finder.provenance_extractor import ProvenancePredicate +from macaron.provenance.provenance_extractor import ProvenancePredicate from macaron.slsa_analyzer.analyze_context import AnalyzeContext, store_inferred_build_info_results from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType @@ -124,7 +124,9 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # If a provenance is found, obtain the workflow that has triggered the artifact release. prov_workflow = None - prov_payload = ctx.dynamic_data["provenance"] + prov_payload = None + if ctx.dynamic_data["provenance_info"]: + prov_payload = ctx.dynamic_data["provenance_info"].provenance_payload if not ctx.dynamic_data["is_inferred_prov"] and prov_payload: try: build_def = ProvenancePredicate.find_build_def(prov_payload.statement) diff --git a/src/macaron/slsa_analyzer/checks/build_script_check.py b/src/macaron/slsa_analyzer/checks/build_script_check.py index 14d02675e..44f675ce4 100644 --- a/src/macaron/slsa_analyzer/checks/build_script_check.py +++ b/src/macaron/slsa_analyzer/checks/build_script_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the BuildScriptCheck class.""" @@ -27,6 +27,10 @@ class BuildScriptFacts(CheckFacts): __tablename__ = "_build_script_check" + # This check is disabled here due to a bug in pylint. The Mapped class triggers a false positive. + # It may arbitrarily become true that this is no longer needed in this check, or will be needed in another check. + # pylint: disable=unsubscriptable-object + #: The primary key. id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index 040daca85..5cb058ca0 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -424,7 +424,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: if confidence is None: confidence = Confidence.HIGH result_type = CheckResultType.PASSED - elif ctx.dynamic_data["validate_malware_switch"]: + elif ctx.dynamic_data["validate_malware"]: is_malware, validation_result = self.validate_malware(pypi_package_json) if is_malware: # Find source code block matched the malicious pattern confidence = Confidence.HIGH diff --git a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py index 8902d6ef2..752494900 100644 --- a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py +++ b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. """This module contains the InferArtifactPipelineCheck class to check if an artifact is published from a pipeline automatically.""" @@ -14,7 +14,7 @@ from macaron.database.table_definitions import CheckFacts from macaron.errors import GitHubActionsValueError, InvalidHTTPResponseError, ProvenanceError from macaron.json_tools import json_extract -from macaron.repo_finder.provenance_extractor import ProvenancePredicate +from macaron.provenance.provenance_extractor import ProvenancePredicate 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 @@ -155,7 +155,9 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # If a provenance is found, obtain the workflow and the pipeline that has triggered the artifact release. prov_workflow = None prov_trigger_run = None - prov_payload = ctx.dynamic_data["provenance"] + prov_payload = None + if ctx.dynamic_data["provenance_info"]: + prov_payload = ctx.dynamic_data["provenance_info"].provenance_payload if not ctx.dynamic_data["is_inferred_prov"] and prov_payload: # Obtain the build-related fields from the provenance. try: diff --git a/src/macaron/slsa_analyzer/checks/provenance_available_check.py b/src/macaron/slsa_analyzer/checks/provenance_available_check.py index b67e5940d..77fcf87fe 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_available_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_available_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the implementation of the Provenance Available check.""" @@ -74,7 +74,11 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - available = ctx.dynamic_data["provenance"] and not ctx.dynamic_data["is_inferred_prov"] + available = ( + ctx.dynamic_data["provenance_info"] + and ctx.dynamic_data["provenance_info"].provenance_payload + and not ctx.dynamic_data["is_inferred_prov"] + ) return CheckResultData( result_tables=[ ProvenanceAvailableFacts( diff --git a/src/macaron/slsa_analyzer/checks/provenance_commit_check.py b/src/macaron/slsa_analyzer/checks/provenance_commit_check.py index c7420cc34..61fb6b8a3 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_commit_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_commit_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module adds a check that determines whether the repository URL came from provenance.""" @@ -22,6 +22,10 @@ class ProvenanceDerivedCommitFacts(CheckFacts): __tablename__ = "_provenance_derived_commit_check" + # This check is disabled here due to a bug in pylint. The Mapped class triggers a false positive. + # It may arbitrarily become true that this is no longer needed in this check, or will be needed in another check. + # pylint: disable=unsubscriptable-object + #: The primary key. id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 @@ -63,7 +67,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - if ctx.dynamic_data["provenance_commit_digest"]: + if ctx.dynamic_data["provenance_info"] and ctx.dynamic_data["provenance_info"].commit_sha: if not ctx.component.repository: return CheckResultData( result_tables=[], @@ -72,7 +76,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: current_commit = ctx.component.repository.commit_sha - if current_commit == ctx.dynamic_data["provenance_commit_digest"]: + if current_commit == ctx.dynamic_data["provenance_info"].commit_sha: return CheckResultData( result_tables=[ ProvenanceDerivedCommitFacts( diff --git a/src/macaron/slsa_analyzer/checks/provenance_l3_check.py b/src/macaron/slsa_analyzer/checks/provenance_l3_check.py deleted file mode 100644 index e34a4ce0b..000000000 --- a/src/macaron/slsa_analyzer/checks/provenance_l3_check.py +++ /dev/null @@ -1,460 +0,0 @@ -# Copyright (c) 2022 - 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/. - -"""This module implements a check to verify a target repo has intoto provenance level 3.""" - -import glob -import hashlib -import json -import logging -import os -import subprocess # nosec B404 -import tarfile -import tempfile -import zipfile -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from typing import NamedTuple - -from sqlalchemy import ForeignKey -from sqlalchemy.orm import Mapped, mapped_column - -from macaron.config.defaults import defaults -from macaron.config.global_config import global_config -from macaron.database.table_definitions import CheckFacts, HashDigest, Provenance, ReleaseArtifact -from macaron.slsa_analyzer.analyze_context import AnalyzeContext -from macaron.slsa_analyzer.asset import AssetLocator -from macaron.slsa_analyzer.checks.base_check import BaseCheck -from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence -from macaron.slsa_analyzer.ci_service.base_ci_service import BaseCIService, NoneCIService -from macaron.slsa_analyzer.git_url import get_repo_dir_name -from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload, v01 -from macaron.slsa_analyzer.provenance.intoto.errors import InTotoAttestationError, UnsupportedInTotoVersionError -from macaron.slsa_analyzer.provenance.loader import load_provenance_payload -from macaron.slsa_analyzer.registry import registry -from macaron.slsa_analyzer.slsa_req import ReqName - -logger: logging.Logger = logging.getLogger(__name__) - - -class ProvenanceL3VerifiedFacts(CheckFacts): - """The ORM mapping for justifications in provenance_l3 check.""" - - __tablename__ = "_provenance_l3_check" - - # The primary key. - id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 - - __mapper_args__ = { - "polymorphic_identity": "_provenance_l3_check", - } - - -class _VerifyArtifactResultType(Enum): - """Result of attempting to verify an asset.""" - - # slsa-verifier succeeded and the artifact passed verification - PASSED = "verify passed" - # slsa-verifier succeeded and the artifact failed verification - FAILED = "verify failed" - # An error occurred running slsa-verifier or downloading the artifact - ERROR = "verify error" - # The artifact was unable to be downloaded because the url was missing or malformed - NO_DOWNLOAD = "unable to download asset" - # The artifact was unable to be downloaded because the file was too large - TOO_LARGE = "asset file too large to download" - - def is_skip(self) -> bool: - """Return whether the verification was skipped.""" - return self in (_VerifyArtifactResultType.NO_DOWNLOAD, _VerifyArtifactResultType.TOO_LARGE) - - def is_fail(self) -> bool: - """Return whether the verification failed.""" - return self in (_VerifyArtifactResultType.FAILED, _VerifyArtifactResultType.ERROR) - - -@dataclass -class _VerifyArtifactResult: - """Dataclass storing the result of verifying a single asset.""" - - result: _VerifyArtifactResultType - artifact_name: str - - def __str__(self) -> str: - return f"{str(self.result.value)} : {self.artifact_name}" - - -class ProvenanceL3Check(BaseCheck): - """This Check checks whether the target repo has SLSA provenance level 3.""" - - def __init__(self) -> None: - """Initialize instance.""" - check_id = "mcn_provenance_level_three_1" - description = "Check whether the target has SLSA provenance level 3." - depends_on: list[tuple[str, CheckResultType]] = [("mcn_provenance_available_1", CheckResultType.PASSED)] - - # SLSA 3: only identifies the top-level build config and not all the build inputs (hermetic). - # TODO: revisit if ReqName.PROV_CONT_SOURCE should be here or not. That's because the definition - # of source is not clear. See https://github.com/slsa-framework/slsa/issues/465. - eval_reqs = [ - ReqName.PROV_NON_FALSIFIABLE, - ReqName.PROV_CONT_BUILD_PARAMS, - ReqName.PROV_CONT_ENTRY, - ReqName.PROV_CONT_SOURCE, - ] - super().__init__( - check_id=check_id, - description=description, - depends_on=depends_on, - eval_reqs=eval_reqs, - result_on_skip=CheckResultType.FAILED, - ) - - def _size_large(self, asset_size: int) -> bool: - """Check the size of the asset.""" - return asset_size > defaults.getint("slsa.verifier", "max_download_size", fallback=1000000) - - def _verify_slsa( - self, macaron_path: str, temp_path: str, prov_asset: AssetLocator, asset_name: str, repository_url: str - ) -> _VerifyArtifactResult: - """Run SLSA verifier to verify the artifact.""" - source_path = get_repo_dir_name(repository_url, sanitize=False) - if not source_path: - logger.error("Invalid repository source path to verify: %s.", repository_url) - return _VerifyArtifactResult(_VerifyArtifactResultType.NO_DOWNLOAD, asset_name) - - errors: list[str] = [] - result: _VerifyArtifactResult - cmd = [ - os.path.join(macaron_path, "bin/slsa-verifier"), - "verify-artifact", - os.path.join(temp_path, asset_name), - "--provenance-path", - os.path.join(temp_path, prov_asset.name), - "--source-uri", - source_path, - ] - - try: - verifier_output = subprocess.run( # nosec B603 - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=True, - cwd=temp_path, - timeout=defaults.getint("slsa.verifier", "timeout", fallback=120), - ) - - output = verifier_output.stdout.decode("utf-8") - if "PASSED: SLSA verification passed" in output: - result = _VerifyArtifactResult(_VerifyArtifactResultType.PASSED, asset_name) - else: - result = _VerifyArtifactResult(_VerifyArtifactResultType.FAILED, asset_name) - - log_path = os.path.join(global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.log") - with open(log_path, mode="a", encoding="utf-8") as log_file: - logger.info("Storing SLSA verifier output for %s to %s", asset_name, log_path) - log_file.writelines( - [f"SLSA verifier output for cmd: {' '.join(cmd)}\n", output, "--------------------------------\n"] - ) - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: - logger.error(error) - errors.append(error.output.decode("utf-8")) - except OSError as error: - logger.error(error) - errors.append(str(error)) - - if errors: - result = _VerifyArtifactResult(result=_VerifyArtifactResultType.ERROR, artifact_name=asset_name) - try: - error_log_path = os.path.join( - global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.errors" - ) - with open(error_log_path, mode="a", encoding="utf-8") as log_file: - logger.info("Storing SLSA verifier log for%s to %s", asset_name, error_log_path) - log_file.write(f"SLSA verifier output for cmd: {' '.join(cmd)}\n") - log_file.writelines(errors) - log_file.write("--------------------------------\n") - except OSError as error: - logger.error(error) - - return result - - def _extract_archive(self, file_path: str, temp_path: str) -> bool: - """Extract the archive file to the temporary path. - - Returns - ------- - bool - Returns True if successful. - """ - - def _validate_path_traversal(path: str) -> bool: - """Check for path traversal attacks.""" - if path.startswith("/") or ".." in path: - logger.debug("Found suspicious path in the archive file: %s.", path) - return False - try: - # Check if there are any symbolic links. - if os.path.realpath(path): - return True - except OSError as error: - logger.debug("Failed to extract artifact from archive file: %s", error) - return False - return False - - try: - if zipfile.is_zipfile(file_path): - with zipfile.ZipFile(file_path, "r") as zip_file: - members = (path for path in zip_file.namelist() if _validate_path_traversal(path)) - zip_file.extractall(temp_path, members=members) # nosec B202:tarfile_unsafe_members - return True - elif tarfile.is_tarfile(file_path): - with tarfile.open(file_path, mode="r:gz") as tar_file: - members_tarinfo = ( - tarinfo for tarinfo in tar_file.getmembers() if _validate_path_traversal(tarinfo.name) - ) - tar_file.extractall(temp_path, members=members_tarinfo) # nosec B202:tarfile_unsafe_members - return True - except (tarfile.TarError, zipfile.BadZipFile, zipfile.LargeZipFile, OSError, ValueError) as error: - logger.info(error) - - return False - - def _find_asset( - self, - subject: v01.InTotoV01Subject, - all_assets: list[dict[str, str]], - temp_path: str, - ci_service: BaseCIService, - ) -> dict | None: - """Find the artifacts that appear in the provenance subject. - - The artifacts can be directly found as a release asset or in an archive file. - """ - sub_asset = next( - (item for item in all_assets if item["name"] == os.path.basename(subject["name"])), - None, - ) - - if sub_asset: - return sub_asset - - extracted_artifact = glob.glob(os.path.join(temp_path, "**", os.path.basename(subject["name"])), recursive=True) - for artifact_path in extracted_artifact: - try: - with open(artifact_path, "rb") as file: - if hashlib.sha256(file.read()).hexdigest() == subject["digest"]["sha256"]: - return {"name": str(Path(artifact_path).relative_to(temp_path))} - except OSError as error: - logger.error("Error in check %s: %s", self.check_info.check_id, error) - continue - - for item in all_assets: - item_path = os.path.join(temp_path, item["name"]) - # Make sure to download an archive just once. - if not Path(item_path).is_file(): - # TODO: check that it's not too large. - if not ci_service.api_client.download_asset(item["url"], item_path): - logger.info("Could not download artifact %s. Skip verifying...", os.path.basename(item_path)) - break - - if self._extract_archive(file_path=item_path, temp_path=temp_path): - return self._find_asset(subject, all_assets, temp_path, ci_service) - - return None - - def run_check(self, ctx: AnalyzeContext) -> CheckResultData: - """Implement the check in this method. - - Parameters - ---------- - ctx : AnalyzeContext - The object containing processed data for the target repo. - - Returns - ------- - CheckResultData - The result of the check. - """ - # TODO: During verification, we need to fetch the workflow and verify that it's not - # using self-hosted runners, custom containers or services, etc. - - class Feedback(NamedTuple): - """Store feedback item.""" - - #: The CI service name. - ci_service_name: str - - #: The provenance asset url. - prov_asset_url: str - - #: The verification result. - verify_result: _VerifyArtifactResult - - all_feedback: list[Feedback] = [] - ci_services = ctx.dynamic_data["ci_services"] - - result_tables: list[CheckFacts] = [] - - for ci_info in ci_services: - ci_service = ci_info["service"] - - # Checking if a CI service is discovered for this repo. - if isinstance(ci_service, NoneCIService): - continue - - # Checking if we have found a release for the repo. - if not ci_info["release"] or "assets" not in ci_info["release"]: - logger.info("Could not find any release assets for the repository.") - break - - # Checking if we have found a SLSA provenance for the repo. - if not ci_info["provenance_assets"]: - logger.info("Could not find SLSA provenances.") - break - - prov_assets = ci_info["provenance_assets"] - all_assets = ci_info["release"]["assets"] - - # Download and verify the artifacts if they are not large. - # Create a temporary directory and automatically remove it when we are done. - try: - with tempfile.TemporaryDirectory() as temp_path: - downloaded_provs = [] - for prov_asset in prov_assets: - # Check the size before downloading. - if self._size_large(prov_asset.size_in_bytes): - logger.info("Skip verifying the provenance %s: asset size too large.", prov_asset.name) - continue - - if not ci_service.api_client.download_asset( - prov_asset.url, os.path.join(temp_path, prov_asset.name) - ): - logger.info("Could not download the provenance %s. Skip verifying...", prov_asset.name) - continue - - # Read the provenance. - provenance_payload = load_provenance_payload( - os.path.join(temp_path, prov_asset.name), - ) - - if not isinstance(provenance_payload, InTotoV01Payload): - raise UnsupportedInTotoVersionError( - f"The provenance asset '{prov_asset.name}' is under an unsupported in-toto version." - ) - - # Add the provenance file. - downloaded_provs.append(provenance_payload.statement) - - # Output provenance - prov = Provenance() - # TODO: fix commit reference for provenance when release/artifact as an analysis entrypoint is - # implemented ensure the provenance commit matches the actual release analyzed - prov.version = "0.2" - prov.release_commit_sha = "" - prov.provenance_json = json.dumps(provenance_payload.statement) - prov.release_tag = ci_info["release"]["tag_name"] - prov.component = ctx.component - - # Iterate through the subjects and verify. - for subject in provenance_payload.statement["subject"]: - sub_asset = self._find_asset(subject, all_assets, temp_path, ci_service) - - result: None | _VerifyArtifactResult = None - for _ in range(1): - if not sub_asset: - result = _VerifyArtifactResult( - result=_VerifyArtifactResultType.NO_DOWNLOAD, artifact_name=subject["name"] - ) - break - if not Path(temp_path, sub_asset["name"]).is_file(): - if "size" in sub_asset and self._size_large(sub_asset["size"]): - result = _VerifyArtifactResult( - result=_VerifyArtifactResultType.TOO_LARGE, - artifact_name=sub_asset["name"], - ) - break - if "url" in sub_asset and not ci_service.api_client.download_asset( - sub_asset["url"], os.path.join(temp_path, sub_asset["name"]) - ): - result = _VerifyArtifactResult( - result=_VerifyArtifactResultType.NO_DOWNLOAD, - artifact_name=sub_asset["name"], - ) - break - - result = self._verify_slsa( - ctx.macaron_path, - temp_path, - prov_asset, - sub_asset["name"], - ctx.component.repository.remote_path, - ) - - if result: - if result.result.is_skip(): - logger.info("Skipped verifying artifact: %s", result.result) - if result.result.is_fail(): - logger.info("Error verifying artifact: %s", result.result) - if result.result == _VerifyArtifactResultType.FAILED: - logger.info("Failed verifying artifact: %s", result.result) - if result.result == _VerifyArtifactResultType.PASSED: - logger.info("Successfully verified artifact: %s", result.result) - - all_feedback.append( - Feedback( - ci_service_name=ci_service.name, - prov_asset_url=prov_asset.url, - verify_result=result, - ) - ) - - # Store artifact information result to database. - artifact = ReleaseArtifact() - artifact.name = subject["name"] - artifact.slsa_verified = result.result == _VerifyArtifactResultType.PASSED - artifact.provenance = prov # pylint: disable=protected-access - - for k, val in subject["digest"].items(): - digest = HashDigest() - digest.digest_algorithm = k - digest.digest = val - # Foreign key relation. - digest.artifact = artifact - - except (OSError, InTotoAttestationError) as error: - logger.error(" %s: %s.", self.check_info.check_id, error) - return CheckResultData( - result_tables=result_tables, - result_type=CheckResultType.FAILED, - ) - - result_value = CheckResultType.FAILED - if all_feedback: - failed = [ - feedback - for feedback in all_feedback - if feedback.verify_result.result == _VerifyArtifactResultType.FAILED - ] - - skipped = [ - feedback - for feedback in all_feedback - if feedback.verify_result.result - not in [_VerifyArtifactResultType.FAILED, _VerifyArtifactResultType.PASSED] - ] - - if failed or skipped: - result_value = CheckResultType.FAILED - else: - result_tables.append(ProvenanceL3VerifiedFacts(confidence=Confidence.HIGH)) - result_value = CheckResultType.PASSED - return CheckResultData(result_tables=result_tables, result_type=result_value) - - return CheckResultData(result_tables=result_tables, result_type=result_value) - - -registry.register(ProvenanceL3Check()) diff --git a/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py b/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py index 16f621a5a..b7bc93c23 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. """This module checks if a SLSA provenance conforms to a given expectation.""" @@ -58,8 +58,8 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: logger.info("%s check was unable to find any expectations.", self.check_info.check_id) return CheckResultData(result_tables=[], result_type=CheckResultType.UNKNOWN) - if ctx.dynamic_data["provenance"]: - if expectation.validate(ctx.dynamic_data["provenance"]): + if ctx.dynamic_data["provenance_info"] and ctx.dynamic_data["provenance_info"].provenance_payload: + if expectation.validate(ctx.dynamic_data["provenance_info"].provenance_payload): return CheckResultData( result_tables=[expectation], result_type=CheckResultType.PASSED, diff --git a/src/macaron/slsa_analyzer/checks/provenance_repo_check.py b/src/macaron/slsa_analyzer/checks/provenance_repo_check.py index 5770aadae..063e68c2b 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_repo_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_repo_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module adds a check that determines whether the repository URL came from provenance.""" @@ -63,7 +63,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - if ctx.dynamic_data["provenance_repo_url"]: + if ctx.dynamic_data["provenance_info"] and ctx.dynamic_data["provenance_info"].repository_url: if not ctx.component.repository: return CheckResultData( result_tables=[], @@ -72,7 +72,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: current_repository = ctx.component.repository.remote_path - if current_repository == ctx.dynamic_data["provenance_repo_url"]: + if current_repository == ctx.dynamic_data["provenance_info"].repository_url: return CheckResultData( result_tables=[ ProvenanceDerivedRepoFacts( diff --git a/src/macaron/slsa_analyzer/checks/provenance_verified_check.py b/src/macaron/slsa_analyzer/checks/provenance_verified_check.py index 4bcbc3a4c..65f028ec0 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_verified_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_verified_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module adds a Check that checks whether the provenance is verified.""" @@ -8,7 +8,7 @@ from sqlalchemy.orm import Mapped, mapped_column from macaron.database.table_definitions import CheckFacts -from macaron.json_tools import json_extract +from macaron.provenance.provenance_extractor import ProvenancePredicate 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 @@ -67,49 +67,25 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - if ctx.dynamic_data["is_inferred_prov"] or not ctx.dynamic_data["provenance"]: - # Provenance is not available. - return CheckResultData( - result_tables=[ProvenanceVerifiedFacts(build_level=0, confidence=Confidence.HIGH)], - result_type=CheckResultType.FAILED, - ) - - predicate = ctx.dynamic_data["provenance"].statement.get("predicate") build_type = None - if predicate: - build_type = json_extract(predicate, ["buildType"], str) - - if not ctx.dynamic_data["provenance_verified"]: - # Provenance is not verified. - return CheckResultData( - result_tables=[ - ProvenanceVerifiedFacts( - build_level=1, - build_type=build_type, - confidence=Confidence.HIGH, - ) - ], - result_type=CheckResultType.FAILED, - ) - - if build_type != "https://github.com/slsa-framework/slsa-github-generator/generic@v1": - # Provenance is verified but the build service does not isolate generation in the control plane from the - # untrusted build process. - return CheckResultData( - result_tables=[ - ProvenanceVerifiedFacts( - build_level=2, - build_type=build_type, - confidence=Confidence.HIGH, - ) - ], - result_type=CheckResultType.PASSED, - ) - - # Provenance is created by the SLSA GitHub generator and verified. + provenance_info = ctx.dynamic_data["provenance_info"] + + if provenance_info and provenance_info.provenance_payload: + build_type = ProvenancePredicate.get_build_type(provenance_info.provenance_payload.statement) + + slsa_level = 0 + if provenance_info: + slsa_level = provenance_info.slsa_level + return CheckResultData( - result_tables=[ProvenanceVerifiedFacts(build_level=3, build_type=build_type, confidence=Confidence.HIGH)], - result_type=CheckResultType.PASSED, + result_tables=[ + ProvenanceVerifiedFacts( + build_level=slsa_level, + build_type=build_type, + confidence=Confidence.HIGH, + ) + ], + result_type=CheckResultType.FAILED if slsa_level < 2 else CheckResultType.PASSED, ) diff --git a/src/macaron/slsa_analyzer/specs/inferred_provenance.py b/src/macaron/slsa_analyzer/specs/inferred_provenance.py index 6d5bba573..302083768 100644 --- a/src/macaron/slsa_analyzer/specs/inferred_provenance.py +++ b/src/macaron/slsa_analyzer/specs/inferred_provenance.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the inferred SLSA provenance spec.""" @@ -7,7 +7,7 @@ from macaron.slsa_analyzer.provenance.intoto import v01 -class Provenance: +class InferredProvenance: """This class implements the inferred SLSA provenance. This inferred provenance implementation follows the SLSA v0.2 provenance schema. diff --git a/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl b/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl index 2c750872a..a51934e8d 100644 --- a/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl +++ b/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl index fdf03032b..578270e2b 100644 --- a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl +++ b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/google/guava"). diff --git a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl +++ b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl index 92e0e16c8..a7b486216 100644 --- a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl +++ b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/mockito/mockito"). diff --git a/tests/integration/cases/apache_maven_local_repo/policy.dl b/tests/integration/cases/apache_maven_local_repo/policy.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_local_repo/policy.dl +++ b/tests/integration/cases/apache_maven_local_repo/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_purl_repo_path/policy.dl b/tests/integration/cases/apache_maven_purl_repo_path/policy.dl index 92d3b8d7b..6a8199df0 100644 --- a/tests/integration/cases/apache_maven_purl_repo_path/policy.dl +++ b/tests/integration/cases/apache_maven_purl_repo_path/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl b/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl +++ b/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl b/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl index fdf03032b..578270e2b 100644 --- a/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl +++ b/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/google/guava"). diff --git a/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl b/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl +++ b/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl b/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl index 92e0e16c8..a7b486216 100644 --- a/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl +++ b/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/mockito/mockito"). diff --git a/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl b/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl index e5dba0031..cccbb3bc9 100644 --- a/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl +++ b/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -10,8 +10,7 @@ .decl violating_dependencies(parent: number) violating_dependencies(parent) :- transitive_dependency(parent, dependency), - !check_passed(dependency, "mcn_find_artifact_pipeline_1"), - !check_passed(dependency, "mcn_provenance_level_three_1"). + !check_passed(dependency, "mcn_find_artifact_pipeline_1"). apply_policy_to("detect-malicious-upload", component_id) :- is_repo(_, "github.com/behnazh-w/example-maven-app", component_id). diff --git a/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl b/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl index 75706c7e6..1efa084e6 100644 --- a/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl +++ b/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/facebook/yoga"). diff --git a/tests/integration/cases/gitlab_tinyMediaManager/policy.dl b/tests/integration/cases/gitlab_tinyMediaManager/policy.dl index 2e6676a22..ded6d28ba 100644 --- a/tests/integration/cases/gitlab_tinyMediaManager/policy.dl +++ b/tests/integration/cases/gitlab_tinyMediaManager/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://gitlab.com/tinyMediaManager/tinyMediaManager"). diff --git a/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl b/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl index c2bb7761c..05bfe5113 100644 --- a/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl +++ b/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://gitlab.com/tinyMediaManager/tinyMediaManager"). diff --git a/tests/integration/cases/google_guava/policy.dl b/tests/integration/cases/google_guava/policy.dl index e872e43db..57c9abd30 100644 --- a/tests/integration/cases/google_guava/policy.dl +++ b/tests/integration/cases/google_guava/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -15,7 +15,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/google/guava"), diff --git a/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl b/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl index c722e0298..c2551db50 100644 --- a/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl +++ b/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/FasterXML/jackson-databind"). diff --git a/tests/integration/cases/jenkinsci_plotplugin/policy.dl b/tests/integration/cases/jenkinsci_plotplugin/policy.dl index 355ee5e08..187f7d2ee 100644 --- a/tests/integration/cases/jenkinsci_plotplugin/policy.dl +++ b/tests/integration/cases/jenkinsci_plotplugin/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/jenkinsci/plot-plugin"). diff --git a/tests/integration/cases/log4j_release_pipeline/policy.dl b/tests/integration/cases/log4j_release_pipeline/policy.dl index 3044be45a..27b256d49 100644 --- a/tests/integration/cases/log4j_release_pipeline/policy.dl +++ b/tests/integration/cases/log4j_release_pipeline/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -14,7 +14,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/logging-log4j2"). diff --git a/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl b/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl index 2a2a68caf..a5e710912 100644 --- a/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl +++ b/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -12,7 +12,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/logging-log4j2"), diff --git a/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl b/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl index 048942d06..e0f43e2ce 100644 --- a/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl +++ b/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,8 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_tool_1"), build_tool_check(gradle_id, "gradle", "java"), check_facts(gradle_id, _, component_id,_,_), - check_passed(component_id, "mcn_provenance_level_three_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), diff --git a/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml b/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml index e0344b508..004958361 100644 --- a/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml +++ b/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -15,6 +15,7 @@ steps: command_args: - -purl - pkg:maven/io.micronaut.test/micronaut-test-junit5@4.5.0 + - --verify-provenance - name: Validate JSON report schema kind: validate_schema options: diff --git a/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl b/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl index 56b09f46a..418dee728 100644 --- a/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl +++ b/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/onu-ui/onu-ui"). diff --git a/tests/integration/cases/ossf_scorecard/config.ini b/tests/integration/cases/ossf_scorecard/config.ini index f39949cc4..ec1d945d4 100644 --- a/tests/integration/cases/ossf_scorecard/config.ini +++ b/tests/integration/cases/ossf_scorecard/config.ini @@ -1,9 +1,9 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. [analysis.checks] exclude = include = mcn_provenance_expectation_1 - mcn_provenance_level_three_1 + mcn_provenance_verified_1 mcn_trusted_builder_level_three_1 diff --git a/tests/integration/cases/ossf_scorecard/policy.dl b/tests/integration/cases/ossf_scorecard/policy.dl index a69a7335b..7468219f5 100644 --- a/tests/integration/cases/ossf_scorecard/policy.dl +++ b/tests/integration/cases/ossf_scorecard/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -6,7 +6,8 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_available_1"), check_passed(component_id, "mcn_provenance_expectation_1"), - check_passed(component_id, "mcn_provenance_level_three_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_passed(component_id, "mcn_trusted_builder_level_three_1"), check_passed(component_id, "mcn_version_control_system_1"), is_repo_url(component_id, "https://github.com/ossf/scorecard"). diff --git a/tests/integration/cases/ossf_scorecard/test.yaml b/tests/integration/cases/ossf_scorecard/test.yaml index a1c778b5a..c3d64f980 100644 --- a/tests/integration/cases/ossf_scorecard/test.yaml +++ b/tests/integration/cases/ossf_scorecard/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: > @@ -17,6 +17,7 @@ steps: command_args: - --package-url - pkg:github/ossf/scorecard@v4.13.1 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/ossf_scorecard/vsa_payload.json b/tests/integration/cases/ossf_scorecard/vsa_payload.json index 2d8288940..2d9c10352 100644 --- a/tests/integration/cases/ossf_scorecard/vsa_payload.json +++ b/tests/integration/cases/ossf_scorecard/vsa_payload.json @@ -16,7 +16,7 @@ "timeVerified": "2024-02-16T06:03:16.417400+00:00", "resourceUri": "pkg:github/ossf/scorecard@v4.13.1", "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\n#include \"prelude.dl\"\n\nPolicy(\"auth-provenance\", component_id, \"\") :- check_passed(component_id, \"mcn_provenance_level_three_1\").\napply_policy_to(\"auth-provenance\", component_id) :- is_component(component_id, \"pkg:github/ossf/scorecard@v4.13.1\").\n" + "content": "/* Copyright (c) 2024 - 2025, 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\n#include \"prelude.dl\"\n\nPolicy(\"auth-provenance\", component_id, \"\") :-\n check_passed(component_id, \"mcn_provenance_verified_1\"),\n provenance_verified_check(_, build_level, _),\n build_level = 3.\n\napply_policy_to(\"auth-provenance\", component_id) :-\n is_component(component_id, \"pkg:github/ossf/scorecard@v4.13.1\").\n" }, "verificationResult": "PASSED", "verifiedLevels": [] diff --git a/tests/integration/cases/ossf_scorecard/vsa_policy.dl b/tests/integration/cases/ossf_scorecard/vsa_policy.dl index 0dba061e5..cefbd0abf 100644 --- a/tests/integration/cases/ossf_scorecard/vsa_policy.dl +++ b/tests/integration/cases/ossf_scorecard/vsa_policy.dl @@ -1,8 +1,13 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" -Policy("auth-provenance", component_id, "") :- check_passed(component_id, "mcn_provenance_level_three_1"). -apply_policy_to("auth-provenance", component_id) :- is_component(component_id, "pkg:github/ossf/scorecard@v4.13.1"). +Policy("auth-provenance", component_id, "") :- + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3. + +apply_policy_to("auth-provenance", component_id) :- + is_component(component_id, "pkg:github/ossf/scorecard@v4.13.1"). diff --git a/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl b/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl index e0ae8d6c7..45d180746 100644 --- a/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl +++ b/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -12,7 +12,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), check_failed(component_id, "mcn_version_control_system_1"). diff --git a/tests/integration/cases/semver/policy.dl b/tests/integration/cases/semver/policy.dl index 717062b48..bdaaed0fa 100644 --- a/tests/integration/cases/semver/policy.dl +++ b/tests/integration/cases/semver/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -19,7 +19,6 @@ Policy("test_policy", component_id, "") :- // mcn_find_artifact_pipeline_1 check fails, which is a false negative. // TODO: improve the build_as_code check analysis. check_failed(component_id, "mcn_find_artifact_pipeline_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/npm/node-semver"). diff --git a/tests/integration/cases/semver/test.yaml b/tests/integration/cases/semver/test.yaml index 5ed51623e..fa6a1b174 100644 --- a/tests/integration/cases/semver/test.yaml +++ b/tests/integration/cases/semver/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -17,6 +17,7 @@ steps: command_args: - -purl - pkg:npm/semver@7.6.2 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/sigstore_mock/policy.dl b/tests/integration/cases/sigstore_mock/policy.dl index b35d2bb4d..283d90313 100644 --- a/tests/integration/cases/sigstore_mock/policy.dl +++ b/tests/integration/cases/sigstore_mock/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_derived_repo_1"), check_passed(component_id, "mcn_provenance_verified_1"), check_failed(component_id, "mcn_find_artifact_pipeline_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/sigstore/sigstore-js"), diff --git a/tests/integration/cases/sigstore_mock/test.yaml b/tests/integration/cases/sigstore_mock/test.yaml index c748cfba3..bd635febe 100644 --- a/tests/integration/cases/sigstore_mock/test.yaml +++ b/tests/integration/cases/sigstore_mock/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -21,6 +21,7 @@ steps: - main - -d - ebdcfdfbdfeb9c9aeee6df53674ef230613629f5 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/sigstore_sget/policy.dl b/tests/integration/cases/sigstore_sget/policy.dl index df5c2a294..5babe3f8d 100644 --- a/tests/integration/cases/sigstore_sget/policy.dl +++ b/tests/integration/cases/sigstore_sget/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), check_failed(component_id, "mcn_provenance_verified_1"), diff --git a/tests/integration/cases/slsa-framework_slsa-verifier/config.ini b/tests/integration/cases/slsa-framework_slsa-verifier/config.ini deleted file mode 100644 index 884ca6874..000000000 --- a/tests/integration/cases/slsa-framework_slsa-verifier/config.ini +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2024 - 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/. - -[analysis.checks] -exclude = mcn_provenance_level_three_1 -include = * diff --git a/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml b/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml index 20c21a992..7dc7fb28d 100644 --- a/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml +++ b/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -12,7 +12,6 @@ steps: - name: Run macaron analyze kind: analyze options: - ini: config.ini expectation: expectation.cue command_args: - -rp diff --git a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/config.ini b/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/config.ini deleted file mode 100644 index 884ca6874..000000000 --- a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/config.ini +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2024 - 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/. - -[analysis.checks] -exclude = mcn_provenance_level_three_1 -include = * diff --git a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml b/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml index 73b57c077..ad62c3024 100644 --- a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml +++ b/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -11,7 +11,6 @@ steps: - name: Run macaron analyze without url link configuration. kind: analyze options: - ini: config.ini expectation: expectation.cue provenance: slsa_verifier.jsonl command_args: diff --git a/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl b/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl index dd3b5d280..715400923 100644 --- a/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl +++ b/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://bitbucket.org/snakeyaml/snakeyaml"). diff --git a/tests/integration/cases/timyarkov_docker_test/policy.dl b/tests/integration/cases/timyarkov_docker_test/policy.dl index 599dcc138..0c5eceb2d 100644 --- a/tests/integration/cases/timyarkov_docker_test/policy.dl +++ b/tests/integration/cases/timyarkov_docker_test/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/timyarkov/docker_test"). diff --git a/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl b/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl index 90c4f2339..3b93d8ab1 100644 --- a/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl +++ b/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -18,7 +18,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/timyarkov/multibuild_test"). diff --git a/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml b/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml index 28b6ca912..ac11642f4 100644 --- a/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml +++ b/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -14,6 +14,7 @@ steps: command_args: - -purl - pkg:npm/semver@7.6.2 + - --verify-provenance - name: Verify checks for semver@7.6.2 kind: verify options: @@ -24,6 +25,7 @@ steps: command_args: - -purl - pkg:npm/semver@7.6.0 + - --verify-provenance - name: Verify checks for all 7.6.x semver runs kind: verify options: @@ -34,6 +36,7 @@ steps: command_args: - -purl - pkg:npm/semver@1.0.0 + - --verify-provenance - name: Verify checks for all semver runs kind: verify options: diff --git a/tests/integration/cases/uiv-lib_uiv/policy.dl b/tests/integration/cases/uiv-lib_uiv/policy.dl index ae20ef440..35e17f423 100644 --- a/tests/integration/cases/uiv-lib_uiv/policy.dl +++ b/tests/integration/cases/uiv-lib_uiv/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/uiv-lib/uiv"). diff --git a/tests/integration/cases/urllib3_expectation_dir/policy.dl b/tests/integration/cases/urllib3_expectation_dir/policy.dl index 2ba5f9dbe..1ce99eca8 100644 --- a/tests/integration/cases/urllib3_expectation_dir/policy.dl +++ b/tests/integration/cases/urllib3_expectation_dir/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -9,7 +9,6 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), check_passed(component_id, "mcn_provenance_available_1"), - check_passed(component_id, "mcn_provenance_level_three_1"), check_passed(component_id, "mcn_provenance_derived_commit_1"), check_passed(component_id, "mcn_provenance_derived_repo_1"), check_passed(component_id, "mcn_provenance_expectation_1"), @@ -17,6 +16,9 @@ Policy("test_policy", component_id, "") :- build_tool_check(pip_id, "pip", "python"), check_facts(pip_id, _, component_id,_,_), check_failed(component_id, "mcn_find_artifact_pipeline_1"), + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/urllib3/urllib3"), diff --git a/tests/integration/cases/urllib3_expectation_dir/test.yaml b/tests/integration/cases/urllib3_expectation_dir/test.yaml index 06ce83914..ec9e2739d 100644 --- a/tests/integration/cases/urllib3_expectation_dir/test.yaml +++ b/tests/integration/cases/urllib3_expectation_dir/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -18,6 +18,7 @@ steps: - pkg:pypi/urllib3@2.0.0a1 - --provenance-expectation - expectation + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/urllib3_expectation_file/policy.dl b/tests/integration/cases/urllib3_expectation_file/policy.dl index 3fea375f2..f230dfe07 100644 --- a/tests/integration/cases/urllib3_expectation_file/policy.dl +++ b/tests/integration/cases/urllib3_expectation_file/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -12,8 +12,10 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_derived_commit_1"), check_passed(component_id, "mcn_provenance_derived_repo_1"), check_passed(component_id, "mcn_provenance_expectation_1"), - check_passed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_find_artifact_pipeline_1"), + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/urllib3/urllib3"). diff --git a/tests/integration/cases/urllib3_expectation_file/test.yaml b/tests/integration/cases/urllib3_expectation_file/test.yaml index c2049d9e7..21441d0a5 100644 --- a/tests/integration/cases/urllib3_expectation_file/test.yaml +++ b/tests/integration/cases/urllib3_expectation_file/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -17,6 +17,7 @@ steps: command_args: - -purl - pkg:pypi/urllib3@2.0.0a1 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/urllib3_invalid_expectation/policy.dl b/tests/integration/cases/urllib3_invalid_expectation/policy.dl index 48dd5adc2..cc71227fa 100644 --- a/tests/integration/cases/urllib3_invalid_expectation/policy.dl +++ b/tests/integration/cases/urllib3_invalid_expectation/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -11,7 +11,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_available_1"), check_passed(component_id, "mcn_provenance_derived_commit_1"), check_passed(component_id, "mcn_provenance_derived_repo_1"), - check_passed(component_id, "mcn_provenance_level_three_1"), + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_find_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), diff --git a/tests/integration/cases/urllib3_invalid_expectation/test.yaml b/tests/integration/cases/urllib3_invalid_expectation/test.yaml index 93a7633c8..f50aefebf 100644 --- a/tests/integration/cases/urllib3_invalid_expectation/test.yaml +++ b/tests/integration/cases/urllib3_invalid_expectation/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -17,6 +17,7 @@ steps: command_args: - -purl - pkg:pypi/urllib3@2.0.0a1 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl b/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl index 0ac7956cb..a1f9c2e28 100644 --- a/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl +++ b/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/wojtekmaj/react-pdf"). diff --git a/tests/provenance/__init__.py b/tests/provenance/__init__.py new file mode 100644 index 000000000..c354d6151 --- /dev/null +++ b/tests/provenance/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024 - 2025, 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/. diff --git a/tests/repo_finder/test_provenance_extractor.py b/tests/provenance/test_provenance_extractor.py similarity index 99% rename from tests/repo_finder/test_provenance_extractor.py rename to tests/provenance/test_provenance_extractor.py index 0fc2460ef..2f1581200 100644 --- a/tests/repo_finder/test_provenance_extractor.py +++ b/tests/provenance/test_provenance_extractor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module tests the provenance extractor on valid example provenances.""" @@ -9,7 +9,7 @@ from macaron.errors import ProvenanceError from macaron.json_tools import JsonType, json_extract -from macaron.repo_finder.provenance_extractor import ( +from macaron.provenance.provenance_extractor import ( check_if_repository_purl_and_url_match, extract_repo_and_commit_from_provenance, ) diff --git a/tests/repo_finder/test_provenance_finder.py b/tests/provenance/test_provenance_finder.py similarity index 88% rename from tests/repo_finder/test_provenance_finder.py rename to tests/provenance/test_provenance_finder.py index 20f2c0ad9..3cd610c0c 100644 --- a/tests/repo_finder/test_provenance_finder.py +++ b/tests/provenance/test_provenance_finder.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module tests the provenance finder.""" @@ -14,14 +14,14 @@ from pydriller import Git from macaron.code_analyzer.call_graph import BaseNode, CallGraph -from macaron.repo_finder.provenance_finder import find_gav_provenance, find_npm_provenance, find_provenance_from_ci +from macaron.provenance.provenance_finder import find_gav_provenance, find_npm_provenance, find_provenance_from_ci from macaron.slsa_analyzer.ci_service import BaseCIService, CircleCI, GitHubActions, GitLabCI, Jenkins, Travis from macaron.slsa_analyzer.git_service.api_client import GhAPIClient from macaron.slsa_analyzer.package_registry import JFrogMavenRegistry, NPMRegistry from macaron.slsa_analyzer.package_registry.jfrog_maven_registry import JFrogMavenAsset, JFrogMavenAssetMetadata from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext @@ -161,15 +161,16 @@ def test_provenance_on_unsupported_ci(macaron_path: Path, service: BaseCIService provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) # Set up the context object with provenances. ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") ctx.dynamic_data["ci_services"] = [ci_info] - provenance = find_provenance_from_ci(ctx, None) - assert provenance is None + with tempfile.TemporaryDirectory() as temp_dir: + provenance = find_provenance_from_ci(ctx, None, temp_dir) + assert provenance is None def test_provenance_on_supported_ci(macaron_path: Path, test_dir: Path) -> None: @@ -185,7 +186,7 @@ def test_provenance_on_supported_ci(macaron_path: Path, test_dir: Path) -> None: provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) # Set up the context object with provenances. @@ -194,13 +195,15 @@ def test_provenance_on_supported_ci(macaron_path: Path, test_dir: Path) -> None: # Test with a valid setup. git_obj = MockGit() - provenance = find_provenance_from_ci(ctx, git_obj) - assert provenance + with tempfile.TemporaryDirectory() as temp_dir: + provenance = find_provenance_from_ci(ctx, git_obj, temp_dir) + assert provenance # Test with a repo that doesn't have any accepted provenance. api_client.release = {"assets": [{"name": "attestation.intoto", "url": "URL", "size": 10}]} - provenance = find_provenance_from_ci(ctx, MockGit()) - assert provenance is None + with tempfile.TemporaryDirectory() as temp_dir: + provenance = find_provenance_from_ci(ctx, MockGit(), temp_dir) + assert provenance is None def test_provenance_available_on_npm_registry( diff --git a/tests/slsa_analyzer/checks/test_build_as_code_check.py b/tests/slsa_analyzer/checks/test_build_as_code_check.py index 99aba2af1..b1bd82b12 100644 --- a/tests/slsa_analyzer/checks/test_build_as_code_check.py +++ b/tests/slsa_analyzer/checks/test_build_as_code_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the tests for the Build As Code Check.""" @@ -26,7 +26,7 @@ from macaron.slsa_analyzer.ci_service.jenkins import Jenkins from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext, build_github_actions_call_graph_for_commands @@ -58,7 +58,7 @@ def test_build_as_code_check_no_callgraph( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) use_build_tool = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") use_build_tool.dynamic_data["build_spec"]["tools"] = [build_tools[build_tool_name]] @@ -109,7 +109,7 @@ def test_deploy_commands( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ci_info["service"] = github_actions_service deploy_ctx.dynamic_data["ci_services"] = [ci_info] @@ -147,7 +147,7 @@ def test_gha_workflow_deployment( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) workflows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "github", "workflow_files") @@ -193,7 +193,7 @@ def test_travis_ci_deploy( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) gradle_deploy = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") gradle_deploy.component.repository.fs_path = str(repo_path.absolute()) @@ -214,7 +214,7 @@ def test_multibuild_facts_saved( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) multi_build = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") diff --git a/tests/slsa_analyzer/checks/test_build_service_check.py b/tests/slsa_analyzer/checks/test_build_service_check.py index 0f5d9aeb0..4a5496c39 100644 --- a/tests/slsa_analyzer/checks/test_build_service_check.py +++ b/tests/slsa_analyzer/checks/test_build_service_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the tests for the Build Service Check.""" @@ -16,7 +16,7 @@ from macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci import GitHubActions from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext, build_github_actions_call_graph_for_commands @@ -48,7 +48,7 @@ def test_build_service_check_no_callgraph( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) use_build_tool = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") use_build_tool.dynamic_data["build_spec"]["tools"] = [build_tools[build_tool_name]] @@ -99,7 +99,7 @@ def test_packaging_commands( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ci_info["service"] = github_actions_service package_ctx.dynamic_data["ci_services"] = [ci_info] @@ -118,7 +118,7 @@ def test_multibuild_facts_saved( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) multi_build = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") diff --git a/tests/slsa_analyzer/checks/test_provenance_l3_check.py b/tests/slsa_analyzer/checks/test_provenance_l3_check.py deleted file mode 100644 index 0715d33c1..000000000 --- a/tests/slsa_analyzer/checks/test_provenance_l3_check.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) 2022 - 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/. - -"""This module contains tests for the provenance l3 check.""" - - -from macaron.code_analyzer.call_graph import BaseNode, CallGraph -from macaron.slsa_analyzer.checks.check_result import CheckResultType -from macaron.slsa_analyzer.checks.provenance_l3_check import ProvenanceL3Check -from macaron.slsa_analyzer.ci_service.circleci import CircleCI -from macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci import GitHubActions -from macaron.slsa_analyzer.ci_service.gitlab_ci import GitLabCI -from macaron.slsa_analyzer.ci_service.jenkins import Jenkins -from macaron.slsa_analyzer.ci_service.travis import Travis -from macaron.slsa_analyzer.git_service.api_client import GhAPIClient, GitHubReleaseAsset -from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload -from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance -from tests.conftest import MockAnalyzeContext - -from ...macaron_testcase import MacaronTestCase - - -class MockGitHubActions(GitHubActions): - """Mock the GitHubActions class.""" - - def has_latest_run_passed( - self, repo_full_name: str, branch_name: str | None, commit_sha: str, commit_date: str, workflow: str - ) -> str: - return "run_feedback" - - -class MockGhAPIClient(GhAPIClient): - """Mock GhAPIClient class.""" - - def __init__(self, profile: dict): - super().__init__(profile) - self.release = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": "10"}, - {"name": "artifact.txt", "url": "URL", "size": "10"}, - ] - } - - def get_latest_release(self, full_name: str) -> dict: - return self.release - - def download_asset(self, url: str, download_path: str) -> bool: - return True - - -class TestProvL3Check(MacaronTestCase): - """Test the provenance l3 check.""" - - def test_provenance_l3_check(self) -> None: - """Test the provenance l3 check.""" - check = ProvenanceL3Check() - github_actions = MockGitHubActions() - api_client = MockGhAPIClient({"headers": {}, "query": []}) - github_actions.api_client = api_client - github_actions.load_defaults() - jenkins = Jenkins() - jenkins.load_defaults() - travis = Travis() - travis.load_defaults() - circle_ci = CircleCI() - circle_ci.load_defaults() - gitlab_ci = GitLabCI() - gitlab_ci.load_defaults() - - ci_info = CIInfo( - service=github_actions, - callgraph=CallGraph(BaseNode(), ""), - provenance_assets=[], - release={}, - provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), - ) - - # Repo has provenances but no downloaded files. - ci_info["provenance_assets"] = [] - ci_info["provenance_assets"].extend( - [ - GitHubReleaseAsset( - name="attestation.intoto.jsonl", - url="URL", - size_in_bytes=10, - api_client=api_client, - ) - ] - ) - ci_info["release"] = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": 10}, - {"name": "artifact.txt", "url": "URL", "size": 10}, - ] - } - ctx = MockAnalyzeContext(macaron_path=MacaronTestCase.macaron_path, output_dir="") - ctx.dynamic_data["ci_services"] = [ci_info] - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Attestation size is too large. - ci_info["provenance_assets"] = [] - ci_info["provenance_assets"].extend( - [ - GitHubReleaseAsset( - name="attestation.intoto.jsonl", - url="URL", - size_in_bytes=100_000_000, - api_client=api_client, - ) - ] - ) - ci_info["release"] = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": 100_000_000}, - {"name": "artifact.txt", "url": "URL", "size": 10}, - ] - } - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # No provenance available. - ci_info["provenance_assets"] = [] - ci_info["release"] = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": 10}, - {"name": "artifact.txt", "url": "URL", "size": 10}, - ] - } - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # No release available - ci_info["provenance_assets"] = [] - ci_info["provenance_assets"].extend( - [ - GitHubReleaseAsset( - name="attestation.intoto.jsonl", - url="URL", - size_in_bytes=10, - api_client=api_client, - ) - ] - ) - ci_info["release"] = {} - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test Jenkins. - ci_info["service"] = jenkins - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test Travis. - ci_info["service"] = travis - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test Circle CI. - ci_info["service"] = circle_ci - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test GitLab CI. - ci_info["service"] = gitlab_ci - assert check.run_check(ctx).result_type == CheckResultType.FAILED diff --git a/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py b/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py index 5decbd16e..8584e5f35 100644 --- a/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py +++ b/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. """This module contains tests for the expectation check.""" @@ -20,7 +20,7 @@ from macaron.slsa_analyzer.provenance.loader import load_provenance_payload from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext from ...macaron_testcase import MacaronTestCase @@ -86,7 +86,7 @@ def test_expectation_check(self) -> None: provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ctx.dynamic_data["ci_services"] = [ci_info] diff --git a/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py b/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py index 57f60875e..fa65d2002 100644 --- a/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py +++ b/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. """This module contains tests for the provenance available check.""" @@ -7,7 +7,7 @@ import pytest -from macaron.database.table_definitions import CheckFacts, Repository +from macaron.database.table_definitions import CheckFacts, Provenance, Repository 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 @@ -51,8 +51,7 @@ def test_provenance_repo_commit_checks_pass( ) -> None: """Test combinations of Repository objects and provenance strings against check.""" context = _prepare_context(macaron_path, repository) - context.dynamic_data["provenance_repo_url"] = repo_url - context.dynamic_data["provenance_commit_digest"] = commit_digest + context.dynamic_data["provenance_info"] = Provenance(repository_url=repo_url, commit_sha=commit_digest) # Check Repo repo_result = _perform_check_assert_result_return_result( diff --git a/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py b/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py index 936a98957..c36eba0d5 100644 --- a/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py +++ b/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains the tests for the Trusted Builder Level three check.""" @@ -20,7 +20,7 @@ from macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci import GitHubActions from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext @@ -51,7 +51,7 @@ def test_trusted_builder_l3_check( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") diff --git a/tests/slsa_analyzer/test_analyze_context.py b/tests/slsa_analyzer/test_analyze_context.py index c9bb23c4e..40a4ad881 100644 --- a/tests/slsa_analyzer/test_analyze_context.py +++ b/tests/slsa_analyzer/test_analyze_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. """This module contains tests for the AnalyzeContext module.""" @@ -14,7 +14,7 @@ from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.slsa_req import ReqName, SLSAReqStatus from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext @@ -101,7 +101,7 @@ def test_provenances(self) -> None: payload=expected_payload, asset=VirtualReleaseAsset(name="No_ASSET", url="NO_URL", size_in_bytes=0) ), ], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) self.analyze_ctx.dynamic_data["ci_services"].append(gh_actions_ci_info)