diff --git a/src/macaron/provenance/__init__.py b/src/macaron/provenance/__init__.py index a99afa31c..7e3c5a63b 100644 --- a/src/macaron/provenance/__init__.py +++ b/src/macaron/provenance/__init__.py @@ -2,3 +2,16 @@ # 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.""" + +from dataclasses import dataclass + +from macaron.slsa_analyzer.provenance.intoto import InTotoPayload + + +@dataclass(frozen=True) +class ProvenanceAsset: + """This class exists to hold a provenance payload with the original asset's name and URL.""" + + payload: InTotoPayload + name: str + url: str diff --git a/src/macaron/provenance/provenance_finder.py b/src/macaron/provenance/provenance_finder.py index 4935ca62d..0c1385d0f 100644 --- a/src/macaron/provenance/provenance_finder.py +++ b/src/macaron/provenance/provenance_finder.py @@ -6,7 +6,6 @@ import logging import os import tempfile -from dataclasses import dataclass from functools import partial from packageurl import PackageURL @@ -14,6 +13,7 @@ from macaron.artifact.local_artifact import get_local_artifact_hash from macaron.config.defaults import defaults +from macaron.provenance import ProvenanceAsset from macaron.repo_finder.commit_finder import AbstractPurlType, determine_abstract_purl_type from macaron.repo_finder.repo_finder_deps_dev import DepsDevRepoFinder from macaron.repo_finder.repo_utils import get_repo_tags @@ -30,7 +30,6 @@ ) from macaron.slsa_analyzer.package_registry.npm_registry import NPMAttestationAsset from macaron.slsa_analyzer.package_registry.pypi_registry import find_or_create_pypi_asset -from macaron.slsa_analyzer.provenance.intoto import InTotoPayload from macaron.slsa_analyzer.provenance.intoto.errors import LoadIntotoAttestationError from macaron.slsa_analyzer.provenance.loader import load_provenance_payload from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData @@ -41,15 +40,6 @@ logger: logging.Logger = logging.getLogger(__name__) -@dataclass(frozen=True) -class ProvenanceAsset: - """This class exists to hold a provenance payload with the original asset's name and URL.""" - - payload: InTotoPayload - name: str - url: str - - class ProvenanceFinder: """This class is used to find and retrieve provenance files from supported registries.""" diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index 31b4f0937..a76e45e1b 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -527,9 +527,10 @@ def run_single( # Try to discover GitHub attestation for the target software component. artifact_hash = get_artifact_hash(parsed_purl, local_artifact_dirs, package_registries_info) if artifact_hash: - provenance_payload = git_service.get_attestation_payload( + provenance_asset = git_service.get_attestation( analyze_ctx.component.repository.full_name, artifact_hash ) + provenance_payload = provenance_asset.payload if provenance_asset else None if provenance_payload: try: provenance_repo_url, provenance_commit_digest = extract_repo_and_commit_from_provenance( diff --git a/src/macaron/slsa_analyzer/git_service/api_client.py b/src/macaron/slsa_analyzer/git_service/api_client.py index 98012fae8..9921c2dc9 100644 --- a/src/macaron/slsa_analyzer/git_service/api_client.py +++ b/src/macaron/slsa_analyzer/git_service/api_client.py @@ -648,8 +648,8 @@ def download_asset(self, url: str, download_path: str) -> bool: return download_file_with_size_limit(url, headers, download_path, timeout, size_limit) - def get_attestation(self, full_name: str, artifact_hash: str) -> dict: - """Download and return the attestation associated with the passed artifact hash, if any. + def get_attestation(self, full_name: str, artifact_hash: str) -> tuple[str | None, dict]: + """Download and return the attestation url and content associated with the passed artifact hash, if any. Parameters ---------- @@ -660,12 +660,14 @@ def get_attestation(self, full_name: str, artifact_hash: str) -> dict: Returns ------- - dict - The attestation data, or an empty dict if not found. + tuple[str|None,dict] + The attestation url and data, or None and an empty dict if not found. """ url = f"{GhAPIClient._REPO_END_POINT}/{full_name}/attestations/sha256:{artifact_hash}" response_data = send_get_http(url, self.headers) - return response_data or {} + if not response_data: + return (None, {}) + return (url, response_data) def get_default_gh_client(access_token: str) -> GhAPIClient: diff --git a/src/macaron/slsa_analyzer/git_service/github.py b/src/macaron/slsa_analyzer/git_service/github.py index ff7ecc593..d5e1c8548 100644 --- a/src/macaron/slsa_analyzer/git_service/github.py +++ b/src/macaron/slsa_analyzer/git_service/github.py @@ -9,10 +9,11 @@ from macaron.config.global_config import global_config from macaron.errors import ConfigurationError, RepoCheckOutError from macaron.json_tools import json_extract +from macaron.provenance import ProvenanceAsset from macaron.slsa_analyzer import git_url from macaron.slsa_analyzer.git_service.api_client import GhAPIClient, get_default_gh_client from macaron.slsa_analyzer.git_service.base_git_service import BaseGitService -from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, ValidateInTotoPayloadError, validate_intoto_payload +from macaron.slsa_analyzer.provenance.intoto import ValidateInTotoPayloadError, validate_intoto_payload from macaron.slsa_analyzer.provenance.loader import decode_provenance logger: logging.Logger = logging.getLogger(__name__) @@ -96,7 +97,7 @@ def check_out_repo(self, git_obj: Git, branch: str, digest: str, offline_mode: b return git_obj - def get_attestation_payload(self, repository_name: str, artifact_hash: str) -> InTotoPayload | None: + def get_attestation(self, repository_name: str, artifact_hash: str) -> ProvenanceAsset | None: """Get the GitHub attestation associated with the given PURL, or None if it cannot be found. The schema of GitHub attestation can be found on the API page: @@ -111,12 +112,12 @@ def get_attestation_payload(self, repository_name: str, artifact_hash: str) -> I Returns ------- - InTotoPayload | None - The attestation payload, if found. + ProvenanceAsset | None + The provenance asset, if found. """ - git_attestation_dict = self.api_client.get_attestation(repository_name, artifact_hash) + attestation_url, git_attestation_dict = self.api_client.get_attestation(repository_name, artifact_hash) - if not git_attestation_dict: + if not attestation_url or not git_attestation_dict: return None git_attestation_list = json_extract(git_attestation_dict, ["attestations"], list) @@ -124,9 +125,13 @@ def get_attestation_payload(self, repository_name: str, artifact_hash: str) -> I return None payload = decode_provenance(git_attestation_list[0]) - + validated_payload = None try: - return validate_intoto_payload(payload) + validated_payload = validate_intoto_payload(payload) except ValidateInTotoPayloadError as error: logger.debug("Invalid attestation payload: %s", error) return None + if not validated_payload: + return None + + return ProvenanceAsset(validated_payload, artifact_hash, attestation_url)