|
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