|
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
|
@@ -395,6 +404,17 @@ def run_single(
|
395 | 404 | status=SCMStatus.ANALYSIS_FAILED,
|
396 | 405 | )
|
397 | 406 |
|
| 407 | + local_artifact_dirs = None |
| 408 | + if parsed_purl and parsed_purl.type in self.local_artifact_repo_mapper: |
| 409 | + local_artifact_repo_path = self.local_artifact_repo_mapper[parsed_purl.type] |
| 410 | + try: |
| 411 | + local_artifact_dirs = get_local_artifact_dirs( |
| 412 | + purl=parsed_purl, |
| 413 | + local_artifact_repo_path=local_artifact_repo_path, |
| 414 | + ) |
| 415 | + except LocalArtifactFinderError as error: |
| 416 | + logger.debug(error) |
| 417 | + |
398 | 418 | # Prepare the repo.
|
399 | 419 | git_obj = None
|
400 | 420 | commit_finder_outcome = CommitFinderInfo.NOT_USED
|
@@ -472,6 +492,37 @@ def run_single(
|
472 | 492 | git_service = self._determine_git_service(analyze_ctx)
|
473 | 493 | self._determine_ci_services(analyze_ctx, git_service)
|
474 | 494 | self._determine_build_tools(analyze_ctx, git_service)
|
| 495 | + |
| 496 | + # Try to find an attestation from GitHub, if applicable. |
| 497 | + if parsed_purl and not provenance_payload and analysis_target.repo_path and isinstance(git_service, GitHub): |
| 498 | + # Try to discover GitHub attestation for the target software component. |
| 499 | + url = None |
| 500 | + try: |
| 501 | + url = urllib.parse.urlparse(analysis_target.repo_path) |
| 502 | + except TypeError as error: |
| 503 | + logger.debug("Failed to parse repository path as URL: %s", error) |
| 504 | + if url and url.hostname == "github.com": |
| 505 | + artifact_hash = self.get_artifact_hash(parsed_purl, local_artifact_dirs, hashlib.sha256()) |
| 506 | + if artifact_hash: |
| 507 | + git_attestation_dict = git_service.api_client.get_attestation( |
| 508 | + analyze_ctx.component.repository.full_name, artifact_hash |
| 509 | + ) |
| 510 | + if git_attestation_dict: |
| 511 | + git_attestation_list = json_extract(git_attestation_dict, ["attestations"], list) |
| 512 | + if git_attestation_list: |
| 513 | + git_attestation = git_attestation_list[0] |
| 514 | + |
| 515 | + with tempfile.TemporaryDirectory() as temp_dir: |
| 516 | + attestation_file = os.path.join(temp_dir, "attestation") |
| 517 | + with open(attestation_file, "w", encoding="UTF-8") as file: |
| 518 | + json.dump(git_attestation, file) |
| 519 | + |
| 520 | + try: |
| 521 | + payload = load_provenance_payload(attestation_file) |
| 522 | + provenance_payload = payload |
| 523 | + except LoadIntotoAttestationError as error: |
| 524 | + logger.debug("Failed to load provenance payload: %s", error) |
| 525 | + |
475 | 526 | if parsed_purl is not None:
|
476 | 527 | self._verify_repository_link(parsed_purl, analyze_ctx)
|
477 | 528 | self._determine_package_registries(analyze_ctx)
|
@@ -533,16 +584,8 @@ def run_single(
|
533 | 584 |
|
534 | 585 | analyze_ctx.dynamic_data["validate_malware"] = validate_malware
|
535 | 586 |
|
536 |
| - if parsed_purl and parsed_purl.type in self.local_artifact_repo_mapper: |
537 |
| - local_artifact_repo_path = self.local_artifact_repo_mapper[parsed_purl.type] |
538 |
| - try: |
539 |
| - local_artifact_dirs = get_local_artifact_dirs( |
540 |
| - purl=parsed_purl, |
541 |
| - local_artifact_repo_path=local_artifact_repo_path, |
542 |
| - ) |
543 |
| - analyze_ctx.dynamic_data["local_artifact_paths"].extend(local_artifact_dirs) |
544 |
| - except LocalArtifactFinderError as error: |
545 |
| - logger.debug(error) |
| 587 | + if local_artifact_dirs: |
| 588 | + analyze_ctx.dynamic_data["local_artifact_paths"].extend(local_artifact_dirs) |
546 | 589 |
|
547 | 590 | analyze_ctx.check_results = registry.scan(analyze_ctx)
|
548 | 591 |
|
@@ -926,6 +969,54 @@ def get_analyze_ctx(self, component: Component) -> AnalyzeContext:
|
926 | 969 |
|
927 | 970 | return analyze_ctx
|
928 | 971 |
|
| 972 | + def get_artifact_hash( |
| 973 | + self, purl: PackageURL, cached_artifacts: list[str] | None, hash_algorithm: Any |
| 974 | + ) -> str | None: |
| 975 | + """Get the hash of the artifact found from the passed PURL using local or remote files. |
| 976 | +
|
| 977 | + Parameters |
| 978 | + ---------- |
| 979 | + purl: PackageURL |
| 980 | + The PURL of the artifact. |
| 981 | + cached_artifacts: list[str] | None |
| 982 | + The list of local files that match the PURL. |
| 983 | + hash_algorithm: Any |
| 984 | + The hash algorithm to use. |
| 985 | +
|
| 986 | + Returns |
| 987 | + ------- |
| 988 | + str | None |
| 989 | + The hash of the artifact, or None if not found. |
| 990 | + """ |
| 991 | + if cached_artifacts: |
| 992 | + # Try to get the hash from a local file. |
| 993 | + artifact_hash = get_local_artifact_hash(purl, cached_artifacts, hash_algorithm.name) |
| 994 | + |
| 995 | + if artifact_hash: |
| 996 | + return artifact_hash |
| 997 | + |
| 998 | + # Download the artifact. |
| 999 | + if purl.type == "maven": |
| 1000 | + maven_registry = next( |
| 1001 | + ( |
| 1002 | + package_registry |
| 1003 | + for package_registry in PACKAGE_REGISTRIES |
| 1004 | + if isinstance(package_registry, MavenCentralRegistry) |
| 1005 | + ), |
| 1006 | + None, |
| 1007 | + ) |
| 1008 | + if not maven_registry: |
| 1009 | + return None |
| 1010 | + |
| 1011 | + return maven_registry.get_artifact_hash(purl, hash_algorithm) |
| 1012 | + |
| 1013 | + if purl.type == "pypi": |
| 1014 | + # TODO implement |
| 1015 | + return None |
| 1016 | + |
| 1017 | + logger.debug("Purl type '%s' not yet supported for GitHub attestation discovery.", purl.type) |
| 1018 | + return None |
| 1019 | + |
929 | 1020 | def _determine_git_service(self, analyze_ctx: AnalyzeContext) -> BaseGitService:
|
930 | 1021 | """Determine the Git service used by the software component."""
|
931 | 1022 | remote_path = analyze_ctx.component.repository.remote_path if analyze_ctx.component.repository else None
|
|
0 commit comments