diff --git a/.github/workflows/slsa3.yml b/.github/workflows/slsa3.yml new file mode 100644 index 00000000..67f3462d --- /dev/null +++ b/.github/workflows/slsa3.yml @@ -0,0 +1,165 @@ +name: Anchore SLSA3 SBOM builder + +permissions: + contents: read + +defaults: + run: + shell: bash + +on: + workflow_call: + + secrets: + registry-password: + required: false + description: "The registry password" + + github-token: + description: "Authorized secret GitHub Personal Access Token. Defaults to github.token" + required: false + + inputs: + path: + required: false + description: "A path to a directory on the filesystem to scan" + default: "." + type: string + + # TODO: support for workflow_dispatch event + # by providing a `tag-name` input. + file: + required: false + description: "A file in a release asset to scan" + type: string + + image: + required: false + description: "A container image to scan" + type: string + + registry-username: + required: false + description: "The registry username" + type: string + + format: + required: false + description: "The SBOM format to export" + default: "spdx-json" + type: string + + artifact-name: + description: "The name to use for the SBOM file generated by this action" + required: false + type: string + + # output-file: + # required: false + # description: "A file location to output the SBOM" + # type: string + + syft-version: + required: false + description: "The version of Syft to use" + type: string + + dependency-snapshot: + required: false + description: "Upload to GitHub dependency snapshot API" + default: false + type: boolean + #default: "false" + #type: string + + upload-artifact: + required: false + description: "Upload artifact to workflow" + default: true + type: boolean + #default: "true" + #type: string + + upload-release-assets: + required: false + description: "Upload release assets" + default: true + type: boolean +# default: "true" +# type: string + + slsa-rekor-log-public: + description: "Allow publication of your repository name on the public Rekor log" + required: false + type: boolean + default: false + + # TODO + # provenance-overwrite: + # description: "overwrite provenance if already present" + # required: false + # type: boolean + # default: false + +jobs: + slsa-setup: + permissions: + id-token: write # For token creation. + outputs: + slsa-token: ${{ steps.generate.outputs.slsa-token }} + runs-on: ubuntu-latest + steps: + - name: Generate the token + id: generate + uses: slsa-framework/slsa-github-generator/actions/delegator/setup-token@main + with: + slsa-workflow-recipient: "delegator_generic_slsa3.yml" + slsa-rekor-log-public: ${{ inputs.slsa-rekor-log-public }} + slsa-runner-label: "ubuntu-latest" + slsa-build-action-path: "./internal/sbom-wrapper" + slsa-workflow-inputs: ${{ toJson(inputs) }} + slsa-workflow-masked-inputs: registry-username + + slsa-run: + needs: [slsa-setup] + permissions: + id-token: write # For signing. + contents: write # For asset uploads. + packages: write + actions: read + uses: slsa-framework/slsa-github-generator/.github/workflows/delegator_generic_slsa3.yml@main + with: + slsa-token: ${{ needs.slsa-setup.outputs.slsa-token }} + secrets: + secret1: ${{ secrets.registry-password }} + secret2: ${{ secrets.github-token }} + + slsa-publish: + needs: [slsa-run] + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write # For asset uploads. Optional + runs-on: ubuntu-latest + steps: + - name: Download attestations + uses: actions/download-artifact@v3 + with: + name: ${{ needs.slsa-run.outputs.attestations-download-name }} + + - name: Verify attestations + env: + SLSA_ATTESTATION_DOWNLOAD_NAME: ${{ needs.slsa-run.outputs.attestations-download-name }} + run: | + echo "download from $SLSA_ATTESTATION_DOWNLOAD_NAME" + + # TODO: Verify thru slsa-verifier + + - name: Upload SBOM provenance + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + with: + files: | + *.sigstore + + + + diff --git a/internal/sbom-wrapper/action.yml b/internal/sbom-wrapper/action.yml new file mode 100644 index 00000000..373e1d00 --- /dev/null +++ b/internal/sbom-wrapper/action.yml @@ -0,0 +1,83 @@ +name: Anchor SBOM internal Action + +description: Anchor SBOM internal Action + +inputs: + slsa-workflow-inputs: + description: 'All the inputs formatted as a map' + type: string + required: true + + slsa-layout-file: + description: 'Location to store the layout content' + type: string + required: true + + slsa-workflow-secret1: + description: 'secret1 stores the registry username' + type: string + required: false + + slsa-workflow-secret2: + description: 'secret2 stores the github-token' + type: string + required: false + + # Unused secret inputs. + slsa-workflow-secret3: {} + slsa-workflow-secret4: {} + slsa-workflow-secret5: {} + slsa-workflow-secret6: {} + slsa-workflow-secret7: {} + slsa-workflow-secret8: {} + slsa-workflow-secret9: {} + slsa-workflow-secret10: {} + slsa-workflow-secret11: {} + slsa-workflow-secret12: {} + slsa-workflow-secret13: {} + slsa-workflow-secret14: {} + slsa-workflow-secret15: {} + +runs: + using: 'composite' + steps: + # NOTE: the repository is already cloned by the caller, so there's no need to + # checkout ourselves. + + - name: Download artifact + if: ${{ startsWith(github.ref, 'refs/tags/') && fromJson(inputs.slsa-workflow-inputs).file != '' }} + env: + GH_TOKEN: ${{ github.token }} + UNTRUSTED_TAG: ${{ github.ref }} + UNTRUSTED_ASSET: ${{ fromJson(inputs.slsa-workflow-inputs).file }} + shell: bash + run: ./../__TOOL_ACTION_DIR__/download-file.sh + + + # This calls the main Action, e.g., ./../__TOOL_CHECKOUT_DIR__/ + # if path is left empty, the Action's action.yml is located at the root of the repository. + - name: Run main sbom-action Action + uses: ./../__TOOL_CHECKOUT_DIR__ + with: + path: ${{ fromJson(inputs.slsa-workflow-inputs).path }} + file: ${{ fromJson(inputs.slsa-workflow-inputs).file }} + image: ${{ fromJson(inputs.slsa-workflow-inputs).image }} + registry-username: ${{ fromJson(inputs.slsa-workflow-inputs).registry-username }} + format: ${{ fromJson(inputs.slsa-workflow-inputs).format }} + artifact-name: ${{ fromJson(inputs.slsa-workflow-inputs).artifact-name }} + #output-file: ${{ fromJson(inputs.slsa-workflow-inputs).output-file }} + syft-version: ${{ fromJson(inputs.slsa-workflow-inputs).syft-version }} + dependency-snapshot: ${{ fromJson(inputs.slsa-workflow-inputs).dependency-snapshot }} + upload-artifact: ${{ fromJson(inputs.slsa-workflow-inputs).upload-artifact }} + upload-release-assets: ${{ fromJson(inputs.slsa-workflow-inputs).upload-release-assets }} + registry-password: ${{ inputs.slsa-workflow-secret1 }} + github-token: ${{ inputs.slsa-workflow-secret2 || github.token}} + + - name: Generate layout file + id: generate-layout + env: + SLSA_OUTPUTS_ARTIFACTS_FILE: ${{ inputs.slsa-layout-file }} + #UNTRUSTED_OUTPUT_FILE: ${{ fromJson(inputs.slsa-workflow-inputs).output-file }} + shell: bash + # The `slsa-build-action-path` is available at `./../__TOOL_ACTION_DIR__`. + run: ./../__TOOL_ACTION_DIR__/generate-layout.sh \ No newline at end of file diff --git a/internal/sbom-wrapper/download-file.sh b/internal/sbom-wrapper/download-file.sh new file mode 100755 index 00000000..fbbdd142 --- /dev/null +++ b/internal/sbom-wrapper/download-file.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +# Download the file from the assets. +version=$(echo "$UNTRUSTED_TAG" | cut -f3 -d '/') +gh release download "$version" -p "$UNTRUSTED_ASSET" --clobber \ No newline at end of file diff --git a/internal/sbom-wrapper/generate-layout.sh b/internal/sbom-wrapper/generate-layout.sh new file mode 100755 index 00000000..befcd7dc --- /dev/null +++ b/internal/sbom-wrapper/generate-layout.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +# Local test. +#find sboms/ -maxdepth 2 -regex 'sboms/sbom-action-.*/*.json' > FILES +# NOTE: the name / extension varies dependecin on user input. +# Here's I'm assuming it's .sbom. +sudo find /tmp/ -maxdepth 2 -regex '/tmp/sbom-action-.*/*.sbom' | tee ./FILES + +attestations=() +n=$(wc -l <./FILES) +i=1 +while IFS= read -r line; do + file="$line" + + echo "SBOM file: $file" + hash=$(sha256sum "$file" | awk '{print $1}') + subject_name=$(basename "$(readlink -m "$file")") + template='{"name": "%s", "digest": {"sha256": "%s"}}' + printf -v entry "$template" "$subject_name" "$hash" + + if [[ $i -eq $n ]]; then + attestations+=("$entry") + else + attestations+=("$entry,") + fi + + i=$((i+1)) +done < FILES + +# NOTE: the name of the attestation should be configurable. +cat <DATA +{ + "version": 1, + "attestations": + [ + { + "name": "attestation.sbom.intoto", + "subjects": + [ + ${attestations[@]} + ] + } + ] +} +EOF + +jq "$SLSA_OUTPUTS_ARTIFACTS_FILE" diff --git a/package-lock.json b/package-lock.json index 7fc8ff6a..a3fc5ad7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4838,9 +4838,9 @@ "dev": true }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http-proxy-agent": { "version": "5.0.0", @@ -14134,9 +14134,9 @@ "dev": true }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "http-proxy-agent": { "version": "5.0.0",