|
4 | 4 | """This module handles the cloning and analyzing a Git repo.""" |
5 | 5 |
|
6 | 6 | import glob |
| 7 | +import hashlib |
| 8 | +import json |
7 | 9 | import logging |
8 | 10 | import os |
9 | 11 | import re |
10 | 12 | import sys |
11 | 13 | import tempfile |
| 14 | +import urllib.parse |
12 | 15 | from collections.abc import Mapping |
13 | 16 | from datetime import datetime, timezone |
14 | 17 | from pathlib import Path |
|
20 | 23 | from sqlalchemy.orm import Session |
21 | 24 |
|
22 | 25 | from macaron import __version__ |
23 | | -from macaron.artifact.local_artifact import get_local_artifact_dirs |
| 26 | +from macaron.artifact.local_artifact import ( |
| 27 | + get_local_artifact_dirs, |
| 28 | + get_local_artifact_hash, |
| 29 | +) |
24 | 30 | from macaron.config.global_config import global_config |
25 | 31 | from macaron.config.target_config import Configuration |
26 | 32 | from macaron.database.database_manager import DatabaseManager, get_db_manager, get_db_session |
|
41 | 47 | ProvenanceError, |
42 | 48 | PURLNotFoundError, |
43 | 49 | ) |
| 50 | +from macaron.json_tools import json_extract |
44 | 51 | from macaron.output_reporter.reporter import FileReporter |
45 | 52 | from macaron.output_reporter.results import Record, Report, SCMStatus |
46 | 53 | from macaron.provenance import provenance_verifier |
|
66 | 73 | from macaron.slsa_analyzer.checks import * # pylint: disable=wildcard-import,unused-wildcard-import # noqa: F401,F403 |
67 | 74 | from macaron.slsa_analyzer.ci_service import CI_SERVICES |
68 | 75 | from macaron.slsa_analyzer.database_store import store_analyze_context_to_db |
69 | | -from macaron.slsa_analyzer.git_service import GIT_SERVICES, BaseGitService |
| 76 | +from macaron.slsa_analyzer.git_service import GIT_SERVICES, BaseGitService, GitHub |
70 | 77 | from macaron.slsa_analyzer.git_service.base_git_service import NoneGitService |
71 | 78 | from macaron.slsa_analyzer.git_url import GIT_REPOS_DIR |
72 | | -from macaron.slsa_analyzer.package_registry import PACKAGE_REGISTRIES |
| 79 | +from macaron.slsa_analyzer.package_registry import PACKAGE_REGISTRIES, MavenCentralRegistry |
73 | 80 | from macaron.slsa_analyzer.provenance.expectations.expectation_registry import ExpectationRegistry |
74 | 81 | from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV01Payload |
| 82 | +from macaron.slsa_analyzer.provenance.intoto.errors import LoadIntotoAttestationError |
| 83 | +from macaron.slsa_analyzer.provenance.loader import load_provenance_payload |
75 | 84 | from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData |
76 | 85 | from macaron.slsa_analyzer.registry import registry |
77 | 86 | from macaron.slsa_analyzer.specs.ci_spec import CIInfo |
@@ -403,6 +412,17 @@ def run_single( |
403 | 412 | status=SCMStatus.ANALYSIS_FAILED, |
404 | 413 | ) |
405 | 414 |
|
| 415 | + local_artifact_dirs = None |
| 416 | + if parsed_purl and parsed_purl.type in self.local_artifact_repo_mapper: |
| 417 | + local_artifact_repo_path = self.local_artifact_repo_mapper[parsed_purl.type] |
| 418 | + try: |
| 419 | + local_artifact_dirs = get_local_artifact_dirs( |
| 420 | + purl=parsed_purl, |
| 421 | + local_artifact_repo_path=local_artifact_repo_path, |
| 422 | + ) |
| 423 | + except LocalArtifactFinderError as error: |
| 424 | + logger.debug(error) |
| 425 | + |
406 | 426 | # Prepare the repo. |
407 | 427 | git_obj = None |
408 | 428 | commit_finder_outcome = CommitFinderInfo.NOT_USED |
@@ -480,6 +500,37 @@ def run_single( |
480 | 500 | git_service = self._determine_git_service(analyze_ctx) |
481 | 501 | self._determine_ci_services(analyze_ctx, git_service) |
482 | 502 | self._determine_build_tools(analyze_ctx, git_service) |
| 503 | + |
| 504 | + # Try to find an attestation from GitHub, if applicable. |
| 505 | + if parsed_purl and not provenance_payload and analysis_target.repo_path and isinstance(git_service, GitHub): |
| 506 | + # Try to discover GitHub attestation for the target software component. |
| 507 | + url = None |
| 508 | + try: |
| 509 | + url = urllib.parse.urlparse(analysis_target.repo_path) |
| 510 | + except TypeError as error: |
| 511 | + logger.debug("Failed to parse repository path as URL: %s", error) |
| 512 | + if url and url.hostname == "github.com": |
| 513 | + artifact_hash = self.get_artifact_hash(parsed_purl, local_artifact_dirs, hashlib.sha256()) |
| 514 | + if artifact_hash: |
| 515 | + git_attestation_dict = git_service.api_client.get_attestation( |
| 516 | + analyze_ctx.component.repository.full_name, artifact_hash |
| 517 | + ) |
| 518 | + if git_attestation_dict: |
| 519 | + git_attestation_list = json_extract(git_attestation_dict, ["attestations"], list) |
| 520 | + if git_attestation_list: |
| 521 | + git_attestation = git_attestation_list[0] |
| 522 | + |
| 523 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 524 | + attestation_file = os.path.join(temp_dir, "attestation") |
| 525 | + with open(attestation_file, "w", encoding="UTF-8") as file: |
| 526 | + json.dump(git_attestation, file) |
| 527 | + |
| 528 | + try: |
| 529 | + payload = load_provenance_payload(attestation_file) |
| 530 | + provenance_payload = payload |
| 531 | + except LoadIntotoAttestationError as error: |
| 532 | + logger.debug("Failed to load provenance payload: %s", error) |
| 533 | + |
483 | 534 | if parsed_purl is not None: |
484 | 535 | self._verify_repository_link(parsed_purl, analyze_ctx) |
485 | 536 | self._determine_package_registries(analyze_ctx, all_package_registries) |
@@ -541,16 +592,8 @@ def run_single( |
541 | 592 |
|
542 | 593 | analyze_ctx.dynamic_data["validate_malware"] = validate_malware |
543 | 594 |
|
544 | | - if parsed_purl and parsed_purl.type in self.local_artifact_repo_mapper: |
545 | | - local_artifact_repo_path = self.local_artifact_repo_mapper[parsed_purl.type] |
546 | | - try: |
547 | | - local_artifact_dirs = get_local_artifact_dirs( |
548 | | - purl=parsed_purl, |
549 | | - local_artifact_repo_path=local_artifact_repo_path, |
550 | | - ) |
551 | | - analyze_ctx.dynamic_data["local_artifact_paths"].extend(local_artifact_dirs) |
552 | | - except LocalArtifactFinderError as error: |
553 | | - logger.debug(error) |
| 595 | + if local_artifact_dirs: |
| 596 | + analyze_ctx.dynamic_data["local_artifact_paths"].extend(local_artifact_dirs) |
554 | 597 |
|
555 | 598 | analyze_ctx.check_results = registry.scan(analyze_ctx) |
556 | 599 |
|
@@ -939,6 +982,54 @@ def create_analyze_ctx(self, component: Component) -> AnalyzeContext: |
939 | 982 |
|
940 | 983 | return analyze_ctx |
941 | 984 |
|
| 985 | + def get_artifact_hash( |
| 986 | + self, purl: PackageURL, cached_artifacts: list[str] | None, hash_algorithm: Any |
| 987 | + ) -> str | None: |
| 988 | + """Get the hash of the artifact found from the passed PURL using local or remote files. |
| 989 | +
|
| 990 | + Parameters |
| 991 | + ---------- |
| 992 | + purl: PackageURL |
| 993 | + The PURL of the artifact. |
| 994 | + cached_artifacts: list[str] | None |
| 995 | + The list of local files that match the PURL. |
| 996 | + hash_algorithm: Any |
| 997 | + The hash algorithm to use. |
| 998 | +
|
| 999 | + Returns |
| 1000 | + ------- |
| 1001 | + str | None |
| 1002 | + The hash of the artifact, or None if not found. |
| 1003 | + """ |
| 1004 | + if cached_artifacts: |
| 1005 | + # Try to get the hash from a local file. |
| 1006 | + artifact_hash = get_local_artifact_hash(purl, cached_artifacts, hash_algorithm.name) |
| 1007 | + |
| 1008 | + if artifact_hash: |
| 1009 | + return artifact_hash |
| 1010 | + |
| 1011 | + # Download the artifact. |
| 1012 | + if purl.type == "maven": |
| 1013 | + maven_registry = next( |
| 1014 | + ( |
| 1015 | + package_registry |
| 1016 | + for package_registry in PACKAGE_REGISTRIES |
| 1017 | + if isinstance(package_registry, MavenCentralRegistry) |
| 1018 | + ), |
| 1019 | + None, |
| 1020 | + ) |
| 1021 | + if not maven_registry: |
| 1022 | + return None |
| 1023 | + |
| 1024 | + return maven_registry.get_artifact_hash(purl, hash_algorithm) |
| 1025 | + |
| 1026 | + if purl.type == "pypi": |
| 1027 | + # TODO implement |
| 1028 | + return None |
| 1029 | + |
| 1030 | + logger.debug("Purl type '%s' not yet supported for GitHub attestation discovery.", purl.type) |
| 1031 | + return None |
| 1032 | + |
942 | 1033 | def _determine_git_service(self, analyze_ctx: AnalyzeContext) -> BaseGitService: |
943 | 1034 | """Determine the Git service used by the software component.""" |
944 | 1035 | remote_path = analyze_ctx.component.repository.remote_path if analyze_ctx.component.repository else None |
|
0 commit comments