diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..91355d47a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,60 @@ +--- +name: Bug Report +about: Report a bug or unexpected behavior in Macaron. +title: "[Bug] - [Describe Issue]" +labels: bug, triage +assignees: '' +--- + +### Description +Please provide a clear and concise description of the issue you're experiencing with Macaron. Be as detailed as possible about the problem. + +### Steps to Reproduce +Please list the steps required to reproduce the issue: + +1. **Step 1**: [Describe the first step] +2. **Step 2**: [Describe the second step] +3. **Step 3**: [Describe the third step] +4. [Continue adding steps if necessary] + +### Expected Behavior +What were you expecting to happen? + +### Actual Behavior +What actually happened? Please include any error messages, logs, or unexpected behavior you observed. + +### Debug Information +Please run the command again with the `--verbose` [option](https://oracle.github.io/macaron/pages/cli_usage/index.html#cmdoption-v) to provide debug information. This will help us diagnose the issue more effectively. You can add this option to the command like this: + +```shell +./run_macaron.sh --verbose [other options] +``` + +Attach the debug output here if possible. + +### Environment Information +To assist with troubleshooting, please provide the following information about your environment: + +Operating System: (e.g., Ubuntu 20.04, macOS 11.2) + +CPU architecture information (e.g., x86-64 (AMD64)) + +Bash Version: (Run bash --version to get the version) + +Docker or Podman Version: (Run docker --version to get the version) + +If you are using Macaron as a Python package, please indicate that in your environment details and specify the Python version you are using. + +Macaron version or commit hash where the issue occurs. + +Additional Information: (Any other relevant details, such as hardware or network environment, such as proxies) + +### Screenshots or Logs +If applicable, please provide screenshots or logs that illustrate the bug. + +### Additional Information +Any other information that might be useful to identify or fix the bug. For example: + +Any steps that worked around the issue + +Specific configurations or files that may be relevant diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 000000000..45e9bae94 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,11 @@ +# Copyright (c) 2025 - 2025, 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/. + +blank_issues_enabled: false +contact_links: +- name: GitHub Discussions + url: https://github.com/oracle/macaron/discussions + about: Please ask and answer questions here. +- name: Security Reports + url: https://github.com/oracle/macaron/blob/main/SECURITY.md + about: Please report security vulnerabilities following the instructions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..424b33a62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,16 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement for Macaron. +title: "[Feature Request] - [Describe Feature]" +labels: enhancement, feature +assignees: '' + +--- + +### Description +Please provide a clear and concise description of the feature or enhancement you'd like to see in Macaron. Explain why it would be useful and how it could improve the tool. + +### Proposed Feature +What functionality or feature would you like to add to Macaron? Please describe it in detail. + +### Use Case diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 5173fb977..efc855ce6 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 configuration file enables Dependabot version updates. @@ -16,7 +16,7 @@ updates: prefix-development: chore include: scope open-pull-requests-limit: 13 - target-branch: staging + target-branch: main # Add additional reviewers for PRs opened by Dependabot. For more information, see: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#reviewers # reviewers: @@ -31,7 +31,7 @@ updates: prefix-development: chore include: scope open-pull-requests-limit: 13 - target-branch: staging + target-branch: main # Add additional reviewers for PRs opened by Dependabot. For more information, see: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#reviewers # reviewers: @@ -46,7 +46,7 @@ updates: prefix-development: chore include: scope open-pull-requests-limit: 13 - target-branch: staging + target-branch: main # Add additional reviewers for PRs opened by Dependabot. For more information, see: # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#reviewers # reviewers: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..0d13c2ec0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +## Checklist + + +- [ ] I have reviewed the [contribution guide](../CONTRIBUTING.md). +- [ ] My PR title and commits follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) convention. +- [ ] My commits include the "Signed-off-by" line. +- [ ] I have signed my commits following the instructions provided by [GitHub](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). Note that we run [GitHub's commit verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) tool to check the commit signatures. A green `verified` label should appear next to **all** of your commits on GitHub. +- [ ] I have updated the relevant documentation, if applicable. +- [ ] I have tested my changes and verified they work as expected. +- [ ] I have referenced the issue(s) this pull request solves. diff --git a/.github/workflows/_generate-rebase.yaml b/.github/workflows/_generate-rebase.yaml index db1f7ab1e..052db4735 100644 --- a/.github/workflows/_generate-rebase.yaml +++ b/.github/workflows/_generate-rebase.yaml @@ -1,7 +1,7 @@ -# Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. -# Automatically rebase one staging branch on top of main after a new package version was published. +# Automatically rebase main branch on top of release after a new package version is published. name: Rebase branch on: diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml index 068e2a577..fdf820984 100644 --- a/.github/workflows/codeql-analysis.yaml +++ b/.github/workflows/codeql-analysis.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. # Run CodeQL over the package. For more configuration options see codeql/codeql-config.yaml @@ -9,11 +9,11 @@ on: push: branches: - main - - staging + - release pull_request: branches: - main - - staging + - release schedule: - cron: 20 15 * * 3 permissions: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 74c64a9c8..9702ce5f4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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/. # We run checks on pushing to the specified branches. @@ -9,7 +9,7 @@ on: push: branches: - main - - staging + - release permissions: contents: read env: @@ -28,11 +28,11 @@ jobs: contents: read packages: read - # On pushes to the 'main' branch create a new release by bumping the version + # On pushes to the 'release' branch create a new release by bumping the version # and generating a change log. That's the new bump commit and associated tag. bump: needs: check - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/release' runs-on: ubuntu-latest permissions: contents: write @@ -72,18 +72,18 @@ jobs: git push git push --tags - # After the bump commit was pushed to the main branch, rebase the staging branch - # (to_head argument) on top of the new main branch (from_base argument), to keep + # After the bump commit was pushed to the release branch, rebase the main branch + # (to_head argument) on top of the release branch (from_base argument), to keep # the histories of both branches in sync. - rebase_staging: + rebase_main: needs: [bump] - name: Rebase staging branch on main + name: Rebase main branch on release uses: ./.github/workflows/_generate-rebase.yaml permissions: contents: read with: - to_head: staging - from_base: origin/main + to_head: main + from_base: origin/release git_user_name: behnazh-w git_user_email: behnazh-w@users.noreply.github.com secrets: @@ -91,7 +91,7 @@ jobs: # When triggered by the version bump commit, build the package and publish the release artifacts. build: - if: github.ref == 'refs/heads/main' && startsWith(github.event.commits[0].message, 'bump:') + if: github.ref == 'refs/heads/release' && startsWith(github.event.commits[0].message, 'bump:') uses: ./.github/workflows/_build.yaml permissions: contents: read diff --git a/.gitignore b/.gitignore index c5977c15b..4bc971ba4 100644 --- a/.gitignore +++ b/.gitignore @@ -176,6 +176,7 @@ tests/slsa_analyzer/build_tool/mock_repos/gradle_repos/no_gradle/ tests/slsa_analyzer/build_tool/mock_repos/maven_repos/no_pom/ tests/slsa_analyzer/checks/mock_repos/** tests/slsa_analyzer/ci_service/mock_repos/** +tests/repo_finder/mock_repos/** docs/_build bin/ requirements.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3bb1fe6c2..6cc6516fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,13 +35,13 @@ See our [Macaron Style Guide](./docs/source/pages/developers_guide/style_guide.r 1. Ensure there is an issue created to track and discuss the fix or enhancement you intend to submit. -2. Fork this repository including the `staging` branch. In Macaron, the `staging` branch is the active development branch and contains the most recent changes. -3. Create a branch in your fork to implement the changes. Make sure to create your branch from the `staging` branch and not `main`. We recommend using the issue number as part of your branch name, e.g. `1234-fixes`. +2. Fork this repository. +3. Create a branch in your fork to implement the changes. We recommend using the issue number as part of your branch name, e.g. `1234-fixes`. 4. The title of the PR should follow the convention of [commit messages](#commit-messages). 5. Ensure that any documentation is updated with the changes that are required by your change. 6. Ensure that any samples are updated if the base image has been changed. 7. Submit the pull request. *Do not leave the pull request blank*. Explain exactly what your changes are meant to do and provide simple steps on how to validate. your changes. Ensure that you reference the issue you created as well. -8. Choose `staging` as the base branch for your PR. +8. Choose `main` as the base branch for your PR. 9. We will assign the pull request to 2-3 people for review before it is merged. ### Commit messages @@ -74,7 +74,7 @@ a detailed commit message body is preferred. Make sure to keep the `Signed-off-b ## Branching model -* The `main` branch is only used for releases and the `staging` branch is used for development. We only merge to `main` when we want to create a new release for Macaron. +* The `main` branch should be used as the base branch for pull requests. The `release` branch is designated for releases and should only be merged into when creating a new release for Macaron. ## Setting up the development environment diff --git a/Makefile b/Makefile index 1b97d02fc..029cdc163 100644 --- a/Makefile +++ b/Makefile @@ -134,36 +134,37 @@ $(PACKAGE_PATH)/resources/schemastore/NOTICE: && wget https://raw.githubusercontent.com/SchemaStore/schemastore/a1689388470d1997f2e5ebd8b430e99587b8d354/NOTICE \ && cd $(REPO_PATH) -# Supports OL8+, Fedora 34+, Ubuntu 20.04+, and macOS. +# Supports OL8+, Fedora 34+, Ubuntu 22.04+ and 24.04+, and macOS. OS := "$(shell uname)" ifeq ($(OS), "Darwin") OS_DISTRO := "Darwin" else ifeq ($(OS), "Linux") OS_DISTRO := "$(shell grep '^NAME=' /etc/os-release | sed 's/^NAME=//' | sed 's/"//g')" + OS_MAJOR_VERSION := "$(shell grep '^VERSION=' /etc/os-release | sed -r 's/^[^0-9]+([0-9]+)\..*/\1/')" endif endif # If Souffle cannot be installed, we advise the user to install it manually # and return status code 0, which is not considered a failure. -# Souffle depends upon the libffiX library, where X is the current version it requires. Depending on the version of Ubuntu being used, the exact library -# may not be present. In this script, we manually download and install version 7 on the Ubuntu operating system. .PHONY: souffle souffle: if ! command -v souffle; then \ echo "Installing system dependency: souffle" && \ case $(OS_DISTRO) in \ "Oracle Linux") \ - sudo dnf -y install https://github.com/souffle-lang/souffle/releases/download/2.4/x86_64-oraclelinux-8-souffle-2.4-Linux.rpm;; \ + sudo dnf -y install https://github.com/souffle-lang/souffle/releases/download/2.5/x86_64-oraclelinux-9-souffle-2.5-Linux.rpm;; \ "Fedora Linux") \ - sudo dnf -y install https://github.com/souffle-lang/souffle/releases/download/2.4/x86_64-fedora-34-souffle-2.4-Linux.rpm;; \ + sudo dnf -y install https://github.com/souffle-lang/souffle/releases/download/2.5/x86_64-fedora-41-souffle-2.5-Linux.rpm;; \ "Ubuntu") \ - sudo wget https://souffle-lang.github.io/ppa/souffle-key.public -O /usr/share/keyrings/souffle-archive-keyring.gpg; \ - echo "deb [signed-by=/usr/share/keyrings/souffle-archive-keyring.gpg] https://souffle-lang.github.io/ppa/ubuntu/ stable main" | sudo tee /etc/apt/sources.list.d/souffle.list; \ - sudo apt update; \ - sudo wget http://archive.ubuntu.com/ubuntu/pool/main/libf/libffi/libffi7_3.3-4_amd64.deb; \ - sudo dpkg -i libffi7_3.3-4_amd64.deb; \ - rm libffi7_3.3-4_amd64.deb; \ - sudo apt install souffle;; \ + if [ $(OS_MAJOR_VERSION) == "24" ]; then \ + wget https://github.com/souffle-lang/souffle/releases/download/2.5/x86_64-ubuntu-2404-souffle-2.5-Linux.deb -O ./souffle.deb; \ + elif [ $(OS_MAJOR_VERSION) == "22" ]; then \ + wget https://github.com/souffle-lang/souffle/releases/download/2.5/x86_64-ubuntu-2204-souffle-2.5-Linux.deb -O ./souffle.deb; \ + else \ + echo "Unsupported Ubuntu major version: $(OS_MAJOR_VERSION)"; exit 0; \ + fi; \ + sudo apt install ./souffle.deb; \ + rm ./souffle.deb;; \ "Darwin") \ if command -v brew; then \ brew install --HEAD souffle-lang/souffle/souffle; \ diff --git a/docs/source/pages/cli_usage/command_analyze.rst b/docs/source/pages/cli_usage/command_analyze.rst index 961ebbc1f..a04f88bd2 100644 --- a/docs/source/pages/cli_usage/command_analyze.rst +++ b/docs/source/pages/cli_usage/command_analyze.rst @@ -80,6 +80,11 @@ Options The path to the local .m2 directory. If this option is not used, Macaron will use the default location at $HOME/.m2 +.. option:: --verify-provenance + + Allow the analysis to attempt to verify provenance files as part of its normal operations. + + ----------- Environment ----------- diff --git a/docs/source/pages/developers_guide/apidoc/macaron.provenance.rst b/docs/source/pages/developers_guide/apidoc/macaron.provenance.rst new file mode 100644 index 000000000..d2b68aa0b --- /dev/null +++ b/docs/source/pages/developers_guide/apidoc/macaron.provenance.rst @@ -0,0 +1,34 @@ +macaron.provenance package +========================== + +.. automodule:: macaron.provenance + :members: + :undoc-members: + :show-inheritance: + +Submodules +---------- + +macaron.provenance.provenance\_extractor module +----------------------------------------------- + +.. automodule:: macaron.provenance.provenance_extractor + :members: + :undoc-members: + :show-inheritance: + +macaron.provenance.provenance\_finder module +-------------------------------------------- + +.. automodule:: macaron.provenance.provenance_finder + :members: + :undoc-members: + :show-inheritance: + +macaron.provenance.provenance\_verifier module +---------------------------------------------- + +.. automodule:: macaron.provenance.provenance_verifier + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst b/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst index 724c2614f..c75a00e3d 100644 --- a/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst +++ b/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst @@ -17,22 +17,6 @@ macaron.repo\_finder.commit\_finder module :undoc-members: :show-inheritance: -macaron.repo\_finder.provenance\_extractor module -------------------------------------------------- - -.. automodule:: macaron.repo_finder.provenance_extractor - :members: - :undoc-members: - :show-inheritance: - -macaron.repo\_finder.provenance\_finder module ----------------------------------------------- - -.. automodule:: macaron.repo_finder.provenance_finder - :members: - :undoc-members: - :show-inheritance: - macaron.repo\_finder.repo\_finder module ---------------------------------------- diff --git a/docs/source/pages/developers_guide/apidoc/macaron.rst b/docs/source/pages/developers_guide/apidoc/macaron.rst index d8fdf3e64..b1910f0aa 100644 --- a/docs/source/pages/developers_guide/apidoc/macaron.rst +++ b/docs/source/pages/developers_guide/apidoc/macaron.rst @@ -20,6 +20,7 @@ Subpackages macaron.output_reporter macaron.parsers macaron.policy_engine + macaron.provenance macaron.repo_finder macaron.repo_verifier macaron.slsa_analyzer diff --git a/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst b/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst index 75e216b56..d843de46a 100644 --- a/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst +++ b/docs/source/pages/developers_guide/apidoc/macaron.slsa_analyzer.checks.rst @@ -89,14 +89,6 @@ macaron.slsa\_analyzer.checks.provenance\_commit\_check module :undoc-members: :show-inheritance: -macaron.slsa\_analyzer.checks.provenance\_l3\_check module ----------------------------------------------------------- - -.. automodule:: macaron.slsa_analyzer.checks.provenance_l3_check - :members: - :undoc-members: - :show-inheritance: - macaron.slsa\_analyzer.checks.provenance\_l3\_content\_check module ------------------------------------------------------------------- diff --git a/docs/source/pages/developers_guide/index.rst b/docs/source/pages/developers_guide/index.rst index eb56d1e6b..084e86b0d 100644 --- a/docs/source/pages/developers_guide/index.rst +++ b/docs/source/pages/developers_guide/index.rst @@ -231,3 +231,18 @@ some examples. style_guide apidoc/index + +----------------------------- +Updating the database diagram +----------------------------- + +Macaron uses a visual representation of its database to better help developers understand the relationships between the tables within it. +This diagram is created using the `eralchemy2 `_ an entity relation diagrams generator Python library. +When modifications have been made to Macaron's database, the representative diagram needs to be regenerated to match. +This can be done using the following command: + +.. code-block:: bash + + eralchemy2 -i 'sqlite:////macaron.db' -o er-diagram.svg + +Where ```` is the location of Macaron's output folder. The resulting diagram can then replace the previous version found at ``docs/source/assets/er-diagram.svg`` diff --git a/docs/source/pages/installation.rst b/docs/source/pages/installation.rst index 74b12fa19..7426d33fe 100644 --- a/docs/source/pages/installation.rst +++ b/docs/source/pages/installation.rst @@ -27,7 +27,7 @@ Macaron is currently distributed as a Docker image. We provide a bash script ``r .. note:: When run, Macaron will create output files inside the current directory where ``run_macaron.sh`` is run. If you run Docker Desktop, please make sure that the current directory is bind mountable for Docker (see the `File Sharing settings `_). -Download the ``run_macaron.sh`` script and make it executable by running the commands (replace ``tag`` with the version you want or ``main`` for the latest version): +Download the ``run_macaron.sh`` script and make it executable by running the commands (replace ``tag`` with the version you want or ``release`` for the latest version): .. code-block:: shell diff --git a/docs/source/pages/tutorials/detect_malicious_java_dep.rst b/docs/source/pages/tutorials/detect_malicious_java_dep.rst index b73a79840..df8f2f02c 100644 --- a/docs/source/pages/tutorials/detect_malicious_java_dep.rst +++ b/docs/source/pages/tutorials/detect_malicious_java_dep.rst @@ -25,7 +25,7 @@ dependencies: * - Artifact name - `Package URL (PURL) `_ * - `log4j-core `_ - - ``pkg:maven/org.apache.logging.log4j/log4j-core@3.0.0-beta2?type=jar`` + - ``pkg:maven/org.apache.logging.log4j/log4j-core@3.0.0-beta3?type=jar`` * - `jackson-databind `_ - ``pkg:maven/io.github.behnazh-w.demo/jackson-databind@1.0?type=jar`` @@ -110,20 +110,20 @@ As you scroll down in the HTML report, you will see a section for the dependenci | Macaron has found the two dependencies as expected: * ``io.github.behnazh-w.demo:jackson-databind:1.0`` -* ``org.apache.logging.log4j:log4j-core:3.0.0-beta2`` +* ``org.apache.logging.log4j:log4j-core:3.0.0-beta3`` -When we open the reports for each dependency, we see that ``mcn_find_artifact_pipeline_1`` is passed for ``org.apache.logging.log4j:log4j-core:3.0.0-beta2`` -and a GitHub Actions workflow run is found for publishing version ``3.0.0-beta2``. However, this check is failing for ``io.github.behnazh-w.demo:jackson-databind:1.0``. +When we open the reports for each dependency, we see that ``mcn_find_artifact_pipeline_1`` is passed for ``org.apache.logging.log4j:log4j-core:3.0.0-beta3`` +and a GitHub Actions workflow run is found for publishing version ``3.0.0-beta3``. However, this check is failing for ``io.github.behnazh-w.demo:jackson-databind:1.0``. This means that ``io.github.behnazh-w.demo:jackson-databind:1.0`` could have been built and published manually to Maven Central and could potentially be malicious. .. _fig_find_artifact_pipeline_log4j: .. figure:: ../../_static/images/tutorial_log4j_find_pipeline.png - :alt: mcn_find_artifact_pipeline_1 for org.apache.logging.log4j:log4j-core:3.0.0-beta2 + :alt: mcn_find_artifact_pipeline_1 for org.apache.logging.log4j:log4j-core:3.0.0-beta3 :align: center - ``org.apache.logging.log4j:log4j-core:3.0.0-beta2`` + ``org.apache.logging.log4j:log4j-core:3.0.0-beta3`` .. _fig_infer_artifact_pipeline_bh_jackson_databind: @@ -202,7 +202,7 @@ And the following relation is declared in this policy: * ``violating_dependencies(parent: number)`` Feel free to browse through the available -relations `here `_ +relations `here `_ to see how they are constructed before moving on. .. code-block:: prolog diff --git a/docs/source/pages/tutorials/npm_provenance.rst b/docs/source/pages/tutorials/npm_provenance.rst index bbd8ec4b4..6a591e716 100644 --- a/docs/source/pages/tutorials/npm_provenance.rst +++ b/docs/source/pages/tutorials/npm_provenance.rst @@ -42,7 +42,7 @@ To perform an analysis on the latest version of semver (when this tutorial was w .. code-block:: shell - ./run_macaron.sh analyze -purl pkg:npm/semver@7.6.2 + ./run_macaron.sh analyze -purl pkg:npm/semver@7.6.2 --verify-provenance The analysis involves Macaron downloading the contents of the target repository to the configured, or default, ``output`` folder. Results from the analysis, including checks, are stored in the database found at ``output/macaron.db`` (See :ref:`Output Files Guide `). Once the analysis is complete, Macaron will also produce a report in the form of a HTML file. @@ -52,7 +52,7 @@ During this analysis, Macaron will retrieve two provenance files from the npm re .. note:: Most of the details from the two provenance files can be found through the links provided on the artifacts page on the npm website. In particular: `Sigstore Rekor `_. The provenance file itself can be found at: `npm registry `_. -Of course to reliably say the above does what is claimed here, proof is needed. For this we can rely on the check results produced from the analysis run. In particular, we want to know the results of three checks: ``mcn_provenance_derived_repo_1``, ``mcn_provenance_derived_commit_1``, and ``mcn_provenance_verified_1``. The first two to ensure that the commit and the repository being analyzed match those found in the provenance file, and the last check to ensure that the provenance file has been verified. +Of course to reliably say the above does what is claimed here, proof is needed. For this we can rely on the check results produced from the analysis run. In particular, we want to know the results of three checks: ``mcn_provenance_derived_repo_1``, ``mcn_provenance_derived_commit_1``, and ``mcn_provenance_verified_1``. The first two to ensure that the commit and the repository being analyzed match those found in the provenance file, and the last check to ensure that the provenance file has been verified. For the third check to succeed, you need to enable provenance verification in Macaron by using the ``--verify-provenance`` command-line argument, as demonstrated above. This verification is disabled by default because it can be slow in some cases due to I/O-bound operations. .. _fig_semver_7.6.2_report: diff --git a/docs/source/pages/tutorials/use_verification_summary_attestation.rst b/docs/source/pages/tutorials/use_verification_summary_attestation.rst index 244e0baf8..aa77808b3 100644 --- a/docs/source/pages/tutorials/use_verification_summary_attestation.rst +++ b/docs/source/pages/tutorials/use_verification_summary_attestation.rst @@ -121,7 +121,7 @@ Here is a pretty-printed version of the policy as it appears in the VSA, along w * Applying the Policy (``apply_policy_to``): To apply the ``gcn_provenance_policy``, Macaron first determines if the ``component_id`` is a valid component and if its ``PURL`` conforms to the pattern defined in the ``match`` predicate. If both conditions are met, the policy is applied. - * The template Datalog policy file can be downloaded from `here `_ + * The template Datalog policy file can be downloaded from `here `_ Below you can find the template CUE file that has been used by the :ref:`mcn_provenance_expectation_1 ` check at verification time to verify the provenance. It contains place holders for expected values that are populated by the GDK maintainers. @@ -148,7 +148,7 @@ Here is a pretty-printed version of the policy as it appears in the VSA, along w * ``projecturl: "https://"``: This checks that the ``projecturl`` attribute exactly matches the expected Repository URL. ```` is a placeholder for the actual repository URL, e.g., ``internal.repo.com/micronaut-projects/micronaut-core``. - * The template CUE expectation can be downloaded from `this location `_. + * The template CUE expectation can be downloaded from `this location `_. ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' @@ -175,7 +175,7 @@ Download the check_vsa.sh script .. code-block:: shell - curl -O https://raw.githubusercontent.com/oracle/macaron/main/scripts/release_scripts/check_vsa.sh + curl -O https://raw.githubusercontent.com/oracle/macaron/release/scripts/release_scripts/check_vsa.sh ++++++++++++++++++++++++++ Make the script executable diff --git a/go.mod b/go.mod index 854b14f0f..a5a4bb7fb 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,13 @@ module github.com/oracle/macaron -go 1.23 +go 1.23.0 toolchain go1.23.2 require ( cuelang.org/go v0.12.0 - mvdan.cc/sh/v3 v3.10.0 + mvdan.cc/sh/v3 v3.11.0 ) require ( @@ -17,7 +17,7 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f825ad20a..6ba2623a7 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ github.com/emicklei/proto v1.13.4 h1:myn1fyf8t7tAqIzV91Tj9qXpvyXXGXk8OS2H6IBSc9g github.com/emicklei/proto v1.13.4/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -32,20 +32,20 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d h1:HWfigq7lB31IeJL8iy7jkUmU/PG1Sr8jVGhS749dbUA= github.com/protocolbuffers/txtpbfmt v0.0.0-20241112170944-20d2c9ebc01d/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= -github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= -github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -53,5 +53,5 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4= -mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY= +mvdan.cc/sh/v3 v3.11.0 h1:q5h+XMDRfUGUedCqFFsjoFjrhwf2Mvtt1rkMvVz0blw= +mvdan.cc/sh/v3 v3.11.0/go.mod h1:LRM+1NjoYCzuq/WZ6y44x14YNAI0NK7FLPeQSaFagGg= diff --git a/pyproject.toml b/pyproject.toml index feafa2b42..d46835842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = [ "cyclonedx-bom >=4.0.0,<5.0.0", "cyclonedx-python-lib[validation] >=7.3.4,<8.0.0", "beautifulsoup4 >= 4.12.0,<5.0.0", + "problog >= 2.2.6,<3.0.0", ] keywords = [] # https://pypi.org/classifiers/ @@ -203,6 +204,7 @@ module = [ "gitdb.*", "yamale.*", "defusedxml.*", + "problog.*", ] ignore_missing_imports = true diff --git a/src/macaron/__main__.py b/src/macaron/__main__.py index da727cdaa..93aca76d7 100644 --- a/src/macaron/__main__.py +++ b/src/macaron/__main__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 is the main entrypoint to run Macaron.""" @@ -32,7 +32,6 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None: """Run the SLSA checks against a single target repository.""" - deps_depth = None if analyzer_single_args.deps_depth == "inf": deps_depth = -1 else: @@ -173,7 +172,8 @@ def analyze_slsa_levels_single(analyzer_single_args: argparse.Namespace) -> None analyzer_single_args.sbom_path, deps_depth, provenance_payload=prov_payload, - validate_malware_switch=analyzer_single_args.validate_malware_switch, + validate_malware=analyzer_single_args.validate_malware, + verify_provenance=analyzer_single_args.verify_provenance, ) sys.exit(status_code) @@ -206,16 +206,21 @@ def verify_policy(verify_policy_args: argparse.Namespace) -> int: vsa = generate_vsa(policy_content=policy_content, policy_result=result) if vsa is not None: vsa_filepath = os.path.join(global_config.output_path, "vsa.intoto.jsonl") - logger.info("Generating the Verification Summary Attestation (VSA) to %s.", vsa_filepath) + logger.info( + "Generating the Verification Summary Attestation (VSA) to %s.", + os.path.relpath(vsa_filepath, os.getcwd()), + ) logger.info( "To decode and inspect the payload, run `cat %s | jq -r '.payload' | base64 -d | jq`.", - vsa_filepath, + os.path.relpath(vsa_filepath, os.getcwd()), ) try: with open(vsa_filepath, mode="w", encoding="utf-8") as file: file.write(json.dumps(vsa)) except OSError as err: - logger.error("Could not generate the VSA to %s. Error: %s", vsa_filepath, err) + logger.error( + "Could not generate the VSA to %s. Error: %s", os.path.relpath(vsa_filepath, os.getcwd()), err + ) policy_reporter = PolicyReporter() policy_reporter.generate(global_config.output_path, result) @@ -360,7 +365,7 @@ def main(argv: list[str] | None = None) -> None: help="The directory where Macaron looks for already cloned repositories.", ) - # Add sub parsers for each action + # Add sub parsers for each action. sub_parser = main_parser.add_subparsers(dest="action", help="Run macaron --help for help") # Use Macaron to analyze one single repository. @@ -470,12 +475,19 @@ def main(argv: list[str] | None = None) -> None: ) single_analyze_parser.add_argument( - "--validate-malware-switch", + "--validate-malware", required=False, action="store_true", help=("Enable malware validation."), ) + single_analyze_parser.add_argument( + "--verify-provenance", + required=False, + action="store_true", + help=("Allow the analysis to attempt to verify provenance files as part of its normal operations."), + ) + # Dump the default values. sub_parser.add_parser(name="dump-defaults", description="Dumps the defaults.ini file to the output directory.") @@ -537,9 +549,9 @@ def main(argv: list[str] | None = None) -> None: sys.exit(os.EX_USAGE) if os.path.isdir(args.output_dir): - logger.info("Setting the output directory to %s", args.output_dir) + logger.info("Setting the output directory to %s", os.path.relpath(args.output_dir, os.getcwd())) else: - logger.info("No directory at %s. Creating one ...", args.output_dir) + logger.info("No directory at %s. Creating one ...", os.path.relpath(args.output_dir, os.getcwd())) os.makedirs(args.output_dir) # Add file handler to the root logger. Remove stream handler from the diff --git a/src/macaron/config/defaults.ini b/src/macaron/config/defaults.ini index f895c20aa..0ccad65c4 100644 --- a/src/macaron/config/defaults.ini +++ b/src/macaron/config/defaults.ini @@ -46,11 +46,6 @@ validate = True # The CycloneDX schema version used for validation. schema = 1.6 -# This is the Analyzer section used as part of Macaron's analysis. -[analyzer] -# This enables or disables attempts at verification of provenance. -verify_provenance = True - # This is the repo finder script. [repofinder] find_repos = True @@ -59,7 +54,11 @@ use_open_source_insights = True redirect_urls = gitbox.apache.org git-wip-us.apache.org +# If False, the find-source operation will use git ls-remote to find tags, instead of cloning repositories. find_source_should_clone = False +# If True, the Repo Finder will try to find and use the latest version of a PURL when the provided version fails +# to return a valid repository. +try_latest_purl = True [repofinder.java] # The list of maven-like repositories to attempt to retrieve artifact POMs from. @@ -159,8 +158,6 @@ jenkins = withMaven buildPlugin asfMavenTlpStdBuild - ./mvnw - ./mvn [builder.maven.ci.deploy] github_actions = @@ -233,14 +230,12 @@ wrapper_files = [builder.gradle.ci.build] github_actions = actions/setup-java travis_ci = - jdk - ./gradlew + gradle circle_ci = - ./gradlew + gradle gitlab_ci = - ./gradlew + gradle jenkins = - ./gradlew [builder.gradle.ci.deploy] github_actions = @@ -248,24 +243,28 @@ github_actions = spring-io/artifactory-deploy-action travis_ci = artifactoryPublish + gradle publish ./gradlew publish publishToSonatype gradle-git-publish gitPublishPush circle_ci = artifactoryPublish + gradle publish ./gradlew publish publishToSonatype gradle-git-publish gitPublishPush gitlab_ci = artifactoryPublish + gradle publish ./gradlew publish publishToSonatype gradle-git-publish gitPublishPush jenkins = artifactoryPublish + gradle publish ./gradlew publish publishToSonatype gradle-git-publish @@ -565,7 +564,7 @@ purl_endpoint = v3alpha/purl # [analysis.checks] # exclude = # mcn_build_as_code_1 -# mcn_provenance_level_three_1 +# mcn_provenance_verified_1 # include = * # ``` # 3. Exclude multiple checks that start with `mcn_provenance`: diff --git a/src/macaron/config/global_config.py b/src/macaron/config/global_config.py index d6d113a3a..8befb4045 100644 --- a/src/macaron/config/global_config.py +++ b/src/macaron/config/global_config.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 GlobalConfig class to be used globally.""" @@ -97,10 +97,10 @@ def load_expectation_files(self, exp_path: str) -> None: policy_file_path = os.path.join(exp_path, policy_path) if os.path.isfile(policy_file_path): exp_files.append(policy_file_path) - logger.info("Added provenance expectation file %s", policy_file_path) + logger.info("Added provenance expectation file %s", os.path.relpath(policy_file_path, os.getcwd())) elif os.path.isfile(exp_path): exp_files.append(exp_path) - logger.info("Added provenance expectation file %s", exp_path) + logger.info("Added provenance expectation file %s", os.path.relpath(exp_path, os.getcwd())) self.expectation_paths = exp_files @@ -114,7 +114,10 @@ def load_python_venv(self, venv_path: str) -> None: The path to the Python virtual environment of the target software component. """ if os.path.isdir(venv_path): - logger.info("Found Python virtual environment for the analysis target at %s", venv_path) + logger.info( + "Found Python virtual environment for the analysis target at %s", + os.path.relpath(venv_path, os.getcwd()), + ) self.python_venv_path = str(os.path.abspath(venv_path)) diff --git a/src/macaron/database/db_custom_types.py b/src/macaron/database/db_custom_types.py index f40256099..e67d22b3a 100644 --- a/src/macaron/database/db_custom_types.py +++ b/src/macaron/database/db_custom_types.py @@ -1,13 +1,21 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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 implements SQLAlchemy type for converting date format to RFC3339 string representation.""" +"""This module implements SQLAlchemy types for Python data types that cannot be automatically stored.""" import datetime +import json from typing import Any from sqlalchemy import JSON, String, TypeDecorator +from macaron.slsa_analyzer.provenance.intoto import ( + InTotoPayload, + InTotoV01Payload, + InTotoV1Payload, + validate_intoto_payload, +) + class RFC3339DateTime(TypeDecorator): # pylint: disable=W0223 """ @@ -36,7 +44,7 @@ def process_bind_param(self, value: None | Any, dialect: Any) -> None | str: if the provided ``datetime`` is a naive ``datetime`` object then UTC is added. value: None | datetime.datetime - The value being stored + The value being stored. """ if value is None: return None @@ -52,7 +60,7 @@ def process_result_value(self, value: None | str, dialect: Any) -> None | dateti If the deserialized ``datetime`` has a timezone then return it, otherwise add UTC as its timezone. value: None | str - The value being loaded + The value being loaded. """ if value is None: return None @@ -76,7 +84,7 @@ def process_bind_param(self, value: None | dict, dialect: Any) -> None | dict: """Process when storing a dict object to the SQLite db. value: None | dict - The value being stored + The value being stored. """ if not isinstance(value, dict): raise TypeError("DBJsonDict type expects a dict.") @@ -87,8 +95,63 @@ def process_result_value(self, value: None | dict, dialect: Any) -> None | dict: """Process when loading a dict object from the SQLite db. value: None | dict - The value being loaded + The value being loaded. """ if not isinstance(value, dict): raise TypeError("DBJsonDict type expects a dict.") return value + + +class ProvenancePayload(TypeDecorator): # pylint: disable=W0223 + """SQLAlchemy column type to serialize InTotoProvenance.""" + + # It is stored in the database as a String value. + impl = String + + # To prevent Sphinx from rendering the docstrings for `cache_ok`, make this docstring private. + #: :meta private: + cache_ok = True + + def process_bind_param(self, value: InTotoPayload | None, dialect: Any) -> str | None: + """Process when storing an InTotoPayload object to the SQLite db. + + value: InTotoPayload | None + The value being stored. + """ + if value is None: + return None + + if not isinstance(value, InTotoPayload): + raise TypeError("ProvenancePayload type expects an InTotoPayload.") + + payload_type = value.__class__.__name__ + payload_dict = {"payload_type": payload_type, "payload": value.statement} + return json.dumps(payload_dict) + + def process_result_value(self, value: str | None, dialect: Any) -> InTotoPayload | None: + """Process when loading an InTotoPayload object from the SQLite db. + + value: str | None + The value being loaded. + """ + if value is None: + return None + + try: + payload_dict = json.loads(value) + except ValueError as error: + raise TypeError(f"Error parsing str as JSON: {error}") from error + + if not isinstance(payload_dict, dict): + raise TypeError("Parsed data is not a dict.") + + if "payload_type" not in payload_dict or "payload" not in payload_dict: + raise TypeError("Missing keys in dict for ProvenancePayload type.") + + payload = payload_dict["payload"] + if payload_dict["payload_type"] == "InTotoV01Payload": + return InTotoV01Payload(statement=payload) + if payload_dict["payload_type"] == "InTotoV1Payload": + return InTotoV1Payload(statement=payload) + + return validate_intoto_payload(payload) diff --git a/src/macaron/database/table_definitions.py b/src/macaron/database/table_definitions.py index d9e1d7f17..2a7f1e95a 100644 --- a/src/macaron/database/table_definitions.py +++ b/src/macaron/database/table_definitions.py @@ -34,7 +34,7 @@ from macaron.artifact.maven import MavenSubjectPURLMatcher from macaron.database.database_manager import ORMBase -from macaron.database.db_custom_types import RFC3339DateTime +from macaron.database.db_custom_types import ProvenancePayload, RFC3339DateTime from macaron.errors import InvalidPURLError from macaron.repo_finder.repo_finder_enums import CommitFinderInfo, RepoFinderInfo from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, ProvenanceSubjectPURLMatcher @@ -491,7 +491,10 @@ class Provenance(ORMBase): component: Mapped["Component"] = relationship(back_populates="provenance") #: The SLSA version. - version: Mapped[str] = mapped_column(String, nullable=False) + slsa_version: Mapped[str] = mapped_column(String, nullable=True) + + #: The SLSA level. + slsa_level: Mapped[int] = mapped_column(Integer, default=0) #: The release tag commit sha. release_commit_sha: Mapped[str] = mapped_column(String, nullable=True) @@ -499,8 +502,17 @@ class Provenance(ORMBase): #: The release tag. release_tag: Mapped[str] = mapped_column(String, nullable=True) - #: The provenance payload content in JSON format. - provenance_json: Mapped[str] = mapped_column(String, nullable=False) + #: The repository URL from the provenance. + repository_url: Mapped[str] = mapped_column(String, nullable=True) + + #: The commit sha from the provenance. + commit_sha: Mapped[str] = mapped_column(String, nullable=True) + + #: The provenance payload. + provenance_payload: Mapped[InTotoPayload] = mapped_column(ProvenancePayload, nullable=False) + + #: The verified status of the provenance. + verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) #: A one-to-many relationship with the release artifacts. artifact: Mapped[list["ReleaseArtifact"]] = relationship(back_populates="provenance") diff --git a/src/macaron/database/views.py b/src/macaron/database/views.py index 5263ead31..0db99814a 100644 --- a/src/macaron/database/views.py +++ b/src/macaron/database/views.py @@ -13,7 +13,7 @@ import sqlalchemy as sa import sqlalchemy.event -from sqlalchemy import Connection +from sqlalchemy import Connection, Dialect from sqlalchemy.ext import compiler from sqlalchemy.schema import BaseDDLElement, DDLElement, MetaData, SchemaItem, Table from sqlalchemy.sql import Select @@ -46,10 +46,12 @@ def _drop_view(element, comp, **kw): # type: ignore def view_exists( ddl: BaseDDLElement, - target: SchemaItem, + target: SchemaItem | str, bind: Connection | None, tables: list[Table] | None = None, state: Any | None = None, + *, + dialect: Dialect, **kw: Any, ) -> bool: """Check if a view exists in the database. @@ -62,7 +64,7 @@ def view_exists( ---------- ddl : BaseDDLElement The DDL element that represents the creation or dropping of a view. - target : SchemaItem + target : SchemaItem | str The target schema item (not directly used in this check). bind : Connection | None The database connection used to inspect the database for existing views. @@ -70,8 +72,10 @@ def view_exists( A list of tables (not directly used in this check). state : Any | None, optional The state of the object (not directly used in this check). + dialect : Dialect + The database dialect to be used for generating SQL (not directly used in this check). kw : Any - Additional keyword arguments passed to the function (not directly used). + Additional keyword arguments passed to the function. Returns ------- @@ -86,10 +90,12 @@ def view_exists( def view_doesnt_exist( ddl: BaseDDLElement, - target: SchemaItem, + target: SchemaItem | str, bind: Connection | None, tables: list[Table] | None = None, state: Any | None = None, + *, + dialect: Dialect, **kw: Any, ) -> bool: """Check if a view does not exist in the database. @@ -101,7 +107,7 @@ def view_doesnt_exist( ---------- ddl : BaseDDLElement The DDL element that represents the creation or dropping of a view. - target : SchemaItem + target : SchemaItem | str The target schema item (not directly used in this check). bind : Connection | None The database connection used to inspect the database for existing views. @@ -109,15 +115,17 @@ def view_doesnt_exist( A list of tables (not directly used in this check). state : Any | None, optional The state of the object (not directly used in this check). + dialect : Dialect + The database dialect to be used for generating SQL (not directly used in this check). kw : Any - Additional keyword arguments passed to the function (not directly used). + Additional keyword arguments passed to the function. Returns ------- bool Returns `True` if the view does not exist in the database, `False` otherwise. """ - return not view_exists(ddl, target, bind, **kw) + return not view_exists(ddl, target, bind, dialect=dialect, **kw) def create_view(name: str, metadata: MetaData, selectable: Select[Any]) -> None: diff --git a/src/macaron/dependency_analyzer/cyclonedx.py b/src/macaron/dependency_analyzer/cyclonedx.py index 8e2714247..bacefb99d 100644 --- a/src/macaron/dependency_analyzer/cyclonedx.py +++ b/src/macaron/dependency_analyzer/cyclonedx.py @@ -381,7 +381,9 @@ def resolve_dependencies(main_ctx: Any, sbom_path: str, recursive: bool = False) continue if sbom_path: - logger.info("Getting the dependencies from the SBOM defined at %s.", sbom_path) + logger.info( + "Getting the dependencies from the SBOM defined at %s.", os.path.relpath(sbom_path, os.getcwd()) + ) deps_resolved = dep_analyzer.get_deps_from_sbom( sbom_path, @@ -406,7 +408,7 @@ def resolve_dependencies(main_ctx: Any, sbom_path: str, recursive: bool = False) "Running %s version %s dependency analyzer on %s", dep_analyzer.tool_name, dep_analyzer.tool_version, - main_ctx.component.repository.fs_path, + os.path.relpath(main_ctx.component.repository.fs_path, os.getcwd()), ) log_path = os.path.join( @@ -452,7 +454,11 @@ def resolve_dependencies(main_ctx: Any, sbom_path: str, recursive: bool = False) recursive=recursive, ) - logger.info("Stored dependency resolver log for %s to %s.", dep_analyzer.tool_name, log_path) + logger.info( + "Stored dependency resolver log for %s to %s.", + dep_analyzer.tool_name, + os.path.relpath(log_path, os.getcwd()), + ) # Use repo finder to find more repositories to analyze. if defaults.getboolean("repofinder", "find_repos"): diff --git a/src/macaron/malware_analyzer/README.md b/src/macaron/malware_analyzer/README.md index 6fe93d89a..7617e4156 100644 --- a/src/macaron/malware_analyzer/README.md +++ b/src/macaron/malware_analyzer/README.md @@ -52,6 +52,19 @@ When a heuristic fails, with `HeuristicResult.FAIL`, then that is an indicator b - **Rule**: Return `HeuristicResult.FAIL` if the major or epoch is abnormally high; otherwise, return `HeuristicResult.PASS`. - **Dependency**: Will be run if the One Release heuristic fails. +### Contributing + +When contributing an analyzer, it must meet the following requirements: + +- The analyzer must be implemented in a separate file, placed in the relevant folder based on what it analyzes ([metadata](./pypi_heuristics/metadata/) or [sourcecode](./pypi_heuristics/sourcecode/)). +- The analyzer must inherit from the `BaseHeuristicAnalyzer` class and implement the `analyze` function, returning relevant information specific to the analysis. +- The analyzer name must be added to [heuristics.py](./pypi_heuristics/heuristics.py) file so it can be used for rule combinations in [detect_malicious_metadata_check.py](../slsa_analyzer/checks/detect_malicious_metadata_check.py) +- Update the `malware_rules_problog_model` in [detect_malicious_metadata_check.py](../slsa_analyzer/checks/detect_malicious_metadata_check.py) with logical statements where the heuristic should be included. When adding new rules, please follow the following guidelines: + - Provide a [confidence value](../slsa_analyzer/checks/check_result.py) using the `Confidence` enum. + - Provide a name based on this confidence value (i.e. `high`, `medium`, or `low`) + - If it does not already exist, make sure to assign this to the result variable (`problog_result_access`) + - If there are commonly used combinations introduced by adding the heuristic, combine and justify them at the top of the static model (see `quickUndetailed` and `forceSetup` as current examples). + ### Confidence Score Motivation The original seven heuristics which started this work were Empty Project Link, Unreachable Project Links, One Release, High Release Frequency, Unchange Release, Closer Release Join Date, and Suspicious Setup. These heuristics (excluding those with a dependency) were run on 1167 packages from trusted organizations, with the following results: diff --git a/src/macaron/output_reporter/reporter.py b/src/macaron/output_reporter/reporter.py index 6ff9b3898..78464e13d 100644 --- a/src/macaron/output_reporter/reporter.py +++ b/src/macaron/output_reporter/reporter.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 reporter classes for creating reports of Macaron analyzed results.""" @@ -60,11 +60,11 @@ def write_file(self, file_path: str, data: str) -> bool: """ try: with open(file_path, mode=self.mode, encoding=self.encoding) as file: - logger.info("Writing to file %s", file_path) + logger.info("Writing to file %s", os.path.relpath(file_path, os.getcwd())) file.write(data) return True except OSError as error: - logger.error("Cannot write to %s. Error: %s", file_path, error) + logger.error("Cannot write to %s. Error: %s", os.path.relpath(file_path, os.getcwd()), error) return False @abc.abstractmethod diff --git a/src/macaron/parsers/bashparser.py b/src/macaron/parsers/bashparser.py index 4e4f322e3..0d5cd66c1 100644 --- a/src/macaron/parsers/bashparser.py +++ b/src/macaron/parsers/bashparser.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 is a Python wrapper for the compiled bashparser binary. @@ -96,10 +96,10 @@ def parse_file(file_path: str, macaron_path: str | None = None) -> dict: macaron_path = global_config.macaron_path try: with open(file_path, encoding="utf8") as file: - logger.info("Parsing %s.", file_path) + logger.info("Parsing %s.", os.path.relpath(file_path, os.getcwd())) return parse(file.read(), macaron_path) except OSError as error: - raise ParseError(f"Could not load the bash script file: {file_path}.") from error + raise ParseError(f"Could not load the bash script file: {os.path.relpath(file_path, os.getcwd())}.") from error except ParseError as error: raise error diff --git a/src/macaron/provenance/__init__.py b/src/macaron/provenance/__init__.py new file mode 100644 index 000000000..a99afa31c --- /dev/null +++ b/src/macaron/provenance/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) 2024 - 2025, 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 package contains the provenance tools for software components.""" diff --git a/src/macaron/repo_finder/provenance_extractor.py b/src/macaron/provenance/provenance_extractor.py similarity index 90% rename from src/macaron/repo_finder/provenance_extractor.py rename to src/macaron/provenance/provenance_extractor.py index 84c57ed3b..623f6d304 100644 --- a/src/macaron/repo_finder/provenance_extractor.py +++ b/src/macaron/provenance/provenance_extractor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 methods for extracting repository and commit metadata from provenance files.""" @@ -7,16 +7,11 @@ from abc import ABC, abstractmethod from packageurl import PackageURL -from pydriller import Git from macaron.errors import ProvenanceError from macaron.json_tools import JsonType, json_extract from macaron.repo_finder import to_domain_from_known_purl_types -from macaron.repo_finder.commit_finder import ( - AbstractPurlType, - determine_abstract_purl_type, - extract_commit_from_version, -) +from macaron.repo_finder.commit_finder import AbstractPurlType, determine_abstract_purl_type from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV1Payload, InTotoV01Payload from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Statement from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1Statement @@ -48,10 +43,10 @@ def extract_repo_and_commit_from_provenance(payload: InTotoPayload) -> tuple[str If the extraction process fails for any reason. """ predicate_type = payload.statement.get("predicateType") - if isinstance(payload, InTotoV1Payload): - if predicate_type == "https://slsa.dev/provenance/v1": - return _extract_from_slsa_v1(payload) - elif isinstance(payload, InTotoV01Payload): + if isinstance(payload, InTotoV1Payload) and predicate_type == "https://slsa.dev/provenance/v1": + return _extract_from_slsa_v1(payload) + + if isinstance(payload, InTotoV01Payload): if predicate_type == "https://slsa.dev/provenance/v0.2": return _extract_from_slsa_v02(payload) if predicate_type == "https://slsa.dev/provenance/v0.1": @@ -61,12 +56,40 @@ def extract_repo_and_commit_from_provenance(payload: InTotoPayload) -> tuple[str msg = ( f"Extraction from provenance not supported for versions: " - f"predicate_type {predicate_type}, in-toto {str(type(payload))}." + f"predicate_type {payload.statement.get('predicateType')}, in-toto {str(type(payload))}." ) logger.debug(msg) raise ProvenanceError(msg) +def extract_predicate_version(payload: InTotoPayload) -> str | None: + """Extract and return the SLSA version from the passed payload. + + Parameters + ---------- + payload: InTotoPayload + The payload to extract from. + + Returns + ------- + str | None + The SLSA version, or None if the payload does contain a supported version number. + """ + predicate_type = payload.statement.get("predicateType") + if isinstance(payload, InTotoV1Payload) and predicate_type == "https://slsa.dev/provenance/v1": + return "SLSA-1.0" + + if isinstance(payload, InTotoV01Payload): + if predicate_type == "https://slsa.dev/provenance/v0.2": + return "SLSA-0.2" + if predicate_type == "https://slsa.dev/provenance/v0.1": + return "SLSA-0.1" + if predicate_type == "https://witness.testifysec.com/attestation-collection/v0.1": + return "WITNESS-0.1" + + return None + + def _extract_from_slsa_v01(payload: InTotoV01Payload) -> tuple[str | None, str | None]: """Extract the repository and commit metadata from the slsa v01 provenance payload.""" predicate: dict[str, JsonType] | None = payload.statement.get("predicate") @@ -126,27 +149,20 @@ def _extract_from_slsa_v1(payload: InTotoV1Payload) -> tuple[str | None, str | N logger.debug("No predicate in payload statement.") return None, None - build_def = json_extract(predicate, ["buildDefinition"], dict) - if not build_def: - return None, None - - build_type = json_extract(build_def, ["buildType"], str) - if not build_type: - return None, None + build_def = ProvenancePredicate.find_build_def(payload.statement) # Extract the repository URL. - match build_type: - case "https://slsa-framework.github.io/gcb-buildtypes/triggered-build/v1": - repo = json_extract(build_def, ["externalParameters", "sourceToBuild", "repository"], str) - if not repo: - repo = json_extract(build_def, ["externalParameters", "configSource", "repository"], str) - case "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1": - repo = json_extract(build_def, ["externalParameters", "workflow", "repository"], str) - case "https://github.com/oracle/macaron/tree/main/src/macaron/resources/provenance-buildtypes/oci/v1": - repo = json_extract(build_def, ["externalParameters", "source"], str) - case _: - logger.debug("Unsupported build type for SLSA v1: %s", build_type) - return None, None + if isinstance(build_def, SLSAGCBBuildDefinitionV1): + repo = json_extract(predicate, ["buildDefinition", "externalParameters", "sourceToBuild", "repository"], str) + if not repo: + repo = json_extract(predicate, ["buildDefinition", "externalParameters", "configSource", "repository"], str) + elif isinstance(build_def, SLSAGithubActionsBuildDefinitionV1): + repo = json_extract(predicate, ["buildDefinition", "externalParameters", "workflow", "repository"], str) + elif isinstance(build_def, SLSAOCIBuildDefinitionV1): + repo = json_extract(predicate, ["buildDefinition", "externalParameters", "source"], str) + else: + logger.debug("Unsupported build type for SLSA v1: %s", type(build_def)) + return None, None if not repo: logger.debug("Repo URL not found in SLSA v1 payload.") @@ -154,10 +170,12 @@ def _extract_from_slsa_v1(payload: InTotoV1Payload) -> tuple[str | None, str | N # Extract the commit hash. commit = None - if build_type == "https://github.com/oracle/macaron/tree/main/src/macaron/resources/provenance-buildtypes/oci/v1": - commit = json_extract(build_def, ["internalParameters", "buildEnvVar", "BLD_COMMIT_HASH"], str) + if isinstance(build_def, SLSAOCIBuildDefinitionV1): + commit = json_extract( + predicate, ["buildDefinition", "internalParameters", "buildEnvVar", "BLD_COMMIT_HASH"], str + ) else: - deps = json_extract(build_def, ["resolvedDependencies"], list) + deps = json_extract(predicate, ["buildDefinition", "resolvedDependencies"], list) if not deps: return repo, None for dep in deps: @@ -278,27 +296,18 @@ def check_if_input_repo_provenance_conflict( def check_if_input_purl_provenance_conflict( - git_obj: Git, repo_path_input: bool, - digest_input: bool, provenance_repo_url: str | None, - provenance_commit_digest: str | None, purl: PackageURL, ) -> bool: """Test if the input repository type PURL's repo and commit match the contents of the provenance. Parameters ---------- - git_obj: Git - The Git object. repo_path_input: bool True if there is a repo as input. - digest_input: str - True if there is a commit as input. provenance_repo_url: str | None The repo url from provenance. - provenance_commit_digest: str | None - The commit digest from provenance. purl: PackageURL The input repository PURL. @@ -321,18 +330,6 @@ def check_if_input_purl_provenance_conflict( ) return True - # Check the PURL commit against the provenance. - if not digest_input and provenance_commit_digest and purl.version: - purl_commit, _ = extract_commit_from_version(git_obj, purl.version) - if purl_commit and purl_commit != provenance_commit_digest: - logger.debug( - "The commit digest passed via purl input does not match what exists in the " - "provenance. Purl Commit: %s, Provenance Commit: %s.", - purl_commit, - provenance_commit_digest, - ) - return True - return False diff --git a/src/macaron/repo_finder/provenance_finder.py b/src/macaron/provenance/provenance_finder.py similarity index 75% rename from src/macaron/repo_finder/provenance_finder.py rename to src/macaron/provenance/provenance_finder.py index aaf6a312a..b02423eec 100644 --- a/src/macaron/repo_finder/provenance_finder.py +++ b/src/macaron/provenance/provenance_finder.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 methods for finding provenance files.""" @@ -107,38 +107,6 @@ def _find_provenance(self, discovery_functions: list[partial[list[InTotoPayload] logger.debug("No provenance found.") return [] - def verify_provenance(self, purl: PackageURL, provenance: list[InTotoPayload]) -> bool: - """Verify the passed provenance. - - Parameters - ---------- - purl: PackageURL - The PURL of the analysis target. - provenance: list[InTotoPayload] - The list of provenance. - - Returns - ------- - bool - True if the provenance could be verified, or False otherwise. - """ - if determine_abstract_purl_type(purl) == AbstractPurlType.REPOSITORY: - # Do not perform default verification for repository type targets. - return False - - verification_function = None - - if purl.type == "npm": - verification_function = partial(verify_npm_provenance, purl, provenance) - - # TODO other verification functions go here. - - if verification_function: - return verification_function() - - logger.debug("Provenance verification not supported for PURL type: %s", purl.type) - return False - def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoPayload]: """Find and download the NPM based provenance for the passed PURL. @@ -213,72 +181,6 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoP return [] -def verify_npm_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool: - """Compare the unsigned payload subject digest with the signed payload digest, if available. - - Parameters - ---------- - purl: PackageURL - The PURL of the analysis target. - provenance: list[InTotoPayload] - The provenances to verify. - - Returns - ------- - bool - True if the provenance was verified, or False otherwise. - """ - if len(provenance) != 2: - logger.debug("Expected unsigned and signed provenance.") - return False - - signed_subjects = provenance[1].statement.get("subject") - if not signed_subjects: - return False - - unsigned_subjects = provenance[0].statement.get("subject") - if not unsigned_subjects: - return False - - found_signed_subject = None - for signed_subject in signed_subjects: - name = signed_subject.get("name") - if name and name == str(purl): - found_signed_subject = signed_subject - break - - if not found_signed_subject: - return False - - found_unsigned_subject = None - for unsigned_subject in unsigned_subjects: - name = unsigned_subject.get("name") - if name and name == str(purl): - found_unsigned_subject = unsigned_subject - break - - if not found_unsigned_subject: - return False - - signed_digest = found_signed_subject.get("digest") - unsigned_digest = found_unsigned_subject.get("digest") - if not (signed_digest and unsigned_digest): - return False - - # For signed and unsigned to match, the digests must be identical. - if signed_digest != unsigned_digest: - return False - - key = list(signed_digest.keys())[0] - logger.debug( - "Verified provenance against signed companion. Signed: %s, Unsigned: %s.", - signed_digest[key][:7], - unsigned_digest[key][:7], - ) - - return True - - def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[InTotoPayload]: """Find and download the GAV based provenance for the passed PURL. @@ -373,7 +275,9 @@ def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[ return provenances[:1] -def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> InTotoPayload | None: +def find_provenance_from_ci( + analyze_ctx: AnalyzeContext, git_obj: Git | None, download_path: str +) -> InTotoPayload | None: """Try to find provenance from CI services of the repository. Note that we stop going through the CI services once we encounter a CI service @@ -385,9 +289,11 @@ def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> Parameters ---------- analyze_ctx: AnalyzeContext - The contenxt of the ongoing analysis. + The context of the ongoing analysis. git_obj: Git | None The Pydriller Git object representing the repository, if any. + download_path: str + The pre-existing location to download discovered files to. Returns ------- @@ -463,9 +369,7 @@ def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> ci_info["provenance_assets"].extend(provenance_assets) # Download the provenance assets and load the provenance payloads. - download_provenances_from_github_actions_ci_service( - ci_info, - ) + download_provenances_from_ci_service(ci_info, download_path) # TODO consider how to handle multiple payloads here. return ci_info["provenances"][0].payload if ci_info["provenances"] else None @@ -476,56 +380,60 @@ def find_provenance_from_ci(analyze_ctx: AnalyzeContext, git_obj: Git | None) -> return None -def download_provenances_from_github_actions_ci_service(ci_info: CIInfo) -> None: +def download_provenances_from_ci_service(ci_info: CIInfo, download_path: str) -> None: """Download provenances from GitHub Actions. Parameters ---------- ci_info: CIInfo, A ``CIInfo`` instance that holds a GitHub Actions git service object. + download_path: str + The pre-existing location to download discovered files to. """ ci_service = ci_info["service"] prov_assets = ci_info["provenance_assets"] - + if not os.path.isdir(download_path): + logger.debug("Download location is not a valid directory.") + return try: - with tempfile.TemporaryDirectory() as temp_path: - downloaded_provs = [] - for prov_asset in prov_assets: - # Check the size before downloading. - if prov_asset.size_in_bytes > defaults.getint( - "slsa.verifier", - "max_download_size", - fallback=1000000, - ): - logger.info( - "Skip verifying the provenance %s: asset size too large.", - prov_asset.name, - ) - continue + downloaded_provs = [] + for prov_asset in prov_assets: + # Check the size before downloading. + if prov_asset.size_in_bytes > defaults.getint( + "slsa.verifier", + "max_download_size", + fallback=1000000, + ): + logger.info( + "Skip verifying the provenance %s: asset size too large.", + prov_asset.name, + ) + continue - provenance_filepath = os.path.join(temp_path, prov_asset.name) + provenance_filepath = os.path.join(download_path, prov_asset.name) - if not ci_service.api_client.download_asset( - prov_asset.url, - provenance_filepath, - ): - logger.debug( - "Could not download the provenance %s. Skip verifying...", - prov_asset.name, - ) - continue + if not ci_service.api_client.download_asset( + prov_asset.url, + provenance_filepath, + ): + logger.debug( + "Could not download the provenance %s. Skip verifying...", + prov_asset.name, + ) + continue - # Read the provenance. - try: - payload = load_provenance_payload(provenance_filepath) - except LoadIntotoAttestationError as error: - logger.error("Error logging provenance: %s", error) - continue + # Read the provenance. + try: + payload = load_provenance_payload(provenance_filepath) + except LoadIntotoAttestationError as error: + logger.error("Error logging provenance: %s", error) + continue - # Add the provenance file. - downloaded_provs.append(SLSAProvenanceData(payload=payload, asset=prov_asset)) + # Add the provenance file. + downloaded_provs.append(SLSAProvenanceData(payload=payload, asset=prov_asset)) # Persist the provenance payloads into the CIInfo object. ci_info["provenances"] = downloaded_provs + except OSError as error: logger.error("Error while storing provenance in the temporary directory: %s", error) diff --git a/src/macaron/provenance/provenance_verifier.py b/src/macaron/provenance/provenance_verifier.py new file mode 100644 index 000000000..18e090f0c --- /dev/null +++ b/src/macaron/provenance/provenance_verifier.py @@ -0,0 +1,400 @@ +# Copyright (c) 2024 - 2025, 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 methods for verifying provenance files.""" +import glob +import hashlib +import logging +import os +import subprocess # nosec B404 +import tarfile +import zipfile +from functools import partial +from pathlib import Path + +from packageurl import PackageURL + +from macaron.config.defaults import defaults +from macaron.config.global_config import global_config +from macaron.provenance.provenance_extractor import ProvenancePredicate, SLSAGithubGenericBuildDefinitionV01 +from macaron.repo_finder.commit_finder import AbstractPurlType, determine_abstract_purl_type +from macaron.slsa_analyzer.analyze_context import AnalyzeContext +from macaron.slsa_analyzer.asset import AssetLocator +from macaron.slsa_analyzer.ci_service import BaseCIService +from macaron.slsa_analyzer.git_url import get_repo_dir_name +from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, InTotoV01Payload, v01 +from macaron.slsa_analyzer.specs.ci_spec import CIInfo + +logger: logging.Logger = logging.getLogger(__name__) + + +def verify_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool: + """Verify the passed provenance. + + Parameters + ---------- + purl: PackageURL + The PURL of the analysis target. + provenance: list[InTotoPayload] + The list of provenance. + + Returns + ------- + bool + True if the provenance could be verified, or False otherwise. + """ + if determine_abstract_purl_type(purl) == AbstractPurlType.REPOSITORY: + # Do not perform default verification for repository type targets. + return False + + verification_function = None + + if purl.type == "npm": + verification_function = partial(verify_npm_provenance, purl, provenance) + + # TODO other verification functions go here. + + if verification_function: + return verification_function() + + logger.debug("Provenance verification not supported for PURL type: %s", purl.type) + return False + + +def verify_npm_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool: + """Compare the unsigned payload subject digest with the signed payload digest, if available. + + Parameters + ---------- + purl: PackageURL + The PURL of the analysis target. + provenance: list[InTotoPayload] + The provenances to verify. + + Returns + ------- + bool + True if the provenance was verified, or False otherwise. + """ + if len(provenance) != 2: + logger.debug("Expected unsigned and signed provenance.") + return False + + signed_subjects = provenance[1].statement.get("subject") + if not signed_subjects: + return False + + unsigned_subjects = provenance[0].statement.get("subject") + if not unsigned_subjects: + return False + + found_signed_subject = None + for signed_subject in signed_subjects: + name = signed_subject.get("name") + if not name or not check_purls_equivalent(purl, PackageURL.from_string(name)): + continue + found_signed_subject = signed_subject + break + + if not found_signed_subject: + return False + + found_unsigned_subject = None + for unsigned_subject in unsigned_subjects: + name = unsigned_subject.get("name") + if not name or not check_purls_equivalent(purl, PackageURL.from_string(name)): + continue + found_unsigned_subject = unsigned_subject + break + + if not found_unsigned_subject: + return False + + signed_digest = found_signed_subject.get("digest") + unsigned_digest = found_unsigned_subject.get("digest") + if not (signed_digest and unsigned_digest): + return False + + # For signed and unsigned to match, the digests must be identical. + if signed_digest != unsigned_digest: + return False + + key = list(signed_digest.keys())[0] + logger.debug( + "Verified provenance against signed companion. Signed: %s, Unsigned: %s.", + signed_digest[key][:7], + unsigned_digest[key][:7], + ) + + return True + + +def check_purls_equivalent(original_purl: PackageURL, new_purl: PackageURL) -> bool: + """Check if `new_purl` is equivalent to `original_purl`, excluding versions if the original has none.""" + if ( + original_purl.type != new_purl.type + or original_purl.name != new_purl.name + or original_purl.namespace != new_purl.namespace + ): + return False + if original_purl.version and original_purl.version != new_purl.version: + return False + return True + + +def verify_ci_provenance(analyze_ctx: AnalyzeContext, ci_info: CIInfo, download_path: str) -> bool: + """Try to verify the CI provenance in terms of SLSA level 3 requirements. + + Involves running the SLSA verifier. + + Parameters + ---------- + analyze_ctx: AnalyzeContext + The context of the analysis. + ci_info: CIInfo + A ``CIInfo`` instance that holds a GitHub Actions git service object. + download_path: str + The location to search for downloaded files. + + Returns + ------- + bool + True if the provenance could be verified. + """ + # TODO: During verification, we need to fetch the workflow and verify that it's not + # using self-hosted runners, custom containers or services, etc. + ci_service = ci_info["service"] + for provenance in ci_info["provenances"]: + if not isinstance(provenance.payload, InTotoV01Payload): + logger.debug("Cannot verify provenance type: %s", type(provenance.payload)) + continue + + all_assets = ci_info["release"]["assets"] + + # Iterate through the subjects and verify. + for subject in provenance.payload.statement["subject"]: + sub_asset = _find_subject_asset(subject, all_assets, download_path, ci_service) + + if not sub_asset: + logger.debug("Sub asset not found for: %s.", provenance.payload.statement["subject"]) + return False + if not Path(download_path, sub_asset["name"]).is_file(): + if "size" in sub_asset and sub_asset["size"] > defaults.getint( + "slsa.verifier", "max_download_size", fallback=1000000 + ): + logger.debug("Sub asset too large to verify: %s", sub_asset["name"]) + return False + if "url" in sub_asset and not ci_service.api_client.download_asset( + sub_asset["url"], os.path.join(download_path, sub_asset["name"]) + ): + logger.debug("Sub asset not found: %s", sub_asset["name"]) + return False + + sub_verified = _verify_slsa( + analyze_ctx.macaron_path, + download_path, + provenance.asset, + sub_asset["name"], + analyze_ctx.component.repository.remote_path, + ) + + if not sub_verified: + logger.info("Sub asset not verified: %s", sub_asset["name"]) + return False + + if sub_verified: + logger.info("Successfully verified sub asset: %s", sub_asset["name"]) + + return True + + +def _find_subject_asset( + subject: v01.InTotoV01Subject, + all_assets: list[dict[str, str]], + download_path: str, + ci_service: BaseCIService, +) -> dict | None: + """Find the artifacts that appear in the provenance subject. + + The artifacts can be directly found as a release asset or in an archive file. + """ + sub_asset = next( + (item for item in all_assets if item["name"] == os.path.basename(subject["name"])), + None, + ) + + if sub_asset: + return sub_asset + + extracted_artifact = glob.glob(os.path.join(download_path, "**", os.path.basename(subject["name"])), recursive=True) + for artifact_path in extracted_artifact: + try: + with open(artifact_path, "rb") as file: + if hashlib.sha256(file.read()).hexdigest() == subject["digest"]["sha256"]: + return {"name": str(Path(artifact_path).relative_to(download_path))} + except OSError as error: + logger.error("Error in check: %s", error) + continue + + for item in all_assets: + item_path = os.path.join(download_path, item["name"]) + # Make sure to download an archive just once. + if not Path(item_path).is_file(): + # TODO: check that it's not too large. + if not ci_service.api_client.download_asset(item["url"], item_path): + logger.info("Could not download artifact %s. Skip verifying...", os.path.basename(item_path)) + break + + if _extract_archive(file_path=item_path, temp_path=download_path): + return _find_subject_asset(subject, all_assets, download_path, ci_service) + + return None + + +def _extract_archive(file_path: str, temp_path: str) -> bool: + """Extract the archive file to the temporary path. + + Returns + ------- + bool + Returns True if successful. + """ + + def _validate_path_traversal(path: str) -> bool: + """Check for path traversal attacks.""" + if path.startswith("/") or ".." in path: + logger.debug("Found suspicious path in the archive file: %s.", path) + return False + try: + # Check if there are any symbolic links. + if os.path.realpath(path): + return True + except OSError as error: + logger.debug("Failed to extract artifact from archive file: %s", error) + return False + return False + + try: + if zipfile.is_zipfile(file_path): + with zipfile.ZipFile(file_path, "r") as zip_file: + members = (path for path in zip_file.namelist() if _validate_path_traversal(path)) + zip_file.extractall(temp_path, members=members) # nosec B202:tarfile_unsafe_members + return True + elif tarfile.is_tarfile(file_path): + with tarfile.open(file_path, mode="r:gz") as tar_file: + members_tarinfo = ( + tarinfo for tarinfo in tar_file.getmembers() if _validate_path_traversal(tarinfo.name) + ) + tar_file.extractall(temp_path, members=members_tarinfo) # nosec B202:tarfile_unsafe_members + return True + except (tarfile.TarError, zipfile.BadZipFile, zipfile.LargeZipFile, OSError, ValueError) as error: + logger.info(error) + + return False + + +def _verify_slsa( + macaron_path: str, download_path: str, prov_asset: AssetLocator, asset_name: str, repository_url: str +) -> bool: + """Run SLSA verifier to verify the artifact.""" + source_path = get_repo_dir_name(repository_url, sanitize=False) + if not source_path: + logger.error("Invalid repository source path to verify: %s.", repository_url) + return False + + errors: list[str] = [] + verified = False + cmd = [ + os.path.join(macaron_path, "bin/slsa-verifier"), + "verify-artifact", + os.path.join(download_path, asset_name), + "--provenance-path", + os.path.join(download_path, prov_asset.name), + "--source-uri", + source_path, + ] + + try: + verifier_output = subprocess.run( # nosec B603 + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + cwd=download_path, + timeout=defaults.getint("slsa.verifier", "timeout", fallback=120), + ) + output = verifier_output.stdout.decode("utf-8") + verified = "PASSED: SLSA verification passed" in output + log_path = os.path.join(global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.log") + with open(log_path, mode="a", encoding="utf-8") as log_file: + logger.info("Storing SLSA verifier output for %s to %s", asset_name, os.path.relpath(log_path, os.getcwd())) + log_file.writelines( + [f"SLSA verifier output for cmd: {' '.join(cmd)}\n", output, "--------------------------------\n"] + ) + + except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: + logger.error(error) + errors.append(error.output.decode("utf-8")) + except OSError as error: + logger.error(error) + errors.append(str(error)) + + if errors: + verified = False + try: + error_log_path = os.path.join( + global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.errors" + ) + with open(error_log_path, mode="a", encoding="utf-8") as log_file: + logger.info( + "Storing SLSA verifier log for%s to %s", asset_name, os.path.relpath(error_log_path, os.getcwd()) + ) + log_file.write(f"SLSA verifier output for cmd: {' '.join(cmd)}\n") + log_file.writelines(errors) + log_file.write("--------------------------------\n") + except OSError as error: + logger.error(error) + + return verified + + +def determine_provenance_slsa_level( + ctx: AnalyzeContext, provenance_payload: InTotoPayload | None, verified: bool, verified_l3: bool +) -> int: + """Implement the check in this method. + + Parameters + ---------- + ctx : AnalyzeContext + The object containing processed data for the target repo. + provenance_payload: dict | None + The provenance payload. + verified: bool + True if the provenance content is verified. + verified_l3: bool + True if the provenance content is level 3 verified. + + Returns + ------- + int + The SLSA level. + """ + if not provenance_payload or ctx.dynamic_data["is_inferred_prov"]: + # 0. Provenance is not available. + return 0 + + predicate = provenance_payload.statement.get("predicate") + build_type = None + if predicate: + build_type = ProvenancePredicate.get_build_type(provenance_payload.statement) + + if build_type in {SLSAGithubGenericBuildDefinitionV01.expected_build_type} and verified_l3: + # 3. Provenance is created by the SLSA GitHub generator and verified. + return 3 + + if verified: + # 2. Provenance is verified. + return 2 + + # 1. Provenance is not verified. + return 1 diff --git a/src/macaron/repo_finder/__init__.py b/src/macaron/repo_finder/__init__.py index dfccaa6a9..6221b357c 100644 --- a/src/macaron/repo_finder/__init__.py +++ b/src/macaron/repo_finder/__init__.py @@ -1,7 +1,7 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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 package contains the dependency resolvers for Java projects.""" +"""This package contains the repository and commit finding tools for software components.""" def to_domain_from_known_purl_types(purl_type: str) -> str | None: diff --git a/src/macaron/repo_finder/repo_finder.py b/src/macaron/repo_finder/repo_finder.py index a43fadc2d..f98f2688e 100644 --- a/src/macaron/repo_finder/repo_finder.py +++ b/src/macaron/repo_finder/repo_finder.py @@ -103,6 +103,9 @@ def find_repo(purl: PackageURL, check_latest_version: bool = True) -> tuple[str, logger.debug("Analyzing %s with Repo Finder: %s", purl, type(repo_finder)) found_repo, outcome = repo_finder.find_repo(purl) + if check_latest_version and not defaults.getboolean("repofinder", "try_latest_purl", fallback=True): + check_latest_version = False + if found_repo or not check_latest_version: return found_repo, outcome diff --git a/src/macaron/repo_finder/repo_finder_deps_dev.py b/src/macaron/repo_finder/repo_finder_deps_dev.py index c64ff37aa..35d257408 100644 --- a/src/macaron/repo_finder/repo_finder_deps_dev.py +++ b/src/macaron/repo_finder/repo_finder_deps_dev.py @@ -146,8 +146,16 @@ def get_latest_version(purl: PackageURL) -> tuple[PackageURL | None, RepoFinderI versions = json_extract(metadata, versions_keys, list) if not versions: return None, RepoFinderInfo.DDEV_JSON_INVALID - latest_version = json_extract(versions[-1], ["versionKey", "version"], str) + + latest_version = None + for version_result in reversed(versions): + if version_result["isDefault"]: + # Accept the version as the latest if it is marked with the "isDefault" property. + latest_version = json_extract(version_result, ["versionKey", "version"], str) + break + if not latest_version: + logger.debug("No latest version found in version list: %s", len(versions)) return None, RepoFinderInfo.DDEV_JSON_INVALID namespace = purl.namespace + "/" if purl.namespace else "" diff --git a/src/macaron/repo_finder/repo_utils.py b/src/macaron/repo_finder/repo_utils.py index 0f9ca2683..e1b0be7af 100644 --- a/src/macaron/repo_finder/repo_utils.py +++ b/src/macaron/repo_finder/repo_utils.py @@ -75,7 +75,7 @@ def generate_report(purl: str, commit: str, repo: str, target_dir: str) -> bool: fullpath = f"{target_dir}/{filename}" os.makedirs(os.path.dirname(fullpath), exist_ok=True) - logger.info("Writing report to: %s", fullpath) + logger.info("Writing report to: %s", os.path.relpath(fullpath, os.getcwd())) try: with open(fullpath, "w", encoding="utf-8") as file: @@ -84,7 +84,7 @@ def generate_report(purl: str, commit: str, repo: str, target_dir: str) -> bool: logger.debug("Failed to write report to file: %s", error) return False - logger.info("Report written to: %s", fullpath) + logger.info("Report written to: %s", os.path.relpath(fullpath, os.getcwd())) return True diff --git a/src/macaron/slsa_analyzer/analyze_context.py b/src/macaron/slsa_analyzer/analyze_context.py index c2f6a0042..84d8151f2 100644 --- a/src/macaron/slsa_analyzer/analyze_context.py +++ b/src/macaron/slsa_analyzer/analyze_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 Analyze Context class. @@ -11,7 +11,7 @@ from collections import defaultdict from typing import Any, TypedDict -from macaron.database.table_definitions import Component, SLSALevel +from macaron.database.table_definitions import Component, Provenance, SLSALevel from macaron.repo_verifier.repo_verifier import RepositoryVerificationResult from macaron.slsa_analyzer.checks.check_result import CheckResult, CheckResultType from macaron.slsa_analyzer.ci_service.base_ci_service import BaseCIService @@ -19,7 +19,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, InTotoV01Payload +from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload 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 @@ -47,17 +47,11 @@ class ChecksOutputs(TypedDict): """The expectation to verify the provenance for the target software component.""" package_registries: list[PackageRegistryInfo] """The package registries for the target software component.""" - provenance: InTotoPayload | None - """The provenance payload for the target software component.""" - provenance_repo_url: str | None - """The repository URL extracted from provenance, if applicable.""" - provenance_commit_digest: str | None - """The commit digest extracted from provenance, if applicable.""" - provenance_verified: bool - """True if the provenance exists and has been verified against a signed companion provenance.""" + provenance_info: Provenance | None + """The provenance and related information.""" local_artifact_paths: list[str] """The local artifact absolute paths.""" - validate_malware_switch: bool + validate_malware: bool """True when the malware validation is enabled.""" @@ -110,12 +104,9 @@ def __init__( package_registries=[], is_inferred_prov=True, expectation=None, - provenance=None, - provenance_repo_url=None, - provenance_commit_digest=None, - provenance_verified=False, + provenance_info=None, local_artifact_paths=[], - validate_malware_switch=False, + validate_malware=False, ) @property diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index fb3adb33b..d17567110 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -8,6 +8,7 @@ import os import re import sys +import tempfile from collections.abc import Mapping from datetime import datetime, timezone from pathlib import Path @@ -20,11 +21,17 @@ from macaron import __version__ from macaron.artifact.local_artifact import get_local_artifact_dirs -from macaron.config.defaults import defaults from macaron.config.global_config import global_config from macaron.config.target_config import Configuration from macaron.database.database_manager import DatabaseManager, get_db_manager, get_db_session -from macaron.database.table_definitions import Analysis, Component, ProvenanceSubject, RepoFinderMetadata, Repository +from macaron.database.table_definitions import ( + Analysis, + Component, + Provenance, + ProvenanceSubject, + RepoFinderMetadata, + Repository, +) from macaron.dependency_analyzer.cyclonedx import DependencyAnalyzer, DependencyInfo from macaron.errors import ( DuplicateError, @@ -36,13 +43,16 @@ ) from macaron.output_reporter.reporter import FileReporter from macaron.output_reporter.results import Record, Report, SCMStatus -from macaron.repo_finder import repo_finder -from macaron.repo_finder.provenance_extractor import ( +from macaron.provenance import provenance_verifier +from macaron.provenance.provenance_extractor import ( check_if_input_purl_provenance_conflict, check_if_input_repo_provenance_conflict, + extract_predicate_version, extract_repo_and_commit_from_provenance, ) -from macaron.repo_finder.provenance_finder import ProvenanceFinder, find_provenance_from_ci +from macaron.provenance.provenance_finder import ProvenanceFinder, find_provenance_from_ci +from macaron.provenance.provenance_verifier import determine_provenance_slsa_level, verify_ci_provenance +from macaron.repo_finder import repo_finder from macaron.repo_finder.repo_finder import prepare_repo from macaron.repo_finder.repo_finder_enums import CommitFinderInfo, RepoFinderInfo from macaron.repo_finder.repo_utils import get_git_service @@ -65,7 +75,7 @@ from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.registry import registry from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo logger: logging.Logger = logging.getLogger(__name__) @@ -126,7 +136,8 @@ def run( sbom_path: str = "", deps_depth: int = 0, provenance_payload: InTotoPayload | None = None, - validate_malware_switch: bool = False, + validate_malware: bool = False, + verify_provenance: bool = False, ) -> int: """Run the analysis and write results to the output path. @@ -143,6 +154,10 @@ def run( The depth of dependency resolution. Default: 0. provenance_payload : InToToPayload | None The provenance intoto payload for the main software component. + validate_malware: bool + Enable malware validation if True. + verify_provenance: bool + Enable provenance verification if True. Returns ------- @@ -175,7 +190,8 @@ def run( main_config, analysis, provenance_payload=provenance_payload, - validate_malware_switch=validate_malware_switch, + validate_malware=validate_malware, + verify_provenance=verify_provenance, ) if main_record.status != SCMStatus.AVAILABLE or not main_record.context: @@ -293,7 +309,8 @@ def run_single( analysis: Analysis, existing_records: dict[str, Record] | None = None, provenance_payload: InTotoPayload | None = None, - validate_malware_switch: bool = False, + validate_malware: bool = False, + verify_provenance: bool = False, ) -> Record: """Run the checks for a single repository target. @@ -310,6 +327,10 @@ def run_single( The mapping of existing records that the analysis has run successfully. provenance_payload : InToToPayload | None The provenance intoto payload for the analyzed software component. + validate_malware: bool + Enable malware validation if True. + verify_provenance: bool + Enable provenance verification if True. Returns ------- @@ -339,12 +360,11 @@ def run_single( provenances = provenance_finder.find_provenance(parsed_purl) if provenances: provenance_payload = provenances[0] - if defaults.getboolean("analyzer", "verify_provenance"): - provenance_is_verified = provenance_finder.verify_provenance(parsed_purl, provenances) + if verify_provenance: + provenance_is_verified = provenance_verifier.verify_provenance(parsed_purl, provenances) # Try to extract the repository URL and commit digest from the Provenance, if it exists. repo_path_input: str | None = config.get_value("path") - digest_input: str | None = config.get_value("digest") provenance_repo_url = provenance_commit_digest = None if provenance_payload: try: @@ -352,10 +372,8 @@ def run_single( provenance_payload ) except ProvenanceError as error: - logger.debug("Failed to extract repo or commit from provenance: %s", error) - - # Try to validate the input repo against provenance contents. - if provenance_repo_url and check_if_input_repo_provenance_conflict(repo_path_input, provenance_repo_url): + logger.debug("Failed to extract from provenance: %s", error) + if check_if_input_repo_provenance_conflict(repo_path_input, provenance_repo_url): return Record( record_id=repo_id, description="Input mismatch between repo and provenance.", @@ -400,18 +418,15 @@ def run_single( ) # Check if only one of the repo or digest came from direct input. - if git_obj and (provenance_repo_url or provenance_commit_digest) and parsed_purl: + if parsed_purl: if check_if_input_purl_provenance_conflict( - git_obj, bool(repo_path_input), - bool(digest_input), provenance_repo_url, - provenance_commit_digest, parsed_purl, ): return Record( record_id=repo_id, - description="Input mismatch between repo/commit (purl) and provenance.", + description="Input mismatch between repo (purl) and provenance.", pre_config=config, status=SCMStatus.ANALYSIS_FAILED, ) @@ -461,42 +476,62 @@ def run_single( self._verify_repository_link(parsed_purl, analyze_ctx) self._determine_package_registries(analyze_ctx) + provenance_l3_verified = False if not provenance_payload: # Look for provenance using the CI. - provenance_payload = find_provenance_from_ci(analyze_ctx, git_obj) - # If found, verify analysis target against new provenance - if provenance_payload: - # If repository URL was not provided as input, check the one found during analysis. - if not repo_path_input and component.repository: - repo_path_input = component.repository.remote_path - - # Extract the digest and repository URL from provenance. - provenance_repo_url = provenance_commit_digest = None - try: - provenance_repo_url, provenance_commit_digest = extract_repo_and_commit_from_provenance( - provenance_payload - ) - except ProvenanceError as error: - logger.debug("Failed to extract repo or commit from provenance: %s", error) - - # Try to validate the input repo against provenance contents. - if provenance_repo_url and check_if_input_repo_provenance_conflict( - repo_path_input, provenance_repo_url - ): - return Record( - record_id=repo_id, - description="Input mismatch between repo/commit and provenance.", - pre_config=config, - status=SCMStatus.ANALYSIS_FAILED, - ) + with tempfile.TemporaryDirectory() as temp_dir: + provenance_payload = find_provenance_from_ci(analyze_ctx, git_obj, temp_dir) + # If found, validate analysis target against new provenance. + if provenance_payload: + # If repository URL was not provided as input, check the one found during analysis. + if not repo_path_input and component.repository: + repo_path_input = component.repository.remote_path + provenance_repo_url = provenance_commit_digest = None + try: + provenance_repo_url, provenance_commit_digest = extract_repo_and_commit_from_provenance( + provenance_payload + ) + except ProvenanceError as error: + logger.debug("Failed to extract from provenance: %s", error) + + if check_if_input_repo_provenance_conflict(repo_path_input, provenance_repo_url): + return Record( + record_id=repo_id, + description="Input mismatch between repo/commit and provenance.", + pre_config=config, + status=SCMStatus.ANALYSIS_FAILED, + ) + + # Also try to verify CI provenance contents. + if verify_provenance: + verified = [] + for ci_info in analyze_ctx.dynamic_data["ci_services"]: + verified.append(verify_ci_provenance(analyze_ctx, ci_info, temp_dir)) + if not verified: + break + if verified and all(verified): + provenance_l3_verified = True - analyze_ctx.dynamic_data["provenance"] = provenance_payload if provenance_payload: analyze_ctx.dynamic_data["is_inferred_prov"] = False - analyze_ctx.dynamic_data["provenance_verified"] = provenance_is_verified - analyze_ctx.dynamic_data["provenance_repo_url"] = provenance_repo_url - analyze_ctx.dynamic_data["provenance_commit_digest"] = provenance_commit_digest - analyze_ctx.dynamic_data["validate_malware_switch"] = validate_malware_switch + slsa_version = extract_predicate_version(provenance_payload) + + slsa_level = determine_provenance_slsa_level( + analyze_ctx, provenance_payload, provenance_is_verified, provenance_l3_verified + ) + + analyze_ctx.dynamic_data["provenance_info"] = Provenance( + component=component, + repository_url=provenance_repo_url, + commit_sha=provenance_commit_digest, + verified=provenance_is_verified, + provenance_payload=provenance_payload, + slsa_level=slsa_level, + slsa_version=slsa_version, + # TODO Add release tag, release digest. + ) + + analyze_ctx.dynamic_data["validate_malware"] = validate_malware if parsed_purl and parsed_purl.type in self.local_artifact_repo_mapper: local_artifact_repo_path = self.local_artifact_repo_mapper[parsed_purl.type] @@ -959,10 +994,7 @@ def _determine_ci_services(self, analyze_ctx: AnalyzeContext, git_service: BaseG # Parse configuration files and generate IRs. # Add the bash commands to the context object to be used by other checks. - callgraph = ci_service.build_call_graph( - analyze_ctx.component.repository.fs_path, - os.path.relpath(analyze_ctx.component.repository.fs_path, analyze_ctx.output_dir), - ) + callgraph = ci_service.build_call_graph(analyze_ctx.component.repository.fs_path) analyze_ctx.dynamic_data["ci_services"].append( CIInfo( service=ci_service, @@ -971,11 +1003,11 @@ def _determine_ci_services(self, analyze_ctx: AnalyzeContext, git_service: BaseG release={}, provenances=[ SLSAProvenanceData( - payload=InTotoV01Payload(statement=Provenance().payload), + payload=InTotoV01Payload(statement=InferredProvenance().payload), asset=VirtualReleaseAsset(name="No_ASSET", url="NO_URL", size_in_bytes=0), ) ], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ) diff --git a/src/macaron/slsa_analyzer/build_tool/base_build_tool.py b/src/macaron/slsa_analyzer/build_tool/base_build_tool.py index db0fff3cb..24c53c5c4 100644 --- a/src/macaron/slsa_analyzer/build_tool/base_build_tool.py +++ b/src/macaron/slsa_analyzer/build_tool/base_build_tool.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 BaseBuildTool class to be inherited by other specific Build Tools.""" @@ -44,7 +44,7 @@ class BuildToolCommand(TypedDict): ci_path: str #: The CI step object that calls the command. - step_node: BaseNode + step_node: BaseNode | None #: The list of name of reachable variables that contain secrets.""" reachable_secrets: list[str] diff --git a/src/macaron/slsa_analyzer/checks/build_as_code_check.py b/src/macaron/slsa_analyzer/checks/build_as_code_check.py index df00ef2b3..1348b1307 100644 --- a/src/macaron/slsa_analyzer/checks/build_as_code_check.py +++ b/src/macaron/slsa_analyzer/checks/build_as_code_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 BuildAsCodeCheck class.""" @@ -15,7 +15,7 @@ from macaron.errors import CallGraphError, ProvenanceError from macaron.parsers.bashparser import BashNode from macaron.parsers.github_workflow_model import ActionStep -from macaron.repo_finder.provenance_extractor import ProvenancePredicate +from macaron.provenance.provenance_extractor import ProvenancePredicate from macaron.slsa_analyzer.analyze_context import AnalyzeContext, store_inferred_build_info_results from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType @@ -27,7 +27,6 @@ GitHubWorkflowType, ) from macaron.slsa_analyzer.ci_service.gitlab_ci import GitLabCI -from macaron.slsa_analyzer.ci_service.jenkins import Jenkins from macaron.slsa_analyzer.ci_service.travis import Travis from macaron.slsa_analyzer.registry import registry from macaron.slsa_analyzer.slsa_req import ReqName @@ -124,7 +123,9 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # If a provenance is found, obtain the workflow that has triggered the artifact release. prov_workflow = None - prov_payload = ctx.dynamic_data["provenance"] + prov_payload = None + if ctx.dynamic_data["provenance_info"]: + prov_payload = ctx.dynamic_data["provenance_info"].provenance_payload if not ctx.dynamic_data["is_inferred_prov"] and prov_payload: try: build_def = ProvenancePredicate.find_build_def(prov_payload.statement) @@ -262,10 +263,11 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: trigger_link=trigger_link, job_id=( build_command["step_node"].caller.name - if isinstance(build_command["step_node"].caller, GitHubJobNode) + if build_command["step_node"] + and isinstance(build_command["step_node"].caller, GitHubJobNode) else None ), - step_id=build_command["step_node"].node_id, + step_id=build_command["step_node"].node_id if build_command["step_node"] else None, step_name=( build_command["step_node"].name if isinstance(build_command["step_node"], BashNode) @@ -299,7 +301,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # We currently don't parse these CI configuration files. # We just look for a keyword for now. - for unparsed_ci in (Jenkins, Travis, CircleCI, GitLabCI): + for unparsed_ci in (Travis, CircleCI, GitLabCI): if isinstance(ci_service, unparsed_ci): if tool.ci_deploy_kws[ci_service.name]: deploy_kw, config_name = ci_service.has_kws_in_config( diff --git a/src/macaron/slsa_analyzer/checks/build_script_check.py b/src/macaron/slsa_analyzer/checks/build_script_check.py index 14d02675e..44f675ce4 100644 --- a/src/macaron/slsa_analyzer/checks/build_script_check.py +++ b/src/macaron/slsa_analyzer/checks/build_script_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 BuildScriptCheck class.""" @@ -27,6 +27,10 @@ class BuildScriptFacts(CheckFacts): __tablename__ = "_build_script_check" + # This check is disabled here due to a bug in pylint. The Mapped class triggers a false positive. + # It may arbitrarily become true that this is no longer needed in this check, or will be needed in another check. + # pylint: disable=unsubscriptable-object + #: The primary key. id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 diff --git a/src/macaron/slsa_analyzer/checks/build_service_check.py b/src/macaron/slsa_analyzer/checks/build_service_check.py index abbef2f35..cea689a7c 100644 --- a/src/macaron/slsa_analyzer/checks/build_service_check.py +++ b/src/macaron/slsa_analyzer/checks/build_service_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 BuildServiceCheck class.""" @@ -18,7 +18,6 @@ from macaron.slsa_analyzer.ci_service.base_ci_service import BaseCIService, NoneCIService from macaron.slsa_analyzer.ci_service.circleci import CircleCI from macaron.slsa_analyzer.ci_service.gitlab_ci import GitLabCI -from macaron.slsa_analyzer.ci_service.jenkins import Jenkins from macaron.slsa_analyzer.ci_service.travis import Travis from macaron.slsa_analyzer.registry import registry from macaron.slsa_analyzer.slsa_req import ReqName @@ -170,7 +169,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # We currently don't parse these CI configuration files. # We just look for a keyword for now. - for unparsed_ci in (Jenkins, Travis, CircleCI, GitLabCI): + for unparsed_ci in (Travis, CircleCI, GitLabCI): if isinstance(ci_service, unparsed_ci): if tool.ci_build_kws[ci_service.name]: build_kw, config_name = ci_service.has_kws_in_config( diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index 040daca85..80439bb79 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -6,6 +6,9 @@ import logging import requests +from problog import get_evaluatable +from problog.logic import Term +from problog.program import PrologString from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column @@ -65,184 +68,6 @@ class MaliciousMetadataFacts(CheckFacts): } -# This list contains the heuristic analyzer classes -# When implementing new analyzer, appending the classes to this list -ANALYZERS: list = [ - EmptyProjectLinkAnalyzer, - SourceCodeRepoAnalyzer, - OneReleaseAnalyzer, - HighReleaseFrequencyAnalyzer, - UnchangedReleaseAnalyzer, - CloserReleaseJoinDateAnalyzer, - SuspiciousSetupAnalyzer, - WheelAbsenceAnalyzer, - AnomalousVersionAnalyzer, -] - - -# The HeuristicResult sequence is aligned with the sequence of ANALYZERS list -SUSPICIOUS_COMBO: dict[ - tuple[ - HeuristicResult, - HeuristicResult, - HeuristicResult, - HeuristicResult, - HeuristicResult, - HeuristicResult, - HeuristicResult, - HeuristicResult, - HeuristicResult, - ], - float, -] = { - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.FAIL, # One Release - HeuristicResult.SKIP, # High Release Frequency - HeuristicResult.SKIP, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.FAIL, # Suspicious Setup - HeuristicResult.FAIL, # Wheel Absence - HeuristicResult.FAIL, # Anomalous Version - # No project link, only one release, and the maintainer released it shortly - # after account registration. - # The setup.py file contains suspicious imports and .whl file isn't present. - # Anomalous version has no effect. - ): Confidence.HIGH, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.FAIL, # One Release - HeuristicResult.SKIP, # High Release Frequency - HeuristicResult.SKIP, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.FAIL, # Suspicious Setup - HeuristicResult.FAIL, # Wheel Absence - HeuristicResult.PASS, # Anomalous Version - # No project link, only one release, and the maintainer released it shortly - # after account registration. - # The setup.py file contains suspicious imports and .whl file isn't present. - # Anomalous version has no effect. - ): Confidence.HIGH, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.PASS, # One Release - HeuristicResult.FAIL, # High Release Frequency - HeuristicResult.FAIL, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.FAIL, # Suspicious Setup - HeuristicResult.FAIL, # Wheel Absence - HeuristicResult.SKIP, # Anomalous Version - # No project link, frequent releases of multiple versions without modifying the content, - # and the maintainer released it shortly after account registration. - # The setup.py file contains suspicious imports and .whl file isn't present. - ): Confidence.HIGH, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.PASS, # One Release - HeuristicResult.FAIL, # High Release Frequency - HeuristicResult.PASS, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.FAIL, # Suspicious Setup - HeuristicResult.FAIL, # Wheel Absence - HeuristicResult.SKIP, # Anomalous Version - # No project link, frequent releases of multiple versions, - # and the maintainer released it shortly after account registration. - # The setup.py file contains suspicious imports and .whl file isn't present. - ): Confidence.HIGH, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.PASS, # One Release - HeuristicResult.FAIL, # High Release Frequency - HeuristicResult.FAIL, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.PASS, # Suspicious Setup - HeuristicResult.PASS, # Wheel Absence - HeuristicResult.SKIP, # Anomalous Version - # No project link, frequent releases of multiple versions without modifying the content, - # and the maintainer released it shortly after account registration. Presence/Absence of - # .whl file has no effect - ): Confidence.MEDIUM, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.PASS, # One Release - HeuristicResult.FAIL, # High Release Frequency - HeuristicResult.FAIL, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.PASS, # Suspicious Setup - HeuristicResult.FAIL, # Wheel Absence - HeuristicResult.SKIP, # Anomalous Version - # No project link, frequent releases of multiple versions without modifying the content, - # and the maintainer released it shortly after account registration. Presence/Absence of - # .whl file has no effect - ): Confidence.MEDIUM, - ( - HeuristicResult.PASS, # Empty Project - HeuristicResult.FAIL, # Source Code Repo - HeuristicResult.PASS, # One Release - HeuristicResult.FAIL, # High Release Frequency - HeuristicResult.PASS, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.FAIL, # Suspicious Setup - HeuristicResult.FAIL, # Wheel Absence - HeuristicResult.SKIP, # Anomalous Version - # No source code repo, frequent releases of multiple versions, - # and the maintainer released it shortly after account registration. - # The setup.py file contains suspicious imports and .whl file isn't present. - ): Confidence.HIGH, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.FAIL, # One Release - HeuristicResult.SKIP, # High Release Frequency - HeuristicResult.SKIP, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.PASS, # Suspicious Setup - HeuristicResult.PASS, # Wheel Absence - HeuristicResult.FAIL, # Anomalous Version - # No project link, only one release, and the maintainer released it shortly - # after account registration. - # The setup.py file has no effect and .whl file is present. - # The version number is anomalous. - ): Confidence.MEDIUM, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.FAIL, # One Release - HeuristicResult.SKIP, # High Release Frequency - HeuristicResult.SKIP, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.FAIL, # Suspicious Setup - HeuristicResult.PASS, # Wheel Absence - HeuristicResult.FAIL, # Anomalous Version - # No project link, only one release, and the maintainer released it shortly - # after account registration. - # The setup.py file has no effect and .whl file is present. - # The version number is anomalous. - ): Confidence.MEDIUM, - ( - HeuristicResult.FAIL, # Empty Project - HeuristicResult.SKIP, # Source Code Repo - HeuristicResult.FAIL, # One Release - HeuristicResult.SKIP, # High Release Frequency - HeuristicResult.SKIP, # Unchanged Release - HeuristicResult.FAIL, # Closer Release Join Date - HeuristicResult.SKIP, # Suspicious Setup - HeuristicResult.PASS, # Wheel Absence - HeuristicResult.FAIL, # Anomalous Version - # No project link, only one release, and the maintainer released it shortly - # after account registration. - # The setup.py file has no effect and .whl file is present. - # The version number is anomalous. - ): Confidence.MEDIUM, -} - - class DetectMaliciousMetadataCheck(BaseCheck): """This check analyzes the metadata of a package for malicious behavior.""" @@ -303,6 +128,41 @@ def validate_malware(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[boo is_malware, detail_info = sourcecode_analyzer.analyze() return is_malware, detail_info + def evaluate_heuristic_results(self, heuristic_results: dict[Heuristics, HeuristicResult]) -> float | None: + """Analyse the heuristic results to determine the maliciousness of the package. + + Parameters + ---------- + heuristic_results: dict[Heuristics, HeuristicResult] + Dictionary of Heuristic keys with HeuristicResult values, results of each heuristic scan. + + Returns + ------- + float | None + Returns the confidence associated with the detected malicious combination, otherwise None if no associated + malicious combination was triggered. + """ + facts_list: list[str] = [] + for heuristic, result in heuristic_results.items(): + if result == HeuristicResult.SKIP: + facts_list.append(f"0.0::{heuristic.value}.") + elif result == HeuristicResult.PASS: + facts_list.append(f"{heuristic.value} :- true.") + else: # HeuristicResult.FAIL + facts_list.append(f"{heuristic.value} :- false.") + + facts = "\n".join(facts_list) + problog_code = f"{facts}\n\n{self.malware_rules_problog_model}" + logger.debug("Problog model used for evaluation:\n %s", problog_code) + + problog_model = PrologString(problog_code) + problog_results: dict[Term, float] = get_evaluatable().create_from(problog_model).evaluate() + + confidence: float | None = problog_results.get(Term(self.problog_result_access)) + if confidence == 0.0: + return None # no rules were triggered + return confidence + def run_heuristics( self, pypi_package_json: PyPIPackageJsonAsset ) -> tuple[dict[Heuristics, HeuristicResult], dict[str, JsonType]]: @@ -326,7 +186,7 @@ def run_heuristics( results: dict[Heuristics, HeuristicResult] = {} detail_info: dict[str, JsonType] = {} - for _analyzer in ANALYZERS: + for _analyzer in self.analyzers: analyzer: BaseHeuristicAnalyzer = _analyzer() logger.debug("Instantiating %s", _analyzer.__name__) @@ -418,13 +278,12 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: except HeuristicAnalyzerValueError: return CheckResultData(result_tables=[], result_type=CheckResultType.UNKNOWN) - result_combo: tuple = tuple(result.values()) - confidence: float | None = SUSPICIOUS_COMBO.get(result_combo, None) + confidence = self.evaluate_heuristic_results(result) result_type = CheckResultType.FAILED if confidence is None: confidence = Confidence.HIGH result_type = CheckResultType.PASSED - elif ctx.dynamic_data["validate_malware_switch"]: + elif ctx.dynamic_data["validate_malware"]: is_malware, validation_result = self.validate_malware(pypi_package_json) if is_malware: # Find source code block matched the malicious pattern confidence = Confidence.HIGH @@ -448,5 +307,66 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # Return UNKNOWN result for unsupported ecosystems. return CheckResultData(result_tables=[], result_type=CheckResultType.UNKNOWN) + # This list contains the heuristic analyzer classes + # When implementing new analyzer, appending the classes to this list + analyzers: list = [ + EmptyProjectLinkAnalyzer, + SourceCodeRepoAnalyzer, + OneReleaseAnalyzer, + HighReleaseFrequencyAnalyzer, + UnchangedReleaseAnalyzer, + CloserReleaseJoinDateAnalyzer, + SuspiciousSetupAnalyzer, + WheelAbsenceAnalyzer, + AnomalousVersionAnalyzer, + ] + + problog_result_access = "result" + + malware_rules_problog_model = f""" + % Heuristic groupings + % These are common combinations of heuristics that are used in many of the rules, thus themselves representing + % certain behaviors. When changing or adding rules here, if there are frequent combinations of particular + % heuristics, group them together here. + + % Maintainer has recently joined, publishing an undetailed page with no links. + quickUndetailed :- not {Heuristics.EMPTY_PROJECT_LINK.value}, not {Heuristics.CLOSER_RELEASE_JOIN_DATE.value}. + + % Maintainer releases a suspicious setup.py and forces it to run by omitting a .whl file. + forceSetup :- not {Heuristics.SUSPICIOUS_SETUP.value}, not {Heuristics.WHEEL_ABSENCE.value}. + + % Suspicious Combinations + + % Package released recently with little detail, forcing the setup.py to run. + {Confidence.HIGH.value}::high :- quickUndetailed, forceSetup, not {Heuristics.ONE_RELEASE.value}. + {Confidence.HIGH.value}::high :- quickUndetailed, forceSetup, not {Heuristics.HIGH_RELEASE_FREQUENCY.value}. + + % Package released recently with little detail, with some more refined trust markers introduced: project links, + % multiple different releases, but there is no source code repository matching it and the setup is suspicious. + {Confidence.HIGH.value}::high :- not {Heuristics.SOURCE_CODE_REPO.value}, + not {Heuristics.HIGH_RELEASE_FREQUENCY.value}, + not {Heuristics.CLOSER_RELEASE_JOIN_DATE.value}, + {Heuristics.UNCHANGED_RELEASE.value}, + forceSetup. + + % Package released recently with little detail, with multiple releases as a trust marker, but frequent and with + % the same code. + {Confidence.MEDIUM.value}::medium :- quickUndetailed, + not {Heuristics.HIGH_RELEASE_FREQUENCY.value}, + not {Heuristics.UNCHANGED_RELEASE.value}, + {Heuristics.SUSPICIOUS_SETUP.value}. + + % Package released recently with little detail and an anomalous version number for a single-release package. + {Confidence.MEDIUM.value}::medium :- quickUndetailed, + not {Heuristics.ONE_RELEASE.value}, + {Heuristics.WHEEL_ABSENCE.value}, + not {Heuristics.ANOMALOUS_VERSION.value}. + + {problog_result_access} :- high. + {problog_result_access} :- medium. + + query({problog_result_access}). + """ + registry.register(DetectMaliciousMetadataCheck()) diff --git a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py index 8902d6ef2..96f83cefc 100644 --- a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py +++ b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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 InferArtifactPipelineCheck class to check if an artifact is published from a pipeline automatically.""" @@ -14,7 +14,7 @@ from macaron.database.table_definitions import CheckFacts from macaron.errors import GitHubActionsValueError, InvalidHTTPResponseError, ProvenanceError from macaron.json_tools import json_extract -from macaron.repo_finder.provenance_extractor import ProvenancePredicate +from macaron.provenance.provenance_extractor import ProvenancePredicate from macaron.slsa_analyzer.analyze_context import AnalyzeContext from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType @@ -155,7 +155,9 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # If a provenance is found, obtain the workflow and the pipeline that has triggered the artifact release. prov_workflow = None prov_trigger_run = None - prov_payload = ctx.dynamic_data["provenance"] + prov_payload = None + if ctx.dynamic_data["provenance_info"]: + prov_payload = ctx.dynamic_data["provenance_info"].provenance_payload if not ctx.dynamic_data["is_inferred_prov"] and prov_payload: # Obtain the build-related fields from the provenance. try: @@ -194,7 +196,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # Obtain the job and step calling the deploy command. # This data must have been found already by the build-as-code check. build_predicate = ci_info["build_info_results"].statement["predicate"] - if build_predicate is None: + if build_predicate is None or build_predicate["buildType"] != f"Custom {ci_service.name}": continue build_entry_point = json_extract(build_predicate, ["invocation", "configSource", "entryPoint"], str) diff --git a/src/macaron/slsa_analyzer/checks/provenance_available_check.py b/src/macaron/slsa_analyzer/checks/provenance_available_check.py index b67e5940d..77fcf87fe 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_available_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_available_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 Provenance Available check.""" @@ -74,7 +74,11 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - available = ctx.dynamic_data["provenance"] and not ctx.dynamic_data["is_inferred_prov"] + available = ( + ctx.dynamic_data["provenance_info"] + and ctx.dynamic_data["provenance_info"].provenance_payload + and not ctx.dynamic_data["is_inferred_prov"] + ) return CheckResultData( result_tables=[ ProvenanceAvailableFacts( diff --git a/src/macaron/slsa_analyzer/checks/provenance_commit_check.py b/src/macaron/slsa_analyzer/checks/provenance_commit_check.py index c7420cc34..61fb6b8a3 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_commit_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_commit_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 adds a check that determines whether the repository URL came from provenance.""" @@ -22,6 +22,10 @@ class ProvenanceDerivedCommitFacts(CheckFacts): __tablename__ = "_provenance_derived_commit_check" + # This check is disabled here due to a bug in pylint. The Mapped class triggers a false positive. + # It may arbitrarily become true that this is no longer needed in this check, or will be needed in another check. + # pylint: disable=unsubscriptable-object + #: The primary key. id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 @@ -63,7 +67,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - if ctx.dynamic_data["provenance_commit_digest"]: + if ctx.dynamic_data["provenance_info"] and ctx.dynamic_data["provenance_info"].commit_sha: if not ctx.component.repository: return CheckResultData( result_tables=[], @@ -72,7 +76,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: current_commit = ctx.component.repository.commit_sha - if current_commit == ctx.dynamic_data["provenance_commit_digest"]: + if current_commit == ctx.dynamic_data["provenance_info"].commit_sha: return CheckResultData( result_tables=[ ProvenanceDerivedCommitFacts( diff --git a/src/macaron/slsa_analyzer/checks/provenance_l3_check.py b/src/macaron/slsa_analyzer/checks/provenance_l3_check.py deleted file mode 100644 index e34a4ce0b..000000000 --- a/src/macaron/slsa_analyzer/checks/provenance_l3_check.py +++ /dev/null @@ -1,460 +0,0 @@ -# Copyright (c) 2022 - 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 implements a check to verify a target repo has intoto provenance level 3.""" - -import glob -import hashlib -import json -import logging -import os -import subprocess # nosec B404 -import tarfile -import tempfile -import zipfile -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from typing import NamedTuple - -from sqlalchemy import ForeignKey -from sqlalchemy.orm import Mapped, mapped_column - -from macaron.config.defaults import defaults -from macaron.config.global_config import global_config -from macaron.database.table_definitions import CheckFacts, HashDigest, Provenance, ReleaseArtifact -from macaron.slsa_analyzer.analyze_context import AnalyzeContext -from macaron.slsa_analyzer.asset import AssetLocator -from macaron.slsa_analyzer.checks.base_check import BaseCheck -from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence -from macaron.slsa_analyzer.ci_service.base_ci_service import BaseCIService, NoneCIService -from macaron.slsa_analyzer.git_url import get_repo_dir_name -from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload, v01 -from macaron.slsa_analyzer.provenance.intoto.errors import InTotoAttestationError, UnsupportedInTotoVersionError -from macaron.slsa_analyzer.provenance.loader import load_provenance_payload -from macaron.slsa_analyzer.registry import registry -from macaron.slsa_analyzer.slsa_req import ReqName - -logger: logging.Logger = logging.getLogger(__name__) - - -class ProvenanceL3VerifiedFacts(CheckFacts): - """The ORM mapping for justifications in provenance_l3 check.""" - - __tablename__ = "_provenance_l3_check" - - # The primary key. - id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003 - - __mapper_args__ = { - "polymorphic_identity": "_provenance_l3_check", - } - - -class _VerifyArtifactResultType(Enum): - """Result of attempting to verify an asset.""" - - # slsa-verifier succeeded and the artifact passed verification - PASSED = "verify passed" - # slsa-verifier succeeded and the artifact failed verification - FAILED = "verify failed" - # An error occurred running slsa-verifier or downloading the artifact - ERROR = "verify error" - # The artifact was unable to be downloaded because the url was missing or malformed - NO_DOWNLOAD = "unable to download asset" - # The artifact was unable to be downloaded because the file was too large - TOO_LARGE = "asset file too large to download" - - def is_skip(self) -> bool: - """Return whether the verification was skipped.""" - return self in (_VerifyArtifactResultType.NO_DOWNLOAD, _VerifyArtifactResultType.TOO_LARGE) - - def is_fail(self) -> bool: - """Return whether the verification failed.""" - return self in (_VerifyArtifactResultType.FAILED, _VerifyArtifactResultType.ERROR) - - -@dataclass -class _VerifyArtifactResult: - """Dataclass storing the result of verifying a single asset.""" - - result: _VerifyArtifactResultType - artifact_name: str - - def __str__(self) -> str: - return f"{str(self.result.value)} : {self.artifact_name}" - - -class ProvenanceL3Check(BaseCheck): - """This Check checks whether the target repo has SLSA provenance level 3.""" - - def __init__(self) -> None: - """Initialize instance.""" - check_id = "mcn_provenance_level_three_1" - description = "Check whether the target has SLSA provenance level 3." - depends_on: list[tuple[str, CheckResultType]] = [("mcn_provenance_available_1", CheckResultType.PASSED)] - - # SLSA 3: only identifies the top-level build config and not all the build inputs (hermetic). - # TODO: revisit if ReqName.PROV_CONT_SOURCE should be here or not. That's because the definition - # of source is not clear. See https://github.com/slsa-framework/slsa/issues/465. - eval_reqs = [ - ReqName.PROV_NON_FALSIFIABLE, - ReqName.PROV_CONT_BUILD_PARAMS, - ReqName.PROV_CONT_ENTRY, - ReqName.PROV_CONT_SOURCE, - ] - super().__init__( - check_id=check_id, - description=description, - depends_on=depends_on, - eval_reqs=eval_reqs, - result_on_skip=CheckResultType.FAILED, - ) - - def _size_large(self, asset_size: int) -> bool: - """Check the size of the asset.""" - return asset_size > defaults.getint("slsa.verifier", "max_download_size", fallback=1000000) - - def _verify_slsa( - self, macaron_path: str, temp_path: str, prov_asset: AssetLocator, asset_name: str, repository_url: str - ) -> _VerifyArtifactResult: - """Run SLSA verifier to verify the artifact.""" - source_path = get_repo_dir_name(repository_url, sanitize=False) - if not source_path: - logger.error("Invalid repository source path to verify: %s.", repository_url) - return _VerifyArtifactResult(_VerifyArtifactResultType.NO_DOWNLOAD, asset_name) - - errors: list[str] = [] - result: _VerifyArtifactResult - cmd = [ - os.path.join(macaron_path, "bin/slsa-verifier"), - "verify-artifact", - os.path.join(temp_path, asset_name), - "--provenance-path", - os.path.join(temp_path, prov_asset.name), - "--source-uri", - source_path, - ] - - try: - verifier_output = subprocess.run( # nosec B603 - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=True, - cwd=temp_path, - timeout=defaults.getint("slsa.verifier", "timeout", fallback=120), - ) - - output = verifier_output.stdout.decode("utf-8") - if "PASSED: SLSA verification passed" in output: - result = _VerifyArtifactResult(_VerifyArtifactResultType.PASSED, asset_name) - else: - result = _VerifyArtifactResult(_VerifyArtifactResultType.FAILED, asset_name) - - log_path = os.path.join(global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.log") - with open(log_path, mode="a", encoding="utf-8") as log_file: - logger.info("Storing SLSA verifier output for %s to %s", asset_name, log_path) - log_file.writelines( - [f"SLSA verifier output for cmd: {' '.join(cmd)}\n", output, "--------------------------------\n"] - ) - - except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as error: - logger.error(error) - errors.append(error.output.decode("utf-8")) - except OSError as error: - logger.error(error) - errors.append(str(error)) - - if errors: - result = _VerifyArtifactResult(result=_VerifyArtifactResultType.ERROR, artifact_name=asset_name) - try: - error_log_path = os.path.join( - global_config.build_log_path, f"{os.path.basename(source_path)}.slsa_verifier.errors" - ) - with open(error_log_path, mode="a", encoding="utf-8") as log_file: - logger.info("Storing SLSA verifier log for%s to %s", asset_name, error_log_path) - log_file.write(f"SLSA verifier output for cmd: {' '.join(cmd)}\n") - log_file.writelines(errors) - log_file.write("--------------------------------\n") - except OSError as error: - logger.error(error) - - return result - - def _extract_archive(self, file_path: str, temp_path: str) -> bool: - """Extract the archive file to the temporary path. - - Returns - ------- - bool - Returns True if successful. - """ - - def _validate_path_traversal(path: str) -> bool: - """Check for path traversal attacks.""" - if path.startswith("/") or ".." in path: - logger.debug("Found suspicious path in the archive file: %s.", path) - return False - try: - # Check if there are any symbolic links. - if os.path.realpath(path): - return True - except OSError as error: - logger.debug("Failed to extract artifact from archive file: %s", error) - return False - return False - - try: - if zipfile.is_zipfile(file_path): - with zipfile.ZipFile(file_path, "r") as zip_file: - members = (path for path in zip_file.namelist() if _validate_path_traversal(path)) - zip_file.extractall(temp_path, members=members) # nosec B202:tarfile_unsafe_members - return True - elif tarfile.is_tarfile(file_path): - with tarfile.open(file_path, mode="r:gz") as tar_file: - members_tarinfo = ( - tarinfo for tarinfo in tar_file.getmembers() if _validate_path_traversal(tarinfo.name) - ) - tar_file.extractall(temp_path, members=members_tarinfo) # nosec B202:tarfile_unsafe_members - return True - except (tarfile.TarError, zipfile.BadZipFile, zipfile.LargeZipFile, OSError, ValueError) as error: - logger.info(error) - - return False - - def _find_asset( - self, - subject: v01.InTotoV01Subject, - all_assets: list[dict[str, str]], - temp_path: str, - ci_service: BaseCIService, - ) -> dict | None: - """Find the artifacts that appear in the provenance subject. - - The artifacts can be directly found as a release asset or in an archive file. - """ - sub_asset = next( - (item for item in all_assets if item["name"] == os.path.basename(subject["name"])), - None, - ) - - if sub_asset: - return sub_asset - - extracted_artifact = glob.glob(os.path.join(temp_path, "**", os.path.basename(subject["name"])), recursive=True) - for artifact_path in extracted_artifact: - try: - with open(artifact_path, "rb") as file: - if hashlib.sha256(file.read()).hexdigest() == subject["digest"]["sha256"]: - return {"name": str(Path(artifact_path).relative_to(temp_path))} - except OSError as error: - logger.error("Error in check %s: %s", self.check_info.check_id, error) - continue - - for item in all_assets: - item_path = os.path.join(temp_path, item["name"]) - # Make sure to download an archive just once. - if not Path(item_path).is_file(): - # TODO: check that it's not too large. - if not ci_service.api_client.download_asset(item["url"], item_path): - logger.info("Could not download artifact %s. Skip verifying...", os.path.basename(item_path)) - break - - if self._extract_archive(file_path=item_path, temp_path=temp_path): - return self._find_asset(subject, all_assets, temp_path, ci_service) - - return None - - 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. - """ - # TODO: During verification, we need to fetch the workflow and verify that it's not - # using self-hosted runners, custom containers or services, etc. - - class Feedback(NamedTuple): - """Store feedback item.""" - - #: The CI service name. - ci_service_name: str - - #: The provenance asset url. - prov_asset_url: str - - #: The verification result. - verify_result: _VerifyArtifactResult - - all_feedback: list[Feedback] = [] - ci_services = ctx.dynamic_data["ci_services"] - - result_tables: list[CheckFacts] = [] - - for ci_info in ci_services: - ci_service = ci_info["service"] - - # Checking if a CI service is discovered for this repo. - if isinstance(ci_service, NoneCIService): - continue - - # Checking if we have found a release for the repo. - if not ci_info["release"] or "assets" not in ci_info["release"]: - logger.info("Could not find any release assets for the repository.") - break - - # Checking if we have found a SLSA provenance for the repo. - if not ci_info["provenance_assets"]: - logger.info("Could not find SLSA provenances.") - break - - prov_assets = ci_info["provenance_assets"] - all_assets = ci_info["release"]["assets"] - - # Download and verify the artifacts if they are not large. - # Create a temporary directory and automatically remove it when we are done. - try: - with tempfile.TemporaryDirectory() as temp_path: - downloaded_provs = [] - for prov_asset in prov_assets: - # Check the size before downloading. - if self._size_large(prov_asset.size_in_bytes): - logger.info("Skip verifying the provenance %s: asset size too large.", prov_asset.name) - continue - - if not ci_service.api_client.download_asset( - prov_asset.url, os.path.join(temp_path, prov_asset.name) - ): - logger.info("Could not download the provenance %s. Skip verifying...", prov_asset.name) - continue - - # Read the provenance. - provenance_payload = load_provenance_payload( - os.path.join(temp_path, prov_asset.name), - ) - - if not isinstance(provenance_payload, InTotoV01Payload): - raise UnsupportedInTotoVersionError( - f"The provenance asset '{prov_asset.name}' is under an unsupported in-toto version." - ) - - # Add the provenance file. - downloaded_provs.append(provenance_payload.statement) - - # Output provenance - prov = Provenance() - # TODO: fix commit reference for provenance when release/artifact as an analysis entrypoint is - # implemented ensure the provenance commit matches the actual release analyzed - prov.version = "0.2" - prov.release_commit_sha = "" - prov.provenance_json = json.dumps(provenance_payload.statement) - prov.release_tag = ci_info["release"]["tag_name"] - prov.component = ctx.component - - # Iterate through the subjects and verify. - for subject in provenance_payload.statement["subject"]: - sub_asset = self._find_asset(subject, all_assets, temp_path, ci_service) - - result: None | _VerifyArtifactResult = None - for _ in range(1): - if not sub_asset: - result = _VerifyArtifactResult( - result=_VerifyArtifactResultType.NO_DOWNLOAD, artifact_name=subject["name"] - ) - break - if not Path(temp_path, sub_asset["name"]).is_file(): - if "size" in sub_asset and self._size_large(sub_asset["size"]): - result = _VerifyArtifactResult( - result=_VerifyArtifactResultType.TOO_LARGE, - artifact_name=sub_asset["name"], - ) - break - if "url" in sub_asset and not ci_service.api_client.download_asset( - sub_asset["url"], os.path.join(temp_path, sub_asset["name"]) - ): - result = _VerifyArtifactResult( - result=_VerifyArtifactResultType.NO_DOWNLOAD, - artifact_name=sub_asset["name"], - ) - break - - result = self._verify_slsa( - ctx.macaron_path, - temp_path, - prov_asset, - sub_asset["name"], - ctx.component.repository.remote_path, - ) - - if result: - if result.result.is_skip(): - logger.info("Skipped verifying artifact: %s", result.result) - if result.result.is_fail(): - logger.info("Error verifying artifact: %s", result.result) - if result.result == _VerifyArtifactResultType.FAILED: - logger.info("Failed verifying artifact: %s", result.result) - if result.result == _VerifyArtifactResultType.PASSED: - logger.info("Successfully verified artifact: %s", result.result) - - all_feedback.append( - Feedback( - ci_service_name=ci_service.name, - prov_asset_url=prov_asset.url, - verify_result=result, - ) - ) - - # Store artifact information result to database. - artifact = ReleaseArtifact() - artifact.name = subject["name"] - artifact.slsa_verified = result.result == _VerifyArtifactResultType.PASSED - artifact.provenance = prov # pylint: disable=protected-access - - for k, val in subject["digest"].items(): - digest = HashDigest() - digest.digest_algorithm = k - digest.digest = val - # Foreign key relation. - digest.artifact = artifact - - except (OSError, InTotoAttestationError) as error: - logger.error(" %s: %s.", self.check_info.check_id, error) - return CheckResultData( - result_tables=result_tables, - result_type=CheckResultType.FAILED, - ) - - result_value = CheckResultType.FAILED - if all_feedback: - failed = [ - feedback - for feedback in all_feedback - if feedback.verify_result.result == _VerifyArtifactResultType.FAILED - ] - - skipped = [ - feedback - for feedback in all_feedback - if feedback.verify_result.result - not in [_VerifyArtifactResultType.FAILED, _VerifyArtifactResultType.PASSED] - ] - - if failed or skipped: - result_value = CheckResultType.FAILED - else: - result_tables.append(ProvenanceL3VerifiedFacts(confidence=Confidence.HIGH)) - result_value = CheckResultType.PASSED - return CheckResultData(result_tables=result_tables, result_type=result_value) - - return CheckResultData(result_tables=result_tables, result_type=result_value) - - -registry.register(ProvenanceL3Check()) diff --git a/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py b/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py index 16f621a5a..b7bc93c23 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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 checks if a SLSA provenance conforms to a given expectation.""" @@ -58,8 +58,8 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: logger.info("%s check was unable to find any expectations.", self.check_info.check_id) return CheckResultData(result_tables=[], result_type=CheckResultType.UNKNOWN) - if ctx.dynamic_data["provenance"]: - if expectation.validate(ctx.dynamic_data["provenance"]): + if ctx.dynamic_data["provenance_info"] and ctx.dynamic_data["provenance_info"].provenance_payload: + if expectation.validate(ctx.dynamic_data["provenance_info"].provenance_payload): return CheckResultData( result_tables=[expectation], result_type=CheckResultType.PASSED, diff --git a/src/macaron/slsa_analyzer/checks/provenance_repo_check.py b/src/macaron/slsa_analyzer/checks/provenance_repo_check.py index 5770aadae..063e68c2b 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_repo_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_repo_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 adds a check that determines whether the repository URL came from provenance.""" @@ -63,7 +63,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - if ctx.dynamic_data["provenance_repo_url"]: + if ctx.dynamic_data["provenance_info"] and ctx.dynamic_data["provenance_info"].repository_url: if not ctx.component.repository: return CheckResultData( result_tables=[], @@ -72,7 +72,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: current_repository = ctx.component.repository.remote_path - if current_repository == ctx.dynamic_data["provenance_repo_url"]: + if current_repository == ctx.dynamic_data["provenance_info"].repository_url: return CheckResultData( result_tables=[ ProvenanceDerivedRepoFacts( diff --git a/src/macaron/slsa_analyzer/checks/provenance_verified_check.py b/src/macaron/slsa_analyzer/checks/provenance_verified_check.py index 4bcbc3a4c..65f028ec0 100644 --- a/src/macaron/slsa_analyzer/checks/provenance_verified_check.py +++ b/src/macaron/slsa_analyzer/checks/provenance_verified_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 adds a Check that checks whether the provenance is verified.""" @@ -8,7 +8,7 @@ from sqlalchemy.orm import Mapped, mapped_column from macaron.database.table_definitions import CheckFacts -from macaron.json_tools import json_extract +from macaron.provenance.provenance_extractor import ProvenancePredicate from macaron.slsa_analyzer.analyze_context import AnalyzeContext from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType @@ -67,49 +67,25 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: CheckResultData The result of the check. """ - if ctx.dynamic_data["is_inferred_prov"] or not ctx.dynamic_data["provenance"]: - # Provenance is not available. - return CheckResultData( - result_tables=[ProvenanceVerifiedFacts(build_level=0, confidence=Confidence.HIGH)], - result_type=CheckResultType.FAILED, - ) - - predicate = ctx.dynamic_data["provenance"].statement.get("predicate") build_type = None - if predicate: - build_type = json_extract(predicate, ["buildType"], str) - - if not ctx.dynamic_data["provenance_verified"]: - # Provenance is not verified. - return CheckResultData( - result_tables=[ - ProvenanceVerifiedFacts( - build_level=1, - build_type=build_type, - confidence=Confidence.HIGH, - ) - ], - result_type=CheckResultType.FAILED, - ) - - if build_type != "https://github.com/slsa-framework/slsa-github-generator/generic@v1": - # Provenance is verified but the build service does not isolate generation in the control plane from the - # untrusted build process. - return CheckResultData( - result_tables=[ - ProvenanceVerifiedFacts( - build_level=2, - build_type=build_type, - confidence=Confidence.HIGH, - ) - ], - result_type=CheckResultType.PASSED, - ) - - # Provenance is created by the SLSA GitHub generator and verified. + provenance_info = ctx.dynamic_data["provenance_info"] + + if provenance_info and provenance_info.provenance_payload: + build_type = ProvenancePredicate.get_build_type(provenance_info.provenance_payload.statement) + + slsa_level = 0 + if provenance_info: + slsa_level = provenance_info.slsa_level + return CheckResultData( - result_tables=[ProvenanceVerifiedFacts(build_level=3, build_type=build_type, confidence=Confidence.HIGH)], - result_type=CheckResultType.PASSED, + result_tables=[ + ProvenanceVerifiedFacts( + build_level=slsa_level, + build_type=build_type, + confidence=Confidence.HIGH, + ) + ], + result_type=CheckResultType.FAILED if slsa_level < 2 else CheckResultType.PASSED, ) diff --git a/src/macaron/slsa_analyzer/ci_service/base_ci_service.py b/src/macaron/slsa_analyzer/ci_service/base_ci_service.py index 4a2b69e19..adaa3ce95 100644 --- a/src/macaron/slsa_analyzer/ci_service/base_ci_service.py +++ b/src/macaron/slsa_analyzer/ci_service/base_ci_service.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 BaseCIService class to be inherited by a CI service.""" @@ -147,7 +147,9 @@ def has_kws_in_config(self, kws: list, build_tool_name: str, repo_path: str) -> line.strip(), ) return keyword, config - logger.info("No build command found for %s in %s", build_tool_name, file_path) + logger.info( + "No build command found for %s in %s", build_tool_name, os.path.relpath(file_path, os.getcwd()) + ) return "", "" except FileNotFoundError as error: logger.debug(error) diff --git a/src/macaron/slsa_analyzer/ci_service/jenkins.py b/src/macaron/slsa_analyzer/ci_service/jenkins.py index c43354884..ebef614ca 100644 --- a/src/macaron/slsa_analyzer/ci_service/jenkins.py +++ b/src/macaron/slsa_analyzer/ci_service/jenkins.py @@ -1,12 +1,27 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 analyzes Jenkins CI.""" +import glob +import logging +import os +import re +from collections.abc import Iterable +from enum import Enum +from typing import Any + from macaron.code_analyzer.call_graph import BaseNode, CallGraph from macaron.config.defaults import defaults +from macaron.config.global_config import global_config +from macaron.errors import ParseError +from macaron.parsers import bashparser +from macaron.repo_verifier.repo_verifier import BaseBuildTool +from macaron.slsa_analyzer.build_tool.base_build_tool import BuildToolCommand from macaron.slsa_analyzer.ci_service.base_ci_service import BaseCIService +logger: logging.Logger = logging.getLogger(__name__) + class Jenkins(BaseCIService): """This class implements Jenkins CI service.""" @@ -29,7 +44,17 @@ def get_workflows(self, repo_path: str) -> list: list The list of workflow files in this repository. """ - return [] + if not self.is_detected(repo_path=repo_path): + logger.debug("There are no Jenkinsfile configurations.") + return [] + + workflow_files = [] + for conf in self.entry_conf: + workflows = glob.glob(os.path.join(repo_path, conf)) + if workflows: + logger.debug("Found Jenkinsfile configuration.") + workflow_files.extend(workflows) + return workflow_files def load_defaults(self) -> None: """Load the default values from defaults.ini.""" @@ -56,7 +81,111 @@ def build_call_graph(self, repo_path: str, macaron_path: str = "") -> CallGraph: CallGraph : CallGraph The call graph built for the CI. """ - return CallGraph(BaseNode(), "") + if not macaron_path: + macaron_path = global_config.macaron_path + + root: BaseNode = BaseNode() + call_graph = CallGraph(root, repo_path) + + # To match lines that start with sh '' or sh ''' ''' (either single or triple quotes) + # TODO: we need to support multi-line cases. + pattern = r"^\s*sh\s+'{1,3}(.*?)'{1,3}$" + workflow_files = self.get_workflows(repo_path) + + for workflow_path in workflow_files: + try: + with open(workflow_path, encoding="utf-8") as wf: + lines = wf.readlines() + except OSError as error: + logger.debug("Unable to read Jenkinsfile %s: %s", workflow_path, error) + return call_graph + + # Add internal workflow. + workflow_name = os.path.basename(workflow_path) + workflow_node = JenkinsNode( + name=workflow_name, + node_type=JenkinsNodeType.INTERNAL, + source_path=workflow_path, + caller=root, + ) + root.add_callee(workflow_node) + + # Find matching lines. + for line in lines: + match = re.match(pattern, line) + if not match: + continue + + try: + parsed_bash_script = bashparser.parse(match.group(1), macaron_path=macaron_path) + except ParseError as error: + logger.debug(error) + continue + + # TODO: Similar to GitHub Actions, we should enable support for recursive calls to bash scripts + # within Jenkinsfiles. While the implementation should be relatively straightforward, it’s + # recommended to first refactor the bashparser to make it agnostic to GitHub Actions. + bash_node = bashparser.BashNode( + "jenkins_inline_cmd", + bashparser.BashScriptType.INLINE, + workflow_path, + parsed_step_obj=None, + parsed_bash_obj=parsed_bash_script, + node_id=None, + caller=workflow_node, + ) + workflow_node.add_callee(bash_node) + + return call_graph + + def get_build_tool_commands(self, callgraph: CallGraph, build_tool: BaseBuildTool) -> Iterable[BuildToolCommand]: + """ + Traverse the callgraph and find all the reachable build tool commands. + + Parameters + ---------- + callgraph: CallGraph + The callgraph reachable from the CI workflows. + build_tool: BaseBuildTool + The corresponding build tool for which shell commands need to be detected. + + Yields + ------ + BuildToolCommand + The object that contains the build command as well useful contextual information. + + Raises + ------ + CallGraphError + Error raised when an error occurs while traversing the callgraph. + """ + yield from sorted( + self._get_build_tool_commands(callgraph=callgraph, build_tool=build_tool), + key=str, + ) + + def _get_build_tool_commands(self, callgraph: CallGraph, build_tool: BaseBuildTool) -> Iterable[BuildToolCommand]: + """Traverse the callgraph and find all the reachable build tool commands.""" + for node in callgraph.bfs(): + # We are just interested in nodes that have bash commands. + if isinstance(node, bashparser.BashNode): + # The Jenkins configuration that triggers the path in the callgraph. + workflow_node = node.caller + + # Find the bash commands that call the build tool. + for cmd in node.parsed_bash_obj.get("commands", []): + if build_tool.is_build_command(cmd): + yield BuildToolCommand( + ci_path=workflow_node.source_path if workflow_node else "", + command=cmd, + step_node=None, + language=build_tool.language, + language_versions=None, + language_distributions=None, + language_url=None, + reachable_secrets=[], + events=None, + ) def has_latest_run_passed( self, repo_full_name: str, branch_name: str | None, commit_sha: str, commit_date: str, workflow: str @@ -85,3 +214,41 @@ def has_latest_run_passed( The feed back of the check, or empty if no passing workflow is found. """ return "" + + +class JenkinsNodeType(str, Enum): + """This class represents Jenkins node type.""" + + INTERNAL = "internal" # Configurations declared in one file. + + +class JenkinsNode(BaseNode): + """This class represents a callgraph node for Jenkinsfile configuration.""" + + def __init__( + self, + name: str, + node_type: JenkinsNodeType, + source_path: str, + **kwargs: Any, + ) -> None: + """Initialize instance. + + Parameters + ---------- + name : str + Name of the workflow. + node_type : JenkinsNodeType + The type of node. + source_path : str + The path of the workflow. + caller: BaseNode | None + The caller node. + """ + super().__init__(**kwargs) + self.name = name + self.node_type: JenkinsNodeType = node_type + self.source_path = source_path + + def __str__(self) -> str: + return f"JenkinsNodeType({self.name},{self.node_type})" diff --git a/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py b/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py index 8b5575145..c457d316f 100644 --- a/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py +++ b/src/macaron/slsa_analyzer/provenance/expectations/cue/__init__.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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 provides CUE expectation implementations. @@ -10,6 +10,7 @@ import hashlib import logging +import os from typing import Self from sqlalchemy import ForeignKey @@ -52,7 +53,7 @@ def make_expectation(cls, expectation_path: str) -> Self | None: Self The instantiated expectation object. """ - logger.info("Generating an expectation from file %s", expectation_path) + logger.info("Generating an expectation from file %s", os.path.relpath(expectation_path, os.getcwd())) expectation: CUEExpectation = CUEExpectation( description="CUE expectation", path=expectation_path, diff --git a/src/macaron/slsa_analyzer/provenance/expectations/expectation_registry.py b/src/macaron/slsa_analyzer/provenance/expectations/expectation_registry.py index 63fca11c1..46b38a271 100644 --- a/src/macaron/slsa_analyzer/provenance/expectations/expectation_registry.py +++ b/src/macaron/slsa_analyzer/provenance/expectations/expectation_registry.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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/. """The provenance expectation module manages expectations that will be provided to checks.""" @@ -37,11 +37,17 @@ def __init__(self, expectation_paths: list[str]) -> None: expectation = CUEExpectation.make_expectation(expectation_path) if expectation and expectation.target: self.expectations[expectation.target] = expectation - logger.info("Found target %s for expectation %s.", expectation.target, expectation_path) + logger.info( + "Found target %s for expectation %s.", + expectation.target, + os.path.relpath(expectation_path, os.getcwd()), + ) else: - logger.error("Unable to find target for expectation %s.", expectation_path) + logger.error( + "Unable to find target for expectation %s.", os.path.relpath(expectation_path, os.getcwd()) + ) else: - logger.error("Unsupported expectation format: %s", expectation_path) + logger.error("Unsupported expectation format: %s", os.path.relpath(expectation_path, os.getcwd())) def get_expectation_for_target(self, repo_complete_name: str) -> Expectation | None: """ diff --git a/src/macaron/slsa_analyzer/specs/inferred_provenance.py b/src/macaron/slsa_analyzer/specs/inferred_provenance.py index 6d5bba573..302083768 100644 --- a/src/macaron/slsa_analyzer/specs/inferred_provenance.py +++ b/src/macaron/slsa_analyzer/specs/inferred_provenance.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 inferred SLSA provenance spec.""" @@ -7,7 +7,7 @@ from macaron.slsa_analyzer.provenance.intoto import v01 -class Provenance: +class InferredProvenance: """This class implements the inferred SLSA provenance. This inferred provenance implementation follows the SLSA v0.2 provenance schema. diff --git a/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl b/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl index 2c750872a..a51934e8d 100644 --- a/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl +++ b/tests/integration/cases/apache_maven_local_path_with_branch_name_digest_deps_cyclonedx_maven/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl index fdf03032b..578270e2b 100644 --- a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl +++ b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/guava.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/google/guava"). diff --git a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl +++ b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl index 92e0e16c8..a7b486216 100644 --- a/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl +++ b/tests/integration/cases/apache_maven_local_paths_without_dep_resolution/mockito.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/mockito/mockito"). diff --git a/tests/integration/cases/apache_maven_local_repo/policy.dl b/tests/integration/cases/apache_maven_local_repo/policy.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_local_repo/policy.dl +++ b/tests/integration/cases/apache_maven_local_repo/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_purl_repo_path/policy.dl b/tests/integration/cases/apache_maven_purl_repo_path/policy.dl index 92d3b8d7b..6a8199df0 100644 --- a/tests/integration/cases/apache_maven_purl_repo_path/policy.dl +++ b/tests/integration/cases/apache_maven_purl_repo_path/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_sbom/config.ini b/tests/integration/cases/apache_maven_sbom/config.ini new file mode 100644 index 000000000..8c9ffd63e --- /dev/null +++ b/tests/integration/cases/apache_maven_sbom/config.ini @@ -0,0 +1,5 @@ +# Copyright (c) 2025 - 2025, 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/. + +[repofinder] +try_latest_purl = False diff --git a/tests/integration/cases/apache_maven_sbom/test.yaml b/tests/integration/cases/apache_maven_sbom/test.yaml index b7f247962..2e2e47a34 100644 --- a/tests/integration/cases/apache_maven_sbom/test.yaml +++ b/tests/integration/cases/apache_maven_sbom/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -12,6 +12,7 @@ steps: - name: Run macaron analyze kind: analyze options: + ini: config.ini command_args: - -purl - pkg:maven/org.apache.maven/maven@4.0.0-alpha-1-SNAPSHOT?type=pom diff --git a/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl b/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl +++ b/tests/integration/cases/apache_maven_using_default_template_file_as_input_template/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl b/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl index fdf03032b..578270e2b 100644 --- a/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl +++ b/tests/integration/cases/apache_maven_yaml_input_skip_deps/guava.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/google/guava"). diff --git a/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl b/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl index 708676471..c7650b1a8 100644 --- a/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl +++ b/tests/integration/cases/apache_maven_yaml_input_skip_deps/maven.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/maven"). diff --git a/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl b/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl index 92e0e16c8..a7b486216 100644 --- a/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl +++ b/tests/integration/cases/apache_maven_yaml_input_skip_deps/mockito.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/mockito/mockito"). diff --git a/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl b/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl index e5dba0031..cccbb3bc9 100644 --- a/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl +++ b/tests/integration/cases/behnazh-w_example-maven-app-tutorial/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -10,8 +10,7 @@ .decl violating_dependencies(parent: number) violating_dependencies(parent) :- transitive_dependency(parent, dependency), - !check_passed(dependency, "mcn_find_artifact_pipeline_1"), - !check_passed(dependency, "mcn_provenance_level_three_1"). + !check_passed(dependency, "mcn_find_artifact_pipeline_1"). apply_policy_to("detect-malicious-upload", component_id) :- is_repo(_, "github.com/behnazh-w/example-maven-app", component_id). diff --git a/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl b/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl index 75706c7e6..1efa084e6 100644 --- a/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl +++ b/tests/integration/cases/facebook_yoga_yarn_classic/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/facebook/yoga"). diff --git a/tests/integration/cases/gitlab_tinyMediaManager/policy.dl b/tests/integration/cases/gitlab_tinyMediaManager/policy.dl index 2e6676a22..ded6d28ba 100644 --- a/tests/integration/cases/gitlab_tinyMediaManager/policy.dl +++ b/tests/integration/cases/gitlab_tinyMediaManager/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://gitlab.com/tinyMediaManager/tinyMediaManager"). diff --git a/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl b/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl index c2bb7761c..05bfe5113 100644 --- a/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl +++ b/tests/integration/cases/gitlab_tinyMediaManager_purl/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://gitlab.com/tinyMediaManager/tinyMediaManager"). diff --git a/tests/integration/cases/google_guava/policy.dl b/tests/integration/cases/google_guava/policy.dl index e872e43db..57c9abd30 100644 --- a/tests/integration/cases/google_guava/policy.dl +++ b/tests/integration/cases/google_guava/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -15,7 +15,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/google/guava"), diff --git a/tests/integration/cases/google_guava_latest/policy.dl b/tests/integration/cases/google_guava_latest/policy.dl new file mode 100644 index 000000000..76922ed27 --- /dev/null +++ b/tests/integration/cases/google_guava_latest/policy.dl @@ -0,0 +1,11 @@ +/* Copyright (c) 2024 - 2025, 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/. */ + +#include "prelude.dl" + +Policy("test_policy", component_id, "") :- + check_passed(component_id, "mcn_version_control_system_1"), + is_repo_url(component_id, "https://github.com/google/guava"). + +apply_policy_to("test_policy", component_id) :- + is_component(component_id, "pkg:maven/com.google.guava/guava@14.0.1?type=jar"). diff --git a/tests/integration/cases/google_guava_latest/test.yaml b/tests/integration/cases/google_guava_latest/test.yaml new file mode 100644 index 000000000..0f9858c82 --- /dev/null +++ b/tests/integration/cases/google_guava_latest/test.yaml @@ -0,0 +1,20 @@ +# Copyright (c) 2024 - 2025, 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/. + +description: | + Analyzing a PURL that requires fetching the latest version, and the ordering of its versions is atypical + +tags: +- macaron-python-package + +steps: +- name: Run macaron analyze + kind: analyze + options: + command_args: + - -purl + - pkg:maven/com.google.guava/guava@14.0.1?type=jar +- name: Run macaron verify-policy to verify passed/failed checks + kind: verify + options: + policy: policy.dl 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 c722e0298..c2551db50 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 @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/FasterXML/jackson-databind"). diff --git a/tests/integration/cases/jenkinsci_plotplugin/policy.dl b/tests/integration/cases/jenkinsci_plotplugin/policy.dl index 355ee5e08..766203c02 100644 --- a/tests/integration/cases/jenkinsci_plotplugin/policy.dl +++ b/tests/integration/cases/jenkinsci_plotplugin/policy.dl @@ -1,19 +1,18 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" 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_script_1"), + check_failed(component_id, "mcn_build_service_1"), check_failed(component_id, "mcn_build_as_code_1"), check_failed(component_id, "mcn_find_artifact_pipeline_1"), 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"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/jenkinsci/plot-plugin"). diff --git a/tests/integration/cases/log4j_release_pipeline/policy.dl b/tests/integration/cases/log4j_release_pipeline/policy.dl index 3044be45a..725afc643 100644 --- a/tests/integration/cases/log4j_release_pipeline/policy.dl +++ b/tests/integration/cases/log4j_release_pipeline/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -14,10 +14,9 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/logging-log4j2"). apply_policy_to("test_policy", component_id) :- - is_component(component_id, "pkg:maven/org.apache.logging.log4j/log4j-core@3.0.0-beta2"). + is_component(component_id, "pkg:maven/org.apache.logging.log4j/log4j-core@3.0.0-beta3"). diff --git a/tests/integration/cases/log4j_release_pipeline/test.yaml b/tests/integration/cases/log4j_release_pipeline/test.yaml index 54600056c..8cb0433c4 100644 --- a/tests/integration/cases/log4j_release_pipeline/test.yaml +++ b/tests/integration/cases/log4j_release_pipeline/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -14,7 +14,7 @@ steps: options: command_args: - -purl - - pkg:maven/org.apache.logging.log4j/log4j-core@3.0.0-beta2 + - pkg:maven/org.apache.logging.log4j/log4j-core@3.0.0-beta3 - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl b/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl index 2a2a68caf..a5e710912 100644 --- a/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl +++ b/tests/integration/cases/log4j_release_pipeline_deleted_run/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -12,7 +12,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/apache/logging-log4j2"), 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 048942d06..e0f43e2ce 100644 --- a/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl +++ b/tests/integration/cases/micronaut-projects_micronaut-test/micronaut-test.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,8 @@ Policy("test_policy", component_id, "") :- 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"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_provenance_derived_commit_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/integration/cases/micronaut-projects_micronaut-test/test.yaml b/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml index e0344b508..004958361 100644 --- a/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml +++ b/tests/integration/cases/micronaut-projects_micronaut-test/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -15,6 +15,7 @@ steps: command_args: - -purl - pkg:maven/io.micronaut.test/micronaut-test-junit5@4.5.0 + - --verify-provenance - name: Validate JSON report schema kind: validate_schema options: diff --git a/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl b/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl index 56b09f46a..418dee728 100644 --- a/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl +++ b/tests/integration/cases/onu-ui_onu-ui_pnpm/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/onu-ui/onu-ui"). diff --git a/tests/integration/cases/org_apache_dubbo_dubbo-rpc-memcached/policy.dl b/tests/integration/cases/org_apache_dubbo_dubbo-rpc-memcached/policy.dl new file mode 100644 index 000000000..7bc465dd3 --- /dev/null +++ b/tests/integration/cases/org_apache_dubbo_dubbo-rpc-memcached/policy.dl @@ -0,0 +1,35 @@ +/* Copyright (c) 2024 - 2025, 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/. */ + +#include "prelude.dl" + +Policy("test_policy", component_id, "") :- + check_passed(component_id, "mcn_version_control_system_1"), + check_passed(component_id, "mcn_scm_authenticity_1"), + check_passed(component_id, "mcn_build_tool_1"), + check_passed(component_id, "mcn_build_script_1"), + check_passed(component_id, "mcn_build_service_1"), + check_passed(component_id, "mcn_build_as_code_1"), + build_as_code_check( + bs_check_id, + "maven", + "jenkins", + _, + "java", + _, + _, + _, + "[\"./mvnw\", \"clean\", \"package\", \"deploy\", \"-pl\", \"dubbo-dependencies-bom\"]" + ), + check_facts(bs_check_id, _, component_id,_,_), + check_failed(component_id, "mcn_find_artifact_pipeline_1"), + 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"), + check_failed(component_id, "mcn_provenance_expectation_1"), + check_failed(component_id, "mcn_provenance_witness_level_one_1"), + check_failed(component_id, "mcn_trusted_builder_level_three_1"), + is_repo_url(component_id, "https://github.com/apache/dubbo"). + +apply_policy_to("test_policy", component_id) :- + is_component(component_id, "pkg:maven/org.apache.dubbo/dubbo-rpc-memcached@2.7.7"). diff --git a/tests/integration/cases/org_apache_dubbo_dubbo-rpc-memcached/test.yaml b/tests/integration/cases/org_apache_dubbo_dubbo-rpc-memcached/test.yaml new file mode 100644 index 000000000..9b4ff9230 --- /dev/null +++ b/tests/integration/cases/org_apache_dubbo_dubbo-rpc-memcached/test.yaml @@ -0,0 +1,21 @@ +# Copyright (c) 2024 - 2025, 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/. + +description: | + Analyzing with PURL and repository path without dependency resolution. + +tags: +- macaron-python-package +- tutorial + +steps: +- name: Run macaron analyze + kind: analyze + options: + command_args: + - -purl + - pkg:maven/org.apache.dubbo/dubbo-rpc-memcached@2.7.7 +- name: Run macaron verify-policy to verify passed/failed checks + kind: verify + options: + policy: policy.dl diff --git a/tests/integration/cases/ossf_scorecard/config.ini b/tests/integration/cases/ossf_scorecard/config.ini index f39949cc4..ec1d945d4 100644 --- a/tests/integration/cases/ossf_scorecard/config.ini +++ b/tests/integration/cases/ossf_scorecard/config.ini @@ -1,9 +1,9 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. [analysis.checks] exclude = include = mcn_provenance_expectation_1 - mcn_provenance_level_three_1 + mcn_provenance_verified_1 mcn_trusted_builder_level_three_1 diff --git a/tests/integration/cases/ossf_scorecard/policy.dl b/tests/integration/cases/ossf_scorecard/policy.dl index a69a7335b..7468219f5 100644 --- a/tests/integration/cases/ossf_scorecard/policy.dl +++ b/tests/integration/cases/ossf_scorecard/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -6,7 +6,8 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_available_1"), check_passed(component_id, "mcn_provenance_expectation_1"), - check_passed(component_id, "mcn_provenance_level_three_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_passed(component_id, "mcn_trusted_builder_level_three_1"), check_passed(component_id, "mcn_version_control_system_1"), is_repo_url(component_id, "https://github.com/ossf/scorecard"). diff --git a/tests/integration/cases/ossf_scorecard/test.yaml b/tests/integration/cases/ossf_scorecard/test.yaml index a1c778b5a..c3d64f980 100644 --- a/tests/integration/cases/ossf_scorecard/test.yaml +++ b/tests/integration/cases/ossf_scorecard/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: > @@ -17,6 +17,7 @@ steps: command_args: - --package-url - pkg:github/ossf/scorecard@v4.13.1 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/ossf_scorecard/vsa_payload.json b/tests/integration/cases/ossf_scorecard/vsa_payload.json index 2d8288940..2d9c10352 100644 --- a/tests/integration/cases/ossf_scorecard/vsa_payload.json +++ b/tests/integration/cases/ossf_scorecard/vsa_payload.json @@ -16,7 +16,7 @@ "timeVerified": "2024-02-16T06:03:16.417400+00:00", "resourceUri": "pkg:github/ossf/scorecard@v4.13.1", "policy": { - "content": "/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */\n/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */\n\n\n#include \"prelude.dl\"\n\nPolicy(\"auth-provenance\", component_id, \"\") :- check_passed(component_id, \"mcn_provenance_level_three_1\").\napply_policy_to(\"auth-provenance\", component_id) :- is_component(component_id, \"pkg:github/ossf/scorecard@v4.13.1\").\n" + "content": "/* Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. */\n/* Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. */\n\n\n#include \"prelude.dl\"\n\nPolicy(\"auth-provenance\", component_id, \"\") :-\n check_passed(component_id, \"mcn_provenance_verified_1\"),\n provenance_verified_check(_, build_level, _),\n build_level = 3.\n\napply_policy_to(\"auth-provenance\", component_id) :-\n is_component(component_id, \"pkg:github/ossf/scorecard@v4.13.1\").\n" }, "verificationResult": "PASSED", "verifiedLevels": [] diff --git a/tests/integration/cases/ossf_scorecard/vsa_policy.dl b/tests/integration/cases/ossf_scorecard/vsa_policy.dl index 0dba061e5..cefbd0abf 100644 --- a/tests/integration/cases/ossf_scorecard/vsa_policy.dl +++ b/tests/integration/cases/ossf_scorecard/vsa_policy.dl @@ -1,8 +1,13 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" -Policy("auth-provenance", component_id, "") :- check_passed(component_id, "mcn_provenance_level_three_1"). -apply_policy_to("auth-provenance", component_id) :- is_component(component_id, "pkg:github/ossf/scorecard@v4.13.1"). +Policy("auth-provenance", component_id, "") :- + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3. + +apply_policy_to("auth-provenance", component_id) :- + is_component(component_id, "pkg:github/ossf/scorecard@v4.13.1"). diff --git a/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl b/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl index e0ae8d6c7..45d180746 100644 --- a/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl +++ b/tests/integration/cases/purl_of_nonexistent_artifact/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -12,7 +12,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), check_failed(component_id, "mcn_version_control_system_1"). diff --git a/tests/integration/cases/semver/policy.dl b/tests/integration/cases/semver/policy.dl index 717062b48..bdaaed0fa 100644 --- a/tests/integration/cases/semver/policy.dl +++ b/tests/integration/cases/semver/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -19,7 +19,6 @@ Policy("test_policy", component_id, "") :- // mcn_find_artifact_pipeline_1 check fails, which is a false negative. // TODO: improve the build_as_code check analysis. check_failed(component_id, "mcn_find_artifact_pipeline_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/npm/node-semver"). diff --git a/tests/integration/cases/semver/test.yaml b/tests/integration/cases/semver/test.yaml index 5ed51623e..fa6a1b174 100644 --- a/tests/integration/cases/semver/test.yaml +++ b/tests/integration/cases/semver/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -17,6 +17,7 @@ steps: command_args: - -purl - pkg:npm/semver@7.6.2 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/sigstore_mock/policy.dl b/tests/integration/cases/sigstore_mock/policy.dl index b35d2bb4d..283d90313 100644 --- a/tests/integration/cases/sigstore_mock/policy.dl +++ b/tests/integration/cases/sigstore_mock/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_provenance_derived_repo_1"), check_passed(component_id, "mcn_provenance_verified_1"), check_failed(component_id, "mcn_find_artifact_pipeline_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/sigstore/sigstore-js"), diff --git a/tests/integration/cases/sigstore_mock/test.yaml b/tests/integration/cases/sigstore_mock/test.yaml index c748cfba3..bd635febe 100644 --- a/tests/integration/cases/sigstore_mock/test.yaml +++ b/tests/integration/cases/sigstore_mock/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -21,6 +21,7 @@ steps: - main - -d - ebdcfdfbdfeb9c9aeee6df53674ef230613629f5 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/sigstore_sget/policy.dl b/tests/integration/cases/sigstore_sget/policy.dl index df5c2a294..5babe3f8d 100644 --- a/tests/integration/cases/sigstore_sget/policy.dl +++ b/tests/integration/cases/sigstore_sget/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), check_failed(component_id, "mcn_provenance_verified_1"), diff --git a/tests/integration/cases/slsa-framework_slsa-verifier/config.ini b/tests/integration/cases/slsa-framework_slsa-verifier/config.ini deleted file mode 100644 index 884ca6874..000000000 --- a/tests/integration/cases/slsa-framework_slsa-verifier/config.ini +++ /dev/null @@ -1,6 +0,0 @@ -# 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/. - -[analysis.checks] -exclude = mcn_provenance_level_three_1 -include = * diff --git a/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml b/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml index 20c21a992..7dc7fb28d 100644 --- a/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml +++ b/tests/integration/cases/slsa-framework_slsa-verifier/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -12,7 +12,6 @@ steps: - name: Run macaron analyze kind: analyze options: - ini: config.ini expectation: expectation.cue command_args: - -rp diff --git a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/config.ini b/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/config.ini deleted file mode 100644 index 884ca6874..000000000 --- a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/config.ini +++ /dev/null @@ -1,6 +0,0 @@ -# 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/. - -[analysis.checks] -exclude = mcn_provenance_level_three_1 -include = * diff --git a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml b/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml index 73b57c077..ad62c3024 100644 --- a/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml +++ b/tests/integration/cases/slsa-framework_slsa-verifier_explicit_provenance_provided/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -11,7 +11,6 @@ steps: - name: Run macaron analyze without url link configuration. kind: analyze options: - ini: config.ini expectation: expectation.cue provenance: slsa_verifier.jsonl command_args: diff --git a/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl b/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl index dd3b5d280..715400923 100644 --- a/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl +++ b/tests/integration/cases/snakeyaml_unsupported_git_service/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://bitbucket.org/snakeyaml/snakeyaml"). diff --git a/tests/integration/cases/timyarkov_docker_test/policy.dl b/tests/integration/cases/timyarkov_docker_test/policy.dl index 599dcc138..0c5eceb2d 100644 --- a/tests/integration/cases/timyarkov_docker_test/policy.dl +++ b/tests/integration/cases/timyarkov_docker_test/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/timyarkov/docker_test"). diff --git a/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl b/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl index 90c4f2339..3b93d8ab1 100644 --- a/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl +++ b/tests/integration/cases/timyarkov_multibuild_test_maven/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -18,7 +18,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/timyarkov/multibuild_test"). diff --git a/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml b/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml index 28b6ca912..ac11642f4 100644 --- a/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml +++ b/tests/integration/cases/tutorial_npm_verify_provenance_semver/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -14,6 +14,7 @@ steps: command_args: - -purl - pkg:npm/semver@7.6.2 + - --verify-provenance - name: Verify checks for semver@7.6.2 kind: verify options: @@ -24,6 +25,7 @@ steps: command_args: - -purl - pkg:npm/semver@7.6.0 + - --verify-provenance - name: Verify checks for all 7.6.x semver runs kind: verify options: @@ -34,6 +36,7 @@ steps: command_args: - -purl - pkg:npm/semver@1.0.0 + - --verify-provenance - name: Verify checks for all semver runs kind: verify options: diff --git a/tests/integration/cases/uiv-lib_uiv/policy.dl b/tests/integration/cases/uiv-lib_uiv/policy.dl index ae20ef440..35e17f423 100644 --- a/tests/integration/cases/uiv-lib_uiv/policy.dl +++ b/tests/integration/cases/uiv-lib_uiv/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -16,7 +16,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/uiv-lib/uiv"). diff --git a/tests/integration/cases/urllib3_expectation_dir/policy.dl b/tests/integration/cases/urllib3_expectation_dir/policy.dl index 2ba5f9dbe..1ce99eca8 100644 --- a/tests/integration/cases/urllib3_expectation_dir/policy.dl +++ b/tests/integration/cases/urllib3_expectation_dir/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -9,7 +9,6 @@ Policy("test_policy", component_id, "") :- check_passed(component_id, "mcn_build_service_1"), check_passed(component_id, "mcn_version_control_system_1"), check_passed(component_id, "mcn_provenance_available_1"), - check_passed(component_id, "mcn_provenance_level_three_1"), 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"), @@ -17,6 +16,9 @@ Policy("test_policy", component_id, "") :- build_tool_check(pip_id, "pip", "python"), check_facts(pip_id, _, component_id,_,_), check_failed(component_id, "mcn_find_artifact_pipeline_1"), + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/urllib3/urllib3"), diff --git a/tests/integration/cases/urllib3_expectation_dir/test.yaml b/tests/integration/cases/urllib3_expectation_dir/test.yaml index 06ce83914..ec9e2739d 100644 --- a/tests/integration/cases/urllib3_expectation_dir/test.yaml +++ b/tests/integration/cases/urllib3_expectation_dir/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -18,6 +18,7 @@ steps: - pkg:pypi/urllib3@2.0.0a1 - --provenance-expectation - expectation + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/urllib3_expectation_file/policy.dl b/tests/integration/cases/urllib3_expectation_file/policy.dl index 3fea375f2..f230dfe07 100644 --- a/tests/integration/cases/urllib3_expectation_file/policy.dl +++ b/tests/integration/cases/urllib3_expectation_file/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -12,8 +12,10 @@ 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_provenance_level_three_1"), check_failed(component_id, "mcn_find_artifact_pipeline_1"), + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/urllib3/urllib3"). diff --git a/tests/integration/cases/urllib3_expectation_file/test.yaml b/tests/integration/cases/urllib3_expectation_file/test.yaml index c2049d9e7..21441d0a5 100644 --- a/tests/integration/cases/urllib3_expectation_file/test.yaml +++ b/tests/integration/cases/urllib3_expectation_file/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -17,6 +17,7 @@ steps: command_args: - -purl - pkg:pypi/urllib3@2.0.0a1 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/urllib3_invalid_expectation/policy.dl b/tests/integration/cases/urllib3_invalid_expectation/policy.dl index 48dd5adc2..cc71227fa 100644 --- a/tests/integration/cases/urllib3_invalid_expectation/policy.dl +++ b/tests/integration/cases/urllib3_invalid_expectation/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -11,7 +11,9 @@ Policy("test_policy", 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"), - check_passed(component_id, "mcn_provenance_level_three_1"), + check_passed(component_id, "mcn_provenance_verified_1"), + provenance_verified_check(_, build_level, _), + build_level = 3, check_failed(component_id, "mcn_find_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/integration/cases/urllib3_invalid_expectation/test.yaml b/tests/integration/cases/urllib3_invalid_expectation/test.yaml index 93a7633c8..f50aefebf 100644 --- a/tests/integration/cases/urllib3_invalid_expectation/test.yaml +++ b/tests/integration/cases/urllib3_invalid_expectation/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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/. description: | @@ -17,6 +17,7 @@ steps: command_args: - -purl - pkg:pypi/urllib3@2.0.0a1 + - --verify-provenance - name: Run macaron verify-policy to verify passed/failed checks kind: verify options: diff --git a/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl b/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl index 0ac7956cb..a1f9c2e28 100644 --- a/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl +++ b/tests/integration/cases/wojtekmaj_reactpdf_yarn_modern/policy.dl @@ -1,4 +1,4 @@ -/* Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. */ +/* Copyright (c) 2024 - 2025, 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/. */ #include "prelude.dl" @@ -13,7 +13,6 @@ Policy("test_policy", component_id, "") :- check_failed(component_id, "mcn_provenance_derived_commit_1"), check_failed(component_id, "mcn_provenance_derived_repo_1"), check_failed(component_id, "mcn_provenance_expectation_1"), - check_failed(component_id, "mcn_provenance_level_three_1"), check_failed(component_id, "mcn_provenance_witness_level_one_1"), check_failed(component_id, "mcn_trusted_builder_level_three_1"), is_repo_url(component_id, "https://github.com/wojtekmaj/react-pdf"). diff --git a/tests/provenance/__init__.py b/tests/provenance/__init__.py new file mode 100644 index 000000000..c354d6151 --- /dev/null +++ b/tests/provenance/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024 - 2025, 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/repo_finder/test_provenance_extractor.py b/tests/provenance/test_provenance_extractor.py similarity index 99% rename from tests/repo_finder/test_provenance_extractor.py rename to tests/provenance/test_provenance_extractor.py index 0fc2460ef..2f1581200 100644 --- a/tests/repo_finder/test_provenance_extractor.py +++ b/tests/provenance/test_provenance_extractor.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 provenance extractor on valid example provenances.""" @@ -9,7 +9,7 @@ from macaron.errors import ProvenanceError from macaron.json_tools import JsonType, json_extract -from macaron.repo_finder.provenance_extractor import ( +from macaron.provenance.provenance_extractor import ( check_if_repository_purl_and_url_match, extract_repo_and_commit_from_provenance, ) diff --git a/tests/repo_finder/test_provenance_finder.py b/tests/provenance/test_provenance_finder.py similarity index 88% rename from tests/repo_finder/test_provenance_finder.py rename to tests/provenance/test_provenance_finder.py index 20f2c0ad9..3cd610c0c 100644 --- a/tests/repo_finder/test_provenance_finder.py +++ b/tests/provenance/test_provenance_finder.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 provenance finder.""" @@ -14,14 +14,14 @@ from pydriller import Git from macaron.code_analyzer.call_graph import BaseNode, CallGraph -from macaron.repo_finder.provenance_finder import find_gav_provenance, find_npm_provenance, find_provenance_from_ci +from macaron.provenance.provenance_finder import find_gav_provenance, find_npm_provenance, find_provenance_from_ci from macaron.slsa_analyzer.ci_service import BaseCIService, CircleCI, GitHubActions, GitLabCI, Jenkins, Travis from macaron.slsa_analyzer.git_service.api_client import GhAPIClient from macaron.slsa_analyzer.package_registry import JFrogMavenRegistry, NPMRegistry from macaron.slsa_analyzer.package_registry.jfrog_maven_registry import JFrogMavenAsset, JFrogMavenAssetMetadata from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext @@ -161,15 +161,16 @@ def test_provenance_on_unsupported_ci(macaron_path: Path, service: BaseCIService provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) # Set up the context object with provenances. ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") ctx.dynamic_data["ci_services"] = [ci_info] - provenance = find_provenance_from_ci(ctx, None) - assert provenance is None + with tempfile.TemporaryDirectory() as temp_dir: + provenance = find_provenance_from_ci(ctx, None, temp_dir) + assert provenance is None def test_provenance_on_supported_ci(macaron_path: Path, test_dir: Path) -> None: @@ -185,7 +186,7 @@ def test_provenance_on_supported_ci(macaron_path: Path, test_dir: Path) -> None: provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) # Set up the context object with provenances. @@ -194,13 +195,15 @@ def test_provenance_on_supported_ci(macaron_path: Path, test_dir: Path) -> None: # Test with a valid setup. git_obj = MockGit() - provenance = find_provenance_from_ci(ctx, git_obj) - assert provenance + with tempfile.TemporaryDirectory() as temp_dir: + provenance = find_provenance_from_ci(ctx, git_obj, temp_dir) + assert provenance # Test with a repo that doesn't have any accepted provenance. api_client.release = {"assets": [{"name": "attestation.intoto", "url": "URL", "size": 10}]} - provenance = find_provenance_from_ci(ctx, MockGit()) - assert provenance is None + with tempfile.TemporaryDirectory() as temp_dir: + provenance = find_provenance_from_ci(ctx, MockGit(), temp_dir) + assert provenance is None def test_provenance_available_on_npm_registry( diff --git a/tests/slsa_analyzer/checks/test_build_as_code_check.py b/tests/slsa_analyzer/checks/test_build_as_code_check.py index 99aba2af1..b1bd82b12 100644 --- a/tests/slsa_analyzer/checks/test_build_as_code_check.py +++ b/tests/slsa_analyzer/checks/test_build_as_code_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 As Code Check.""" @@ -26,7 +26,7 @@ from macaron.slsa_analyzer.ci_service.jenkins import Jenkins from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext, build_github_actions_call_graph_for_commands @@ -58,7 +58,7 @@ def test_build_as_code_check_no_callgraph( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) use_build_tool = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") use_build_tool.dynamic_data["build_spec"]["tools"] = [build_tools[build_tool_name]] @@ -109,7 +109,7 @@ def test_deploy_commands( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ci_info["service"] = github_actions_service deploy_ctx.dynamic_data["ci_services"] = [ci_info] @@ -147,7 +147,7 @@ def test_gha_workflow_deployment( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) workflows_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources", "github", "workflow_files") @@ -193,7 +193,7 @@ def test_travis_ci_deploy( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) gradle_deploy = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") gradle_deploy.component.repository.fs_path = str(repo_path.absolute()) @@ -214,7 +214,7 @@ def test_multibuild_facts_saved( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) multi_build = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") diff --git a/tests/slsa_analyzer/checks/test_build_service_check.py b/tests/slsa_analyzer/checks/test_build_service_check.py index 0f5d9aeb0..4a5496c39 100644 --- a/tests/slsa_analyzer/checks/test_build_service_check.py +++ b/tests/slsa_analyzer/checks/test_build_service_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 Service Check.""" @@ -16,7 +16,7 @@ from macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci import GitHubActions from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext, build_github_actions_call_graph_for_commands @@ -48,7 +48,7 @@ def test_build_service_check_no_callgraph( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) use_build_tool = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") use_build_tool.dynamic_data["build_spec"]["tools"] = [build_tools[build_tool_name]] @@ -99,7 +99,7 @@ def test_packaging_commands( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ci_info["service"] = github_actions_service package_ctx.dynamic_data["ci_services"] = [ci_info] @@ -118,7 +118,7 @@ def test_multibuild_facts_saved( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) multi_build = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") diff --git a/tests/slsa_analyzer/checks/test_provenance_l3_check.py b/tests/slsa_analyzer/checks/test_provenance_l3_check.py deleted file mode 100644 index 0715d33c1..000000000 --- a/tests/slsa_analyzer/checks/test_provenance_l3_check.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright (c) 2022 - 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 tests for the provenance l3 check.""" - - -from macaron.code_analyzer.call_graph import BaseNode, CallGraph -from macaron.slsa_analyzer.checks.check_result import CheckResultType -from macaron.slsa_analyzer.checks.provenance_l3_check import ProvenanceL3Check -from macaron.slsa_analyzer.ci_service.circleci import CircleCI -from macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci import GitHubActions -from macaron.slsa_analyzer.ci_service.gitlab_ci import GitLabCI -from macaron.slsa_analyzer.ci_service.jenkins import Jenkins -from macaron.slsa_analyzer.ci_service.travis import Travis -from macaron.slsa_analyzer.git_service.api_client import GhAPIClient, GitHubReleaseAsset -from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload -from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance -from tests.conftest import MockAnalyzeContext - -from ...macaron_testcase import MacaronTestCase - - -class MockGitHubActions(GitHubActions): - """Mock the GitHubActions class.""" - - def has_latest_run_passed( - self, repo_full_name: str, branch_name: str | None, commit_sha: str, commit_date: str, workflow: str - ) -> str: - return "run_feedback" - - -class MockGhAPIClient(GhAPIClient): - """Mock GhAPIClient class.""" - - def __init__(self, profile: dict): - super().__init__(profile) - self.release = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": "10"}, - {"name": "artifact.txt", "url": "URL", "size": "10"}, - ] - } - - def get_latest_release(self, full_name: str) -> dict: - return self.release - - def download_asset(self, url: str, download_path: str) -> bool: - return True - - -class TestProvL3Check(MacaronTestCase): - """Test the provenance l3 check.""" - - def test_provenance_l3_check(self) -> None: - """Test the provenance l3 check.""" - check = ProvenanceL3Check() - github_actions = MockGitHubActions() - api_client = MockGhAPIClient({"headers": {}, "query": []}) - github_actions.api_client = api_client - github_actions.load_defaults() - jenkins = Jenkins() - jenkins.load_defaults() - travis = Travis() - travis.load_defaults() - circle_ci = CircleCI() - circle_ci.load_defaults() - gitlab_ci = GitLabCI() - gitlab_ci.load_defaults() - - ci_info = CIInfo( - service=github_actions, - callgraph=CallGraph(BaseNode(), ""), - provenance_assets=[], - release={}, - provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), - ) - - # Repo has provenances but no downloaded files. - ci_info["provenance_assets"] = [] - ci_info["provenance_assets"].extend( - [ - GitHubReleaseAsset( - name="attestation.intoto.jsonl", - url="URL", - size_in_bytes=10, - api_client=api_client, - ) - ] - ) - ci_info["release"] = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": 10}, - {"name": "artifact.txt", "url": "URL", "size": 10}, - ] - } - ctx = MockAnalyzeContext(macaron_path=MacaronTestCase.macaron_path, output_dir="") - ctx.dynamic_data["ci_services"] = [ci_info] - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Attestation size is too large. - ci_info["provenance_assets"] = [] - ci_info["provenance_assets"].extend( - [ - GitHubReleaseAsset( - name="attestation.intoto.jsonl", - url="URL", - size_in_bytes=100_000_000, - api_client=api_client, - ) - ] - ) - ci_info["release"] = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": 100_000_000}, - {"name": "artifact.txt", "url": "URL", "size": 10}, - ] - } - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # No provenance available. - ci_info["provenance_assets"] = [] - ci_info["release"] = { - "assets": [ - {"name": "attestation.intoto.jsonl", "url": "URL", "size": 10}, - {"name": "artifact.txt", "url": "URL", "size": 10}, - ] - } - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # No release available - ci_info["provenance_assets"] = [] - ci_info["provenance_assets"].extend( - [ - GitHubReleaseAsset( - name="attestation.intoto.jsonl", - url="URL", - size_in_bytes=10, - api_client=api_client, - ) - ] - ) - ci_info["release"] = {} - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test Jenkins. - ci_info["service"] = jenkins - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test Travis. - ci_info["service"] = travis - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test Circle CI. - ci_info["service"] = circle_ci - assert check.run_check(ctx).result_type == CheckResultType.FAILED - - # Test GitLab CI. - ci_info["service"] = gitlab_ci - assert check.run_check(ctx).result_type == CheckResultType.FAILED diff --git a/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py b/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py index 5decbd16e..8584e5f35 100644 --- a/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py +++ b/tests/slsa_analyzer/checks/test_provenance_l3_content_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 2025, 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 tests for the expectation check.""" @@ -20,7 +20,7 @@ from macaron.slsa_analyzer.provenance.loader import load_provenance_payload from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext from ...macaron_testcase import MacaronTestCase @@ -86,7 +86,7 @@ def test_expectation_check(self) -> None: provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ctx.dynamic_data["ci_services"] = [ci_info] diff --git a/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py b/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py index 57f60875e..fa65d2002 100644 --- a/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py +++ b/tests/slsa_analyzer/checks/test_provenance_repo_commit_checks.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, 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 tests for the provenance available check.""" @@ -7,7 +7,7 @@ import pytest -from macaron.database.table_definitions import CheckFacts, Repository +from macaron.database.table_definitions import CheckFacts, Provenance, Repository from macaron.slsa_analyzer.analyze_context import AnalyzeContext from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType @@ -51,8 +51,7 @@ def test_provenance_repo_commit_checks_pass( ) -> None: """Test combinations of Repository objects and provenance strings against check.""" context = _prepare_context(macaron_path, repository) - context.dynamic_data["provenance_repo_url"] = repo_url - context.dynamic_data["provenance_commit_digest"] = commit_digest + context.dynamic_data["provenance_info"] = Provenance(repository_url=repo_url, commit_sha=commit_digest) # Check Repo repo_result = _perform_check_assert_result_return_result( diff --git a/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py b/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py index 936a98957..c36eba0d5 100644 --- a/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py +++ b/tests/slsa_analyzer/checks/test_trusted_builder_l3_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 Trusted Builder Level three check.""" @@ -20,7 +20,7 @@ from macaron.slsa_analyzer.ci_service.github_actions.github_actions_ci import GitHubActions from macaron.slsa_analyzer.provenance.intoto import InTotoV01Payload from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext @@ -51,7 +51,7 @@ def test_trusted_builder_l3_check( provenance_assets=[], release={}, provenances=[], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="") diff --git a/tests/slsa_analyzer/test_analyze_context.py b/tests/slsa_analyzer/test_analyze_context.py index c9bb23c4e..40a4ad881 100644 --- a/tests/slsa_analyzer/test_analyze_context.py +++ b/tests/slsa_analyzer/test_analyze_context.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2022 - 2025, 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 tests for the AnalyzeContext module.""" @@ -14,7 +14,7 @@ from macaron.slsa_analyzer.provenance.slsa import SLSAProvenanceData from macaron.slsa_analyzer.slsa_req import ReqName, SLSAReqStatus from macaron.slsa_analyzer.specs.ci_spec import CIInfo -from macaron.slsa_analyzer.specs.inferred_provenance import Provenance +from macaron.slsa_analyzer.specs.inferred_provenance import InferredProvenance from tests.conftest import MockAnalyzeContext @@ -101,7 +101,7 @@ def test_provenances(self) -> None: payload=expected_payload, asset=VirtualReleaseAsset(name="No_ASSET", url="NO_URL", size_in_bytes=0) ), ], - build_info_results=InTotoV01Payload(statement=Provenance().payload), + build_info_results=InTotoV01Payload(statement=InferredProvenance().payload), ) self.analyze_ctx.dynamic_data["ci_services"].append(gh_actions_ci_info)