From 6ccde3e6adf0087711da5852e534738e697518e4 Mon Sep 17 00:00:00 2001 From: Tim Yarkov Date: Thu, 7 Sep 2023 18:13:40 +1000 Subject: [PATCH] feat: separate out npm and yarn Signed-off-by: Tim Yarkov --- scripts/dev_scripts/integration_tests.sh | 10 + src/macaron/config/defaults.ini | 14 + .../slsa_analyzer/build_tool/__init__.py | 8 +- src/macaron/slsa_analyzer/build_tool/npm.py | 2 +- src/macaron/slsa_analyzer/build_tool/yarn.py | 90 +++++++ tests/conftest.py | 20 ++ tests/e2e/expected_results/yoga/yoga.json | 248 ++++++++++++++++++ .../build_tool/__snapshots__/test_yarn.ambr | 20 ++ .../mock_repos/yarn_repos/nested_package | 1 + .../mock_repos/yarn_repos/no_package | 1 + .../mock_repos/yarn_repos/root_package | 1 + .../yarn_repos/root_package_packagelock | 1 + tests/slsa_analyzer/build_tool/test_yarn.py | 41 +++ 13 files changed, 452 insertions(+), 5 deletions(-) create mode 100644 src/macaron/slsa_analyzer/build_tool/yarn.py create mode 100644 tests/e2e/expected_results/yoga/yoga.json create mode 100644 tests/slsa_analyzer/build_tool/__snapshots__/test_yarn.ambr create mode 160000 tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/nested_package create mode 160000 tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/no_package create mode 160000 tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package create mode 160000 tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package_packagelock create mode 100644 tests/slsa_analyzer/build_tool/test_yarn.py diff --git a/scripts/dev_scripts/integration_tests.sh b/scripts/dev_scripts/integration_tests.sh index 12d8becbd..5bd045498 100755 --- a/scripts/dev_scripts/integration_tests.sh +++ b/scripts/dev_scripts/integration_tests.sh @@ -100,6 +100,16 @@ $RUN_MACARON analyze -rp https://github.com/uiv-lib/uiv -b dev -d 057b25b4db0913 python $COMPARE_JSON_OUT $JSON_RESULT $JSON_EXPECTED || log_fail +echo -e "\n----------------------------------------------------------------------------------" +echo "facebook/yoga: Analysing the repo path, the branch name and the commit digest for an NPM project," +echo "skipping dependency resolution." +echo -e "----------------------------------------------------------------------------------\n" +JSON_EXPECTED=$WORKSPACE/tests/e2e/expected_results/yoga/yoga.json +JSON_RESULT=$WORKSPACE/output/reports/github_com/facebook/facebook/yoga.json +$RUN_MACARON analyze -rp https://github.com/facebook/yoga -b main -d f8e2bc0875c145c429d0e865c9b83a40f65b3070 --skip-deps || log_fail + +python $COMPARE_JSON_OUT $JSON_RESULT $JSON_EXPECTED || log_fail + echo -e "\n----------------------------------------------------------------------------------" echo "sigstore/sget: Analysing the repo path, the branch name and the" echo "commit digest for a Go project, skipping dependency resolution." diff --git a/src/macaron/config/defaults.ini b/src/macaron/config/defaults.ini index b87736980..7860f836a 100644 --- a/src/macaron/config/defaults.ini +++ b/src/macaron/config/defaults.ini @@ -319,6 +319,20 @@ deploy_arg = github_actions = JS-DevTools/npm-publish +[builder.yarn] +entry_conf = + .yarnrc +build_configs = + package.json +package_lock = + package-lock.json +builder = + yarn +build_arg = + build +deploy_arg = + publish + [builder.go] entry_conf = build_configs = diff --git a/src/macaron/slsa_analyzer/build_tool/__init__.py b/src/macaron/slsa_analyzer/build_tool/__init__.py index 052d3f673..941725106 100644 --- a/src/macaron/slsa_analyzer/build_tool/__init__.py +++ b/src/macaron/slsa_analyzer/build_tool/__init__.py @@ -3,15 +3,15 @@ """The build_tool package contains the supported build tools for Macaron.""" -from macaron.slsa_analyzer.build_tool.go import Go -from macaron.slsa_analyzer.build_tool.npm import NPM - from .base_build_tool import BaseBuildTool +from .go import Go from .gradle import Gradle from .maven import Maven +from .npm import NPM from .pip import Pip from .poetry import Poetry +from .yarn import Yarn # 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(), NPM(), Go()] +BUILD_TOOLS: list[BaseBuildTool] = [Gradle(), Maven(), Poetry(), Pip(), NPM(), Yarn(), Go()] diff --git a/src/macaron/slsa_analyzer/build_tool/npm.py b/src/macaron/slsa_analyzer/build_tool/npm.py index 04037ca27..fd2bf2592 100644 --- a/src/macaron/slsa_analyzer/build_tool/npm.py +++ b/src/macaron/slsa_analyzer/build_tool/npm.py @@ -3,7 +3,7 @@ """This module contains the NPM class which inherits BaseBuildTool. -This module is used to work with repositories that use NPM or Yarn as its +This module is used to work with repositories that use NPM as its build tool. """ diff --git a/src/macaron/slsa_analyzer/build_tool/yarn.py b/src/macaron/slsa_analyzer/build_tool/yarn.py new file mode 100644 index 000000000..0076561e3 --- /dev/null +++ b/src/macaron/slsa_analyzer/build_tool/yarn.py @@ -0,0 +1,90 @@ +# 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 Yarn class which inherits BaseBuildTool. + +This module is used to work with repositories that use Yarn as its +build tool. +""" + +from macaron.config.defaults import defaults +from macaron.dependency_analyzer.dependency_resolver import DependencyAnalyzer, NoneDependencyAnalyzer +from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool, file_exists + + +class Yarn(BaseBuildTool): + """This class contains the information of the yarn build tool.""" + + def __init__(self) -> None: + super().__init__(name="yarn") + + def load_defaults(self) -> None: + """Load the default values from defaults.ini.""" + if "builder.yarn" in defaults: + for item in defaults["builder.yarn"]: + if hasattr(self, item): + setattr(self, item, defaults.get_list("builder.yarn", item)) + + # TODO: Find a suitable github action for Yarn + # if "builder.yarn.ci.deploy" in defaults: + # for item in defaults["builder.yarn.ci.deploy"]: + # if item in self.ci_deploy_kws: + # self.ci_deploy_kws[item] = defaults.get_list("builder.yarn.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. + """ + # TODO: When more complex build detection is being implemented, consider + # cases like .yarnrc existing but not package-lock.json and whether + # they would still count as "detected" + yarn_config_files = self.build_configs + self.package_lock + self.entry_conf + for file in yarn_config_files: + if file_exists(repo_path, file): + return True + + return False + + def prepare_config_files(self, wrapper_path: str, build_dir: str) -> bool: + """Prepare the necessary wrapper files for running the build. + + yarn doesn't require preparation, so return true. + + 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. + """ + return True + + def get_dep_analyzer(self, repo_path: str) -> DependencyAnalyzer: + """Create a DependencyAnalyzer for the build tool. + + Parameters + ---------- + repo_path: str + The path to the target repo. + + Returns + ------- + DependencyAnalyzer + The DependencyAnalyzer object. + """ + # TODO: Implement this method. + return NoneDependencyAnalyzer() diff --git a/tests/conftest.py b/tests/conftest.py index 528760029..0a15c1ee4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ from macaron.slsa_analyzer.build_tool.npm import NPM from macaron.slsa_analyzer.build_tool.pip import Pip from macaron.slsa_analyzer.build_tool.poetry import Poetry +from macaron.slsa_analyzer.build_tool.yarn import Yarn from macaron.slsa_analyzer.ci_service.circleci import CircleCI from macaron.slsa_analyzer.ci_service.github_actions import GitHubActions from macaron.slsa_analyzer.ci_service.gitlab_ci import GitLabCI @@ -169,6 +170,25 @@ def npm_tool(setup_test) -> NPM: # type: ignore # pylint: disable=unused-argume return npm +@pytest.fixture(autouse=True) +def yarn_tool(setup_test) -> Yarn: # type: ignore # pylint: disable=unused-argument + """Create a Yarn tool instance. + + Parameters + ---------- + setup_test + Depends on setup_test fixture. + + Returns + ------- + Yarn + The Yarn instance. + """ + yarn = Yarn() + yarn.load_defaults() + return yarn + + @pytest.fixture(autouse=True) def go_tool(setup_test) -> Go: # type: ignore # pylint: disable=unused-argument """Create a Go tool instance. diff --git a/tests/e2e/expected_results/yoga/yoga.json b/tests/e2e/expected_results/yoga/yoga.json new file mode 100644 index 000000000..9197a9fca --- /dev/null +++ b/tests/e2e/expected_results/yoga/yoga.json @@ -0,0 +1,248 @@ +{ + "metadata": { + "timestamps": "2023-09-07 17:53:52" + }, + "target": { + "info": { + "full_name": "pkg:github.com/facebook/yoga@f8e2bc0875c145c429d0e865c9b83a40f65b3070", + "local_cloned_path": "git_repos/github_com/facebook/yoga", + "remote_path": "https://github.com/facebook/yoga", + "branch": "main", + "commit_hash": "f8e2bc0875c145c429d0e865c9b83a40f65b3070", + "commit_date": "2023-09-06T09:50:43-07: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/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-npm-release.yml" + }, + "buildType": "Custom github_actions", + "invocation": { + "configSource": { + "uri": "https://github.com/facebook/yoga@refs/heads/main", + "digest": { + "sha1": "f8e2bc0875c145c429d0e865c9b83a40f65b3070" + }, + "entryPoint": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-npm-release.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 gradle to deploy": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-android-snashot.yml", + "The build is triggered by": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-android-snashot.yml" + }, + "Deploy command: ['./gradlew', 'publishToSonatype']", + { + "The status of the build can be seen at": "https://github.com/facebook/yoga/actions/runs/6100054904" + }, + { + "The target repository uses build tool npm to deploy": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-npm-release.yml", + "The build is triggered by": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-npm-release.yml" + }, + "Deploy command: ['yarn', 'publish']", + "However, could not find a passing workflow run.", + { + "The target repository uses build tool yarn to deploy": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-npm-release.yml", + "The build is triggered by": "https://github.com/facebook/yoga/blob/f8e2bc0875c145c429d0e865c9b83a40f65b3070/.github/workflows/publish-npm-release.yml" + }, + "Deploy command: ['yarn', 'publish']", + "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/facebook/yoga" + } + ], + "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_yarn.ambr b/tests/slsa_analyzer/build_tool/__snapshots__/test_yarn.ambr new file mode 100644 index 000000000..83dd8ecaf --- /dev/null +++ b/tests/slsa_analyzer/build_tool/__snapshots__/test_yarn.ambr @@ -0,0 +1,20 @@ +# serializer version: 1 +# name: test_get_build_dirs[mock_repo0] + list([ + PosixPath('.'), + ]) +# --- +# name: test_get_build_dirs[mock_repo1] + list([ + PosixPath('.'), + ]) +# --- +# name: test_get_build_dirs[mock_repo2] + list([ + PosixPath('project'), + ]) +# --- +# name: test_get_build_dirs[mock_repo3] + list([ + ]) +# --- diff --git a/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/nested_package b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/nested_package new file mode 160000 index 000000000..7de9df23c --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/nested_package @@ -0,0 +1 @@ +Subproject commit 7de9df23c8272aeeb395cf16a2202905c9cf20a8 diff --git a/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/no_package b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/no_package new file mode 160000 index 000000000..41ae078b7 --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/no_package @@ -0,0 +1 @@ +Subproject commit 41ae078b78da16eba7922f6c580086cec1731ab5 diff --git a/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package new file mode 160000 index 000000000..26e96d12e --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package @@ -0,0 +1 @@ +Subproject commit 26e96d12e370d86b965a1a7e5476e1e9fd904b74 diff --git a/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package_packagelock b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package_packagelock new file mode 160000 index 000000000..1fa44eba2 --- /dev/null +++ b/tests/slsa_analyzer/build_tool/mock_repos/yarn_repos/root_package_packagelock @@ -0,0 +1 @@ +Subproject commit 1fa44eba248b8b9c5727f196452c9f91a3afa42f diff --git a/tests/slsa_analyzer/build_tool/test_yarn.py b/tests/slsa_analyzer/build_tool/test_yarn.py new file mode 100644 index 000000000..a0498850e --- /dev/null +++ b/tests/slsa_analyzer/build_tool/test_yarn.py @@ -0,0 +1,41 @@ +# 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 Yarn build functions.""" + +from pathlib import Path + +import pytest + +from macaron.slsa_analyzer.build_tool.yarn import Yarn +from tests.slsa_analyzer.mock_git_utils import prepare_repo_for_testing + + +@pytest.mark.parametrize( + "mock_repo", + [ + Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "root_package"), + Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "root_package_packagelock"), + Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "nested_package"), + Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "no_package"), + ], +) +def test_get_build_dirs(snapshot: list, yarn_tool: Yarn, mock_repo: Path) -> None: + """Test discovering build directories.""" + assert list(yarn_tool.get_build_dirs(str(mock_repo))) == snapshot + + +@pytest.mark.parametrize( + ("mock_repo", "expected_value"), + [ + (Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "root_package"), True), + (Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "root_package_packagelock"), True), + (Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "nested_package"), True), + (Path(__file__).parent.joinpath("mock_repos", "yarn_repos", "no_package"), False), + ], +) +def test_yarn_build_tool(yarn_tool: Yarn, macaron_path: str, mock_repo: str, expected_value: bool) -> None: + """Test the yarn build tool.""" + base_dir = Path(__file__).parent + ctx = prepare_repo_for_testing(mock_repo, macaron_path, base_dir) + assert yarn_tool.is_detected(ctx.component.repository.fs_path) == expected_value