diff --git a/scripts/dev_scripts/integration_tests.sh b/scripts/dev_scripts/integration_tests.sh index ee1e60dfb..11fdb61dc 100755 --- a/scripts/dev_scripts/integration_tests.sh +++ b/scripts/dev_scripts/integration_tests.sh @@ -90,6 +90,15 @@ python $COMPARE_DEPS $DEP_RESULT $DEP_EXPECTED || log_fail python $COMPARE_JSON_OUT $JSON_RESULT $JSON_EXPECTED || log_fail +echo "timyarkov/docker_test: Analyzing the repo path, the branch name and the commit digest" +echo "when automatic dependency resolution is skipped, for a project using docker as a build tool." +echo -e "----------------------------------------------------------------------------------\n" +JSON_EXPECTED=$WORKSPACE/tests/e2e/expected_results/docker_test/docker_test.json +JSON_RESULT=$WORKSPACE/output/reports/github.com/timyarkov/docker_test/docker_test.json +$RUN_MACARON analyze -rp https://github.com/timyarkov/docker_test -b main -d 404a51a2f38c4470af6b32e4e00b5318c2d7c0cc --skip-deps || log_fail + +python $COMPARE_JSON_OUT $JSON_RESULT $JSON_EXPECTED || log_fail + echo -e "\n----------------------------------------------------------------------------------" echo "apache/maven: Analyzing with PURL and repository path without dependency resolution." echo -e "----------------------------------------------------------------------------------\n" diff --git a/src/macaron/config/defaults.ini b/src/macaron/config/defaults.ini index c0a1464c6..7170394e5 100644 --- a/src/macaron/config/defaults.ini +++ b/src/macaron/config/defaults.ini @@ -300,6 +300,24 @@ deploy_arg = [builder.poetry.ci.deploy] github_actions = pypa/gh-action-pypi-publish +# This is the spec for trusted Docker build tool usages. +[builder.docker] +entry_conf = +# Also account for if there's multiple dockerfiles, e.g. test.Dockerfile +build_configs = + Dockerfile + Dockerfile.* + *.Dockerfile +builder = + docker +build_arg = + build +deploy_arg = + push +[builder.docker.ci.deploy] +github_actions = + docker/build-push-action + # This is the spec for GitHub Actions CI. [ci.github_actions] entry_conf = diff --git a/src/macaron/slsa_analyzer/build_tool/__init__.py b/src/macaron/slsa_analyzer/build_tool/__init__.py index c80581919..b9a45e500 100644 --- a/src/macaron/slsa_analyzer/build_tool/__init__.py +++ b/src/macaron/slsa_analyzer/build_tool/__init__.py @@ -4,6 +4,7 @@ """The build_tool package contains the supported build tools for Macaron.""" from .base_build_tool import BaseBuildTool +from .docker import Docker from .gradle import Gradle from .maven import Maven from .pip import Pip @@ -11,4 +12,4 @@ # The list of supported build tools. The order of the list determine the order # in which each build tool is checked against the target repository. -BUILD_TOOLS: list[BaseBuildTool] = [Gradle(), Maven(), Poetry(), Pip()] +BUILD_TOOLS: list[BaseBuildTool] = [Gradle(), Maven(), Poetry(), Pip(), Docker()] diff --git a/src/macaron/slsa_analyzer/build_tool/docker.py b/src/macaron/slsa_analyzer/build_tool/docker.py new file mode 100644 index 000000000..3bf816b49 --- /dev/null +++ b/src/macaron/slsa_analyzer/build_tool/docker.py @@ -0,0 +1,92 @@ +# Copyright (c) 2023 - 2023, 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 Docker class which inherits BaseBuildTool. + +This module is used to work with repositories that use Docker as a build tool. +""" + +from macaron.config.defaults import defaults +from macaron.dependency_analyzer.dependency_resolver import NoneDependencyAnalyzer +from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool, file_exists + + +class Docker(BaseBuildTool): + """This class contains the information of Docker when used as a build tool.""" + + def __init__(self) -> None: + """Initialize instance.""" + super().__init__(name="docker") + + def load_defaults(self) -> None: + """Load the default values from defaults.ini.""" + if "builder.docker" in defaults: + for item in defaults["builder.docker"]: + if hasattr(self, item): + setattr(self, item, defaults.get_list("builder.docker", item)) + + if "builder.docker.ci.deploy" in defaults: + for item in defaults["builder.docker.ci.deploy"]: + if item in self.ci_deploy_kws: + self.ci_deploy_kws[item] = defaults.get_list("builder.docker.ci.deploy", item) + + def is_detected(self, repo_path: str) -> bool: + """Return True if this build tool is used in the target repo. + + Parameters + ---------- + repo_path : str + The path to the target repo. + + Returns + ------- + bool + True if this build tool is detected, else False. + """ + for file in self.build_configs: + if file_exists(repo_path, file): + return True + + return False + + def prepare_config_files(self, wrapper_path: str, build_dir: str) -> bool: + """Make necessary preparations for using this build tool. + + Parameters + ---------- + wrapper_path : str + The path where all necessary wrapper files are located. + build_dir : str + The path of the build dir. This is where all files are copied to. + + Returns + ------- + bool + True if succeed else False. + """ + # TODO: Future dependency analysis may require some preprocessing, e.g. + # saving images to tar files. Need to investigate when implementing + # and work with this method accordingly. + + return False + + def get_dep_analyzer(self, repo_path: str) -> NoneDependencyAnalyzer: + """Create a DependencyAnalyzer for the Docker build tool. Currently unimplemented. + + Parameters + ---------- + repo_path: str + The path to the target repo. + + Returns + ------- + NoneDependencyAnalyser + The NoneDependencyAnalyser object. + + Raises + ------ + DependencyAnalyzerError + """ + # TODO: Find a suitable tool to analyse dependencies; as of now Syft + # seems to be a good option, but need to experiment. + return NoneDependencyAnalyzer() diff --git a/tests/conftest.py b/tests/conftest.py index 93657809b..fdf247cd4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ from macaron.config.defaults import create_defaults, defaults, load_defaults from macaron.database.table_definitions import Analysis, Component, Repository from macaron.slsa_analyzer.analyze_context import AnalyzeContext +from macaron.slsa_analyzer.build_tool.docker import Docker from macaron.slsa_analyzer.build_tool.gradle import Gradle from macaron.slsa_analyzer.build_tool.maven import Maven from macaron.slsa_analyzer.build_tool.pip import Pip @@ -148,6 +149,25 @@ def pip_tool(setup_test) -> Pip: # type: ignore # pylint: disable=unused-argume return pip +@pytest.fixture(autouse=True) +def docker_tool(setup_test) -> Docker: # type: ignore # pylint: disable=unused-argument + """Create a Docker tool instance. + + Parameters + ---------- + setup_test + Depends on setup_test fixture. + + Returns + ------- + Docker + The Docker instance. + """ + docker = Docker() + docker.load_defaults() + return docker + + class MockGitHubActions(GitHubActions): """Mock the GitHubActions class.""" diff --git a/tests/e2e/expected_results/docker_test/docker_test.json b/tests/e2e/expected_results/docker_test/docker_test.json new file mode 100644 index 000000000..2ded272c1 --- /dev/null +++ b/tests/e2e/expected_results/docker_test/docker_test.json @@ -0,0 +1,234 @@ +{ + "metadata": { + "timestamps": "2023-08-28 12:52:37" + }, + "target": { + "info": { + "full_name": "pkg:github.com/timyarkov/docker_test@404a51a2f38c4470af6b32e4e00b5318c2d7c0cc", + "local_cloned_path": "git_repos/github.com/timyarkov/docker_test", + "remote_path": "https://github.com/timyarkov/docker_test", + "branch": "main", + "commit_hash": "404a51a2f38c4470af6b32e4e00b5318c2d7c0cc", + "commit_date": "2023-08-13T16:24:13+10:00" + }, + "provenances": { + "is_inferred": true, + "content": { + "github_actions": [ + { + "_type": "https://in-toto.io/Statement/v0.1", + "subject": [], + "predicateType": "https://slsa.dev/provenance/v0.2", + "predicate": { + "builder": { + "id": "https://github.com/timyarkov/docker_test/blob/404a51a2f38c4470af6b32e4e00b5318c2d7c0cc/.github/workflows/github-actions-basic.yml" + }, + "buildType": "Custom github_actions", + "invocation": { + "configSource": { + "uri": "https://github.com/timyarkov/docker_test@refs/heads/main", + "digest": { + "sha1": "404a51a2f38c4470af6b32e4e00b5318c2d7c0cc" + }, + "entryPoint": "https://github.com/timyarkov/docker_test/blob/404a51a2f38c4470af6b32e4e00b5318c2d7c0cc/.github/workflows/github-actions-basic.yml" + }, + "parameters": {}, + "environment": {} + }, + "buildConfig": {}, + "metadata": { + "buildInvocationId": "", + "buildStartedOn": "", + "buildFinishedOn": "", + "completeness": { + "parameters": "false", + "environment": "false", + "materials": "false" + }, + "reproducible": "false" + }, + "materials": [ + { + "uri": "", + "digest": {} + } + ] + } + } + ] + } + }, + "checks": { + "summary": { + "DISABLED": 0, + "FAILED": 5, + "PASSED": 4, + "SKIPPED": 0, + "UNKNOWN": 0 + }, + "results": [ + { + "check_id": "mcn_build_as_code_1", + "check_description": "The build definition and configuration executed by the build service is verifiably derived from text file definitions stored in a version control system.", + "slsa_requirements": [ + "Build as code - SLSA Level 3" + ], + "justification": [ + { + "The target repository uses build tool docker to deploy": "https://github.com/timyarkov/docker_test/blob/404a51a2f38c4470af6b32e4e00b5318c2d7c0cc/.github/workflows/github-actions-basic.yml", + "The build is triggered by": "https://github.com/timyarkov/docker_test/blob/404a51a2f38c4470af6b32e4e00b5318c2d7c0cc/.github/workflows/github-actions-basic.yml" + }, + "Deploy command: ['docker', 'push', 'mock_proj']", + "However, could not find a passing workflow run." + ], + "result_type": "PASSED" + }, + { + "check_id": "mcn_build_script_1", + "check_description": "Check if the target repo has a valid build script.", + "slsa_requirements": [ + "Scripted Build - SLSA Level 1" + ], + "justification": [ + "Check mcn_build_script_1 is set to PASSED because mcn_build_service_1 PASSED." + ], + "result_type": "PASSED" + }, + { + "check_id": "mcn_build_service_1", + "check_description": "Check if the target repo has a valid build service.", + "slsa_requirements": [ + "Build service - SLSA Level 2" + ], + "justification": [ + "Check mcn_build_service_1 is set to PASSED because mcn_build_as_code_1 PASSED." + ], + "result_type": "PASSED" + }, + { + "check_id": "mcn_version_control_system_1", + "check_description": "Check whether the target repo uses a version control system.", + "slsa_requirements": [ + "Version controlled - SLSA Level 2" + ], + "justification": [ + { + "This is a Git repository": "https://github.com/timyarkov/docker_test" + } + ], + "result_type": "PASSED" + }, + { + "check_id": "mcn_provenance_available_1", + "check_description": "Check whether the target has intoto provenance.", + "slsa_requirements": [ + "Provenance - Available - SLSA Level 1", + "Provenance content - Identifies build instructions - SLSA Level 1", + "Provenance content - Identifies artifacts - SLSA Level 1", + "Provenance content - Identifies builder - SLSA Level 1" + ], + "justification": [ + "Could not find any SLSA provenances." + ], + "result_type": "FAILED" + }, + { + "check_id": "mcn_provenance_expectation_1", + "check_description": "Check whether the SLSA provenance for the produced artifact conforms to the expected value.", + "slsa_requirements": [ + "Provenance conforms with expectations - SLSA Level 3" + ], + "justification": [ + "Check mcn_provenance_expectation_1 is set to FAILED because mcn_provenance_available_1 FAILED." + ], + "result_type": "FAILED" + }, + { + "check_id": "mcn_provenance_level_three_1", + "check_description": "Check whether the target has SLSA provenance level 3.", + "slsa_requirements": [ + "Provenance - Non falsifiable - SLSA Level 3", + "Provenance content - Includes all build parameters - SLSA Level 3", + "Provenance content - Identifies entry point - SLSA Level 3", + "Provenance content - Identifies source code - SLSA Level 2" + ], + "justification": [ + "Check mcn_provenance_level_three_1 is set to FAILED because mcn_provenance_available_1 FAILED." + ], + "result_type": "FAILED" + }, + { + "check_id": "mcn_provenance_witness_level_one_1", + "check_description": "Check whether the target has a level-1 witness provenance.", + "slsa_requirements": [ + "Provenance - Available - SLSA Level 1", + "Provenance content - Identifies build instructions - SLSA Level 1", + "Provenance content - Identifies artifacts - SLSA Level 1", + "Provenance content - Identifies builder - SLSA Level 1" + ], + "justification": [ + "Check mcn_provenance_witness_level_one_1 is set to FAILED because mcn_provenance_available_1 FAILED." + ], + "result_type": "FAILED" + }, + { + "check_id": "mcn_trusted_builder_level_three_1", + "check_description": "Check whether the target uses a trusted SLSA level 3 builder.", + "slsa_requirements": [ + "Hermetic - SLSA Level 4", + "Isolated - SLSA Level 3", + "Parameterless - SLSA Level 4", + "Ephemeral environment - SLSA Level 3" + ], + "justification": [ + "Could not find a trusted level 3 builder as a GitHub Actions workflow." + ], + "result_type": "FAILED" + } + ] + } + }, + "dependencies": { + "analyzed_deps": 0, + "unique_dep_repos": 0, + "checks_summary": [ + { + "check_id": "mcn_build_script_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_version_control_system_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_build_service_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_trusted_builder_level_three_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_provenance_witness_level_one_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_provenance_available_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_build_as_code_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_provenance_level_three_1", + "num_deps_pass": 0 + }, + { + "check_id": "mcn_provenance_expectation_1", + "num_deps_pass": 0 + } + ], + "dep_status": [] + } +} diff --git a/tests/slsa_analyzer/build_tool/__snapshots__/test_docker.ambr b/tests/slsa_analyzer/build_tool/__snapshots__/test_docker.ambr new file mode 100644 index 000000000..4a6064ad0 --- /dev/null +++ b/tests/slsa_analyzer/build_tool/__snapshots__/test_docker.ambr @@ -0,0 +1,25 @@ +# serializer version: 1 +# name: test_get_build_dirs[mock_repo0] + list([ + PosixPath('.'), + ]) +# --- +# name: test_get_build_dirs[mock_repo1] + list([ + PosixPath('project'), + ]) +# --- +# name: test_get_build_dirs[mock_repo2] + list([ + PosixPath('.'), + ]) +# --- +# name: test_get_build_dirs[mock_repo3] + list([ + PosixPath('.'), + ]) +# --- +# name: test_get_build_dirs[mock_repo4] + list([ + ]) +# --- diff --git a/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/nested_dockerfile/project/Dockerfile b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/nested_dockerfile/project/Dockerfile new file mode 100644 index 000000000..b5d51ea3b --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/nested_dockerfile/project/Dockerfile @@ -0,0 +1,7 @@ +# Copyright (c) 2023 - 2023, 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/. + +# syntax=docker/dockerfile:1 + +FROM node:18-alpine +CMD ["echo", "Hello!"] diff --git a/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/no_docker/dummy_file.txt b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/no_docker/dummy_file.txt new file mode 100644 index 000000000..19aeac023 --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/no_docker/dummy_file.txt @@ -0,0 +1,2 @@ +# Copyright (c) 2023 - 2023, 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/. diff --git a/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_dockerfile/Dockerfile b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_dockerfile/Dockerfile new file mode 100644 index 000000000..b5d51ea3b --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_dockerfile/Dockerfile @@ -0,0 +1,7 @@ +# Copyright (c) 2023 - 2023, 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/. + +# syntax=docker/dockerfile:1 + +FROM node:18-alpine +CMD ["echo", "Hello!"] diff --git a/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_dockerfile_wildcard/Dockerfile.final b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_dockerfile_wildcard/Dockerfile.final new file mode 100644 index 000000000..b5d51ea3b --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_dockerfile_wildcard/Dockerfile.final @@ -0,0 +1,7 @@ +# Copyright (c) 2023 - 2023, 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/. + +# syntax=docker/dockerfile:1 + +FROM node:18-alpine +CMD ["echo", "Hello!"] diff --git a/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_wildcard_dockerfile/final.Dockerfile b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_wildcard_dockerfile/final.Dockerfile new file mode 100644 index 000000000..b5d51ea3b --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/docker_repos/root_wildcard_dockerfile/final.Dockerfile @@ -0,0 +1,7 @@ +# Copyright (c) 2023 - 2023, 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/. + +# syntax=docker/dockerfile:1 + +FROM node:18-alpine +CMD ["echo", "Hello!"] diff --git a/tests/slsa_analyzer/build_tool/test_docker.py b/tests/slsa_analyzer/build_tool/test_docker.py new file mode 100644 index 000000000..77a764856 --- /dev/null +++ b/tests/slsa_analyzer/build_tool/test_docker.py @@ -0,0 +1,43 @@ +# Copyright (c) 2023 - 2023, 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 tests the Docker build functions.""" + +from pathlib import Path + +import pytest + +from macaron.slsa_analyzer.build_tool.docker import Docker +from tests.slsa_analyzer.mock_git_utils import prepare_repo_for_testing + + +@pytest.mark.parametrize( + "mock_repo", + [ + Path(__file__).parent.joinpath("mock_repos", "docker_repos", "root_dockerfile"), + Path(__file__).parent.joinpath("mock_repos", "docker_repos", "nested_dockerfile"), + Path(__file__).parent.joinpath("mock_repos", "docker_repos", "root_wildcard_dockerfile"), + Path(__file__).parent.joinpath("mock_repos", "docker_repos", "root_dockerfile_wildcard"), + Path(__file__).parent.joinpath("mock_repos", "docker_repos", "no_docker"), + ], +) +def test_get_build_dirs(snapshot: list, docker_tool: Docker, mock_repo: Path) -> None: + """Test discovering build directories.""" + assert list(docker_tool.get_build_dirs(str(mock_repo))) == snapshot + + +@pytest.mark.parametrize( + ("mock_repo", "expected_value"), + [ + (Path(__file__).parent.joinpath("mock_repos", "docker_repos", "root_dockerfile"), True), + (Path(__file__).parent.joinpath("mock_repos", "docker_repos", "nested_dockerfile"), True), + (Path(__file__).parent.joinpath("mock_repos", "docker_repos", "root_wildcard_dockerfile"), True), + (Path(__file__).parent.joinpath("mock_repos", "docker_repos", "root_dockerfile_wildcard"), True), + (Path(__file__).parent.joinpath("mock_repos", "docker_repos", "no_docker"), False), + ], +) +def test_docker_build_tool(docker_tool: Docker, macaron_path: str, mock_repo: str, expected_value: bool) -> None: + """Test the Docker build tool.""" + base_dir = Path(__file__).parent + ctx = prepare_repo_for_testing(mock_repo, macaron_path, base_dir) + assert docker_tool.is_detected(ctx.component.repository.fs_path) == expected_value