diff --git a/docs/source/index.rst b/docs/source/index.rst index 0b7342ac6..e1abecc58 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -53,6 +53,9 @@ the requirements that are currently supported by Macaron. * - Check ID - SLSA requirement - Concrete check + * - ``mcn_build_tool_1`` + - **Build tool exists** - The source code repository includes configurations for a supported build tool used to produce the software component. + - Detect the build tool used in the source code repository to build the software component. * - ``mcn_build_script_1`` - **Scripted build** - All build steps were fully defined in a “build script”. - Identify and validate build script(s). diff --git a/src/macaron/slsa_analyzer/checks/build_tool_check.py b/src/macaron/slsa_analyzer/checks/build_tool_check.py new file mode 100644 index 000000000..8432b014e --- /dev/null +++ b/src/macaron/slsa_analyzer/checks/build_tool_check.py @@ -0,0 +1,85 @@ +# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""This module contains the implementation of the build tool detection check.""" + + +import logging + +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column + +from macaron.database.table_definitions import CheckFacts +from macaron.slsa_analyzer.analyze_context import AnalyzeContext +from macaron.slsa_analyzer.checks.base_check import BaseCheck, CheckResultType +from macaron.slsa_analyzer.checks.check_result import CheckResultData, Confidence, JustificationType +from macaron.slsa_analyzer.registry import registry +from macaron.slsa_analyzer.slsa_req import ReqName + +logger: logging.Logger = logging.getLogger(__name__) + + +class BuildToolFacts(CheckFacts): + """The ORM mapping for the facts collected by the build tool check.""" + + __tablename__ = "_build_tool_check" + + #: The primary key. + id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 + + #: The build tool name. + build_tool_name: Mapped[str] = mapped_column(String, nullable=False, info={"justification": JustificationType.TEXT}) + + #: The language of the artifact built by build tool. + language: Mapped[str] = mapped_column(String, nullable=False, info={"justification": JustificationType.TEXT}) + + __mapper_args__ = { + "polymorphic_identity": "_build_tool_check", + } + + +class BuildToolCheck(BaseCheck): + """This check detects the build tool used in the source code repository to build the software component.""" + + def __init__(self) -> None: + """Initialize instance.""" + check_id = "mcn_build_tool_1" + description = "Detect the build tool used in the source code repository to build the software component." + depends_on: list[tuple[str, CheckResultType]] = [("mcn_version_control_system_1", CheckResultType.PASSED)] + eval_reqs = [ReqName.SCRIPTED_BUILD] + super().__init__(check_id=check_id, description=description, depends_on=depends_on, eval_reqs=eval_reqs) + + def run_check(self, ctx: AnalyzeContext) -> CheckResultData: + """Implement the check in this method. + + Parameters + ---------- + ctx : AnalyzeContext + The object containing processed data for the target repo. + + Returns + ------- + CheckResultData + The result of the check. + """ + if not ctx.component.repository: + logger.info("Unable to find a Git repository for %s", ctx.component.purl) + return CheckResultData(result_tables=[], result_type=CheckResultType.FAILED) + + build_tools = ctx.dynamic_data["build_spec"]["tools"] + if not build_tools: + return CheckResultData(result_tables=[], result_type=CheckResultType.FAILED) + + result_tables: list[CheckFacts] = [] + for tool in build_tools: + result_tables.append( + BuildToolFacts(build_tool_name=tool.name, language=tool.language.value, confidence=Confidence.HIGH) + ) + + return CheckResultData( + result_tables=result_tables, + result_type=CheckResultType.PASSED, + ) + + +registry.register(BuildToolCheck()) diff --git a/src/macaron/slsa_analyzer/registry.py b/src/macaron/slsa_analyzer/registry.py index 065273b0b..3d8b6000f 100644 --- a/src/macaron/slsa_analyzer/registry.py +++ b/src/macaron/slsa_analyzer/registry.py @@ -147,9 +147,10 @@ def _validate_check(check: Any) -> bool: True if check is valid, else False. """ if not isinstance(check, BaseCheck): + class_name = check.__name__ if isinstance(check, type) else type(check).__name__ logger.error( - "The registered Check is of type %s. Please register a child class of BaseCheck.", - type(check).__name__, + "The registered Check %s is not a valid instance of BaseCheck.", + class_name, ) return False diff --git a/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl b/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl index 3848cb503..0d652339e 100644 --- a/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl +++ b/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl @@ -8,6 +8,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_script_1"), check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(yarn_id, "yarn", "javascript"), + check_facts(yarn_id, _, component_id,_,_), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_available_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), diff --git a/tests/integration/cases/google_guava/policy.dl b/tests/integration/cases/google_guava/policy.dl index 84d77bbcc..dddcdea35 100644 --- a/tests/integration/cases/google_guava/policy.dl +++ b/tests/integration/cases/google_guava/policy.dl @@ -12,6 +12,9 @@ Policy("test_policy", component_id, "") :- // the logic in the mcn_infer_artifact_pipeline_1 check. check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(maven_id, "maven", "java"), + check_facts(maven_id, _, component_id,_,_), check_failed(component_id, "mcn_provenance_available_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), diff --git a/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl b/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl index 04a317c83..d045d955b 100644 --- a/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl +++ b/tests/integration/cases/jackson_databind_with_purl_and_no_deps/jackson-databind.dl @@ -8,6 +8,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_script_1"), check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(maven_id, "maven", "java"), + check_facts(maven_id, _, component_id,_,_), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_available_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), diff --git a/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl b/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl index a567df4d0..35e35f7ee 100644 --- a/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl +++ b/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl @@ -10,6 +10,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_version_control_system_1"), check_passed(component_id, "mcn_provenance_available_1"), check_passed(component_id, "mcn_provenance_derived_repo_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(gradle_id, "gradle", "java"), + check_facts(gradle_id, _, component_id,_,_), check_passed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), diff --git a/tests/integration/cases/slsa-framework_slsa-verifier/policy.dl b/tests/integration/cases/slsa-framework_slsa-verifier/policy.dl index 67a171df2..d9ab6910c 100644 --- a/tests/integration/cases/slsa-framework_slsa-verifier/policy.dl +++ b/tests/integration/cases/slsa-framework_slsa-verifier/policy.dl @@ -9,6 +9,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_trusted_builder_level_three_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(go_id, "go", "go"), + check_facts(go_id, _, component_id,_,_), check_passed(component_id, "mcn_provenance_available_1"), check_passed(component_id, "mcn_provenance_derived_commit_1"), check_passed(component_id, "mcn_provenance_derived_repo_1"), diff --git a/tests/integration/cases/timyarkov_docker_test/policy.dl b/tests/integration/cases/timyarkov_docker_test/policy.dl index 3f521e2da..1d8efaec1 100644 --- a/tests/integration/cases/timyarkov_docker_test/policy.dl +++ b/tests/integration/cases/timyarkov_docker_test/policy.dl @@ -8,6 +8,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_script_1"), check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(docker_id, "docker", "docker"), + check_facts(docker_id, _, component_id,_,_), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_available_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), diff --git a/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl b/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl index e185fa8a7..d70078002 100644 --- a/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl +++ b/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl @@ -8,6 +8,11 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_script_1"), check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(gradle_id, "gradle", "java"), + check_facts(gradle_id, _, component_id,_,_), + build_tool_check(maven_id, "maven", "java"), + check_facts(maven_id, _, component_id,_,_), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_available_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), diff --git a/tests/integration/cases/uiv-lib_uiv/policy.dl b/tests/integration/cases/uiv-lib_uiv/policy.dl index e31e0050e..3823d052d 100644 --- a/tests/integration/cases/uiv-lib_uiv/policy.dl +++ b/tests/integration/cases/uiv-lib_uiv/policy.dl @@ -8,6 +8,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_script_1"), check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(npm_id, "npm", "javascript"), + check_facts(npm_id, _, component_id,_,_), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_available_1"), check_failed(component_id, "mcn_provenance_derived_commit_1"), diff --git a/tests/integration/cases/urllib3_expectation_dir/policy.dl b/tests/integration/cases/urllib3_expectation_dir/policy.dl index 048252508..dfa3d0d4a 100644 --- a/tests/integration/cases/urllib3_expectation_dir/policy.dl +++ b/tests/integration/cases/urllib3_expectation_dir/policy.dl @@ -13,6 +13,9 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_derived_commit_1"), check_passed(component_id, "mcn_provenance_derived_repo_1"), check_passed(component_id, "mcn_provenance_expectation_1"), + check_passed(component_id, "mcn_build_tool_1"), + build_tool_check(pip_id, "pip", "python"), + check_facts(pip_id, _, component_id,_,_), check_failed(component_id, "mcn_infer_artifact_pipeline_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), diff --git a/tests/slsa_analyzer/checks/test_build_tool_check.py b/tests/slsa_analyzer/checks/test_build_tool_check.py new file mode 100644 index 000000000..6e79b5227 --- /dev/null +++ b/tests/slsa_analyzer/checks/test_build_tool_check.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""This module contains the tests for the build tool detection Check.""" + +from pathlib import Path + +import pytest + +from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool +from macaron.slsa_analyzer.checks.build_tool_check import BuildToolCheck +from macaron.slsa_analyzer.checks.check_result import CheckResultType +from tests.conftest import MockAnalyzeContext + + +@pytest.mark.parametrize( + "build_tool_name", + [ + "maven", + "gradle", + "poetry", + "pip", + "npm", + "docker", + "go", + ], +) +def test_build_tool_check_pass( + macaron_path: Path, + build_tools: dict[str, BaseBuildTool], + build_tool_name: str, +) -> None: + """Test the build tool detection check passes.""" + ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") + ctx.dynamic_data["build_spec"]["tools"] = [build_tools[build_tool_name]] + check = BuildToolCheck() + assert check.run_check(ctx).result_type == CheckResultType.PASSED + + +def test_build_tool_check_fail( + macaron_path: Path, +) -> None: + """Test the build tool detection check fails.""" + ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") + ctx.dynamic_data["build_spec"]["tools"] = [] + check = BuildToolCheck() + assert check.run_check(ctx).result_type == CheckResultType.FAILED