diff --git a/docs/source/pages/cli_usage/command_analyze.rst b/docs/source/pages/cli_usage/command_analyze.rst index 05d79f9b0..0de7a3a29 100644 --- a/docs/source/pages/cli_usage/command_analyze.rst +++ b/docs/source/pages/cli_usage/command_analyze.rst @@ -58,6 +58,10 @@ Options The path to provenance expectation file or directory. +.. option:: -pf PROVENANCE_FILE, --provenance-file PROVENANCE_FILE + + The path to the provenance file in in-toto format. + .. option:: -c CONFIG_PATH, --config-path CONFIG_PATH The path to the user configuration. diff --git a/scripts/dev_scripts/integration_tests.sh b/scripts/dev_scripts/integration_tests.sh index c57da3749..49d997bb9 100755 --- a/scripts/dev_scripts/integration_tests.sh +++ b/scripts/dev_scripts/integration_tests.sh @@ -635,6 +635,17 @@ then log_fail fi +echo -e "\n----------------------------------------------------------------------------------" +echo "Test providing an invalid provenance file as input." +echo -e "----------------------------------------------------------------------------------\n" +$RUN_MACARON analyze -rp https://github.com/apache/maven --provenance-file $WORKSPACE/golang/internal/cue_validator/resources/invalid_provenance.json --skip-deps + +if [ $? -eq 0 ]; +then + echo -e "Expect non-zero status code but got $?." + log_fail +fi + # Testing the CUE provenance expectation verifier. echo -e "\n----------------------------------------------------------------------------------" echo "Test verifying CUE provenance expectation for ossf/scorecard" diff --git a/src/macaron/__main__.py b/src/macaron/__main__.py index 766f07c5a..ad70156f3 100644 --- a/src/macaron/__main__.py +++ b/src/macaron/__main__.py @@ -23,6 +23,8 @@ from macaron.slsa_analyzer.analyzer import Analyzer from macaron.slsa_analyzer.git_service import GIT_SERVICES from macaron.slsa_analyzer.package_registry import PACKAGE_REGISTRIES +from macaron.slsa_analyzer.provenance.intoto.errors import LoadIntotoAttestationError +from macaron.slsa_analyzer.provenance.loader import load_provenance_payload from macaron.vsa.vsa import generate_vsa logger: logging.Logger = logging.getLogger(__name__) @@ -78,7 +80,6 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None analyzer.reporters.append(JSONReporter()) run_config = {} - if analyzer_single_args.config_path: # Get user config from yaml file loaded_config = YamlLoader.load(analyzer_single_args.config_path) @@ -129,7 +130,20 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None "dependencies": [], } - status_code = analyzer.run(run_config, analyzer_single_args.sbom_path, analyzer_single_args.skip_deps) + prov_payload = None + if analyzer_single_args.provenance_file: + try: + prov_payload = load_provenance_payload(analyzer_single_args.provenance_file) + except LoadIntotoAttestationError as error: + logger.error("Error while loading the input provenance file: %s", error) + sys.exit(os.EX_DATAERR) + + status_code = analyzer.run( + run_config, + analyzer_single_args.sbom_path, + analyzer_single_args.skip_deps, + prov_payload=prov_payload, + ) sys.exit(status_code) @@ -365,6 +379,13 @@ def main(argv: list[str] | None = None) -> None: help=("The path to provenance expectation file or directory."), ) + single_analyze_parser.add_argument( + "-pf", + "--provenance-file", + required=False, + help=("The path to the provenance file in in-toto format."), + ) + group.add_argument( "-c", "--config-path", diff --git a/src/macaron/slsa_analyzer/analyze_context.py b/src/macaron/slsa_analyzer/analyze_context.py index a5f4ed8e8..9bb225e52 100644 --- a/src/macaron/slsa_analyzer/analyze_context.py +++ b/src/macaron/slsa_analyzer/analyze_context.py @@ -17,6 +17,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 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 @@ -31,17 +32,19 @@ class ChecksOutputs(TypedDict): """Data computed at runtime by checks.""" git_service: BaseGitService - """The git service information for this repository.""" + """The git service information for the target software component.""" build_spec: BuildSpec - """The build spec inferred for this repository.""" + """The build spec inferred for the target software component.""" ci_services: list[CIInfo] - """The CI services information for this repository.""" + """The CI services information for the target software component.""" is_inferred_prov: bool """True if we cannot find the provenance and Macaron need to infer the provenance.""" expectation: Expectation | None - """The expectation to verify the provenance for this repository.""" + """The expectation to verify the provenance for the target software component.""" package_registries: list[PackageRegistryInfo] - """The package registries for this repository.""" + """The package registries for the target software component.""" + provenance: InTotoPayload | None + """The provenance payload for the target software component.""" class AnalyzeContext: @@ -92,6 +95,7 @@ def __init__( package_registries=[], is_inferred_prov=True, expectation=None, + provenance=None, ) @property diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index df500a21e..7eab59b43 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -40,7 +40,7 @@ from macaron.slsa_analyzer.git_service.base_git_service import NoneGitService from macaron.slsa_analyzer.package_registry import PACKAGE_REGISTRIES from macaron.slsa_analyzer.provenance.expectations.expectation_registry import ExpectationRegistry -from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload +from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV01Payload from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.registry import registry from macaron.slsa_analyzer.specs.ci_spec import CIInfo @@ -111,7 +111,13 @@ def __init__(self, output_path: str, build_log_path: str) -> None: # Create database tables: all checks have been registered so all tables should be mapped now self.db_man.create_tables() - def run(self, user_config: dict, sbom_path: str = "", skip_deps: bool = False) -> int: + def run( + self, + user_config: dict, + sbom_path: str = "", + skip_deps: bool = False, + prov_payload: InTotoPayload | None = None, + ) -> int: """Run the analysis and write results to the output path. This method handles the configuration file and writes the result html reports including dependencies. @@ -125,6 +131,8 @@ def run(self, user_config: dict, sbom_path: str = "", skip_deps: bool = False) - The path to the SBOM. skip_deps : bool Flag to skip dependency resolution. + prov_payload : InToToPayload | None + The provenance intoto payload for the main software component. Returns ------- @@ -154,7 +162,11 @@ def run(self, user_config: dict, sbom_path: str = "", skip_deps: bool = False) - ) # Analyze the main target. - main_record = self.run_single(main_config, analysis) + main_record = self.run_single( + main_config, + analysis, + prov_payload=prov_payload, + ) if main_record.status != SCMStatus.AVAILABLE or not main_record.context: logger.info("Analysis has failed.") @@ -255,6 +267,7 @@ def run_single( config: Configuration, analysis: Analysis, existing_records: dict[str, Record] | None = None, + prov_payload: InTotoPayload | None = None, ) -> Record: """Run the checks for a single repository target. @@ -269,6 +282,8 @@ def run_single( The current analysis instance. existing_records : dict[str, Record] | None The mapping of existing records that the analysis has run successfully. + prov_payload : InToToPayload | None + The provenance intoto payload for the analyzed software component. Returns ------- @@ -306,6 +321,7 @@ def run_single( analyze_ctx.dynamic_data["expectation"] = self.expectations.get_expectation_for_target( analyze_ctx.component.purl.split("@")[0] ) + analyze_ctx.dynamic_data["provenance"] = prov_payload analyze_ctx.check_results = self.perform_checks(analyze_ctx) return Record(