diff --git a/.github/workflows/daily-build.yml b/.github/workflows/daily-build.yml index c0b61d867..f7c84d787 100644 --- a/.github/workflows/daily-build.yml +++ b/.github/workflows/daily-build.yml @@ -67,18 +67,3 @@ jobs: with: key: ${{ github.run_id }}-nrlf-permissions path: dist/nrlf_permissions.zip - - sbom: - name: Generate SBOM - ${{ github.ref }} - runs-on: ubuntu-latest - - steps: - - name: Git clone - ${{ github.ref }} - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - - name: Generate SBOM - uses: nhs-england-tools/trivy-action/sbom-scan@v1.4.0 - with: - repo-path: "./" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05ace11ec..fa7dc4d8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,13 @@ -name: Release +name: Release Published run-name: Release NRL ${{ github.event.release.name }} permissions: id-token: write - contents: read + contents: write actions: write +env: + SYFT_VERSION: "1.27.1" + on: release: types: [published] @@ -15,11 +18,105 @@ on: jobs: sbom: - name: Generate SBOM - ${{ github.ref }} - runs-on: ubuntu-latest + name: Generate Software Bill of Materials - ${{ github.event.release.name }} + runs-on: codebuild-nhsd-nrlf-ci-build-project-${{ github.run_id }}-${{ github.run_attempt }} steps: - name: Git clone - ${{ github.ref }} uses: actions/checkout@v4 with: ref: ${{ github.ref }} + + - name: Setup environment + run: | + echo "${HOME}/.asdf/bin" >> $GITHUB_PATH + poetry install --no-root + + - name: Configure Management Credentials + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a #v4.3.1 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.MGMT_ROLE_ARN }} + role-session-name: github-actions-ci-release-tag-${{ github.run_id }} + + - name: Terraform Init + run: | + terraform -chdir=terraform/account-wide-infrastructure/mgmt init + terraform -chdir=terraform/account-wide-infrastructure/dev init + terraform -chdir=terraform/account-wide-infrastructure/test init + terraform -chdir=terraform/account-wide-infrastructure/prod init + terraform -chdir=terraform/backup-infrastructure/test init + terraform -chdir=terraform/backup-infrastructure/prod init + terraform -chdir=terraform/bastion init + terraform -chdir=terraform/infrastructure init + + - name: Set architecture variable + id: os-arch + run: | + case "${{ runner.arch }}" in + X64) ARCH="amd64" ;; + ARM64) ARCH="arm64" ;; + esac + echo "arch=${ARCH}" >> $GITHUB_OUTPUT + + - name: Download and setup Syft + run: | + DOWNLOAD_URL="https://github.com/anchore/syft/releases/download/v${{ env.SYFT_VERSION }}/syft_${{ env.SYFT_VERSION }}_linux_${{ steps.os-arch.outputs.arch }}.tar.gz" + echo "Downloading: ${DOWNLOAD_URL}" + curl -L -o syft.tar.gz "${DOWNLOAD_URL}" + tar -xzf syft.tar.gz + chmod +x syft + # Add to PATH for subsequent steps + echo "$(pwd)" >> $GITHUB_PATH + + - name: Create SBOM + run: bash scripts/sbom-create.sh + + - name: Upload SBOM artifact + uses: actions/upload-artifact@v4 + with: + name: sbom-${{ github.sha }} + path: sbom.spdx.json + + - name: Append SBOM inventory to summary + if: always() + shell: bash + run: | + cat > sbom_to_summary.jq <<'JQ' + def clean: (.|tostring) | gsub("\\|"; "\\|") | gsub("\r?\n"; " "); + def purl: ((.externalRefs[]? | select(.referenceType=="purl") | .referenceLocator) // ""); + def license: (.licenseConcluded // .licenseDeclared // ""); + def supplier: ((.supplier // "") | sub("^Person: *|^Organization: *";"")); + if (has("spdxVersion") | not) then + "### SBOM Inventory (SPDX)\n\nSBOM is not SPDX JSON." + else + .packages as $pkgs + | "### SBOM Inventory (SPDX)\n\n" + + "| Metric | Value |\n|---|---|\n" + + "| Packages | " + ($pkgs|length|tostring) + " |\n\n" + + "
Full inventory\n\n" + + "| Package | Version | Supplier | License | PURL |\n|---|---|---|---|---|\n" + + ( + $pkgs + | map("| " + + ((.name // .SPDXID) | clean) + + " | " + ((.versionInfo // "") | clean) + + " | " + (supplier | clean) + + " | " + (license | clean) + + " | " + (purl | clean) + + " |") + | join("\n") + ) + + "\n\n
\n" + end + JQ + jq -r -f sbom_to_summary.jq sbom.spdx.json >> "$GITHUB_STEP_SUMMARY" + + - name: Upload SBOM to release + if: ${{ github.event.release.tag_name }} + uses: svenstaro/upload-release-action@v2.11.3 + with: + file: sbom.spdx.json + asset_name: sbom-${{ github.event.release.tag_name }} + tag: ${{ github.ref }} + repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 7ee6f3701..0ce0f1dc2 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,6 @@ producer-internal-*.json producer-public-*.json consumer-internal-*.json consumer-public-*.json + +# SBOM files +sbom*.spdx.json diff --git a/scripts/sbom-create.sh b/scripts/sbom-create.sh new file mode 100644 index 000000000..16a547472 --- /dev/null +++ b/scripts/sbom-create.sh @@ -0,0 +1,9 @@ +REPO_ROOT=$(git rev-parse --show-toplevel) + +syft -o spdx-json . > sbom.spdx.json + +ASDF_SBOM="sbom-asdf.spdx.json" + +poetry run python "$REPO_ROOT/scripts/sbom_from_asdf.py" $ASDF_SBOM + +poetry run python "$REPO_ROOT/scripts/sbom_update.py" $ASDF_SBOM "sbom.spdx.json" diff --git a/scripts/sbom_from_asdf.py b/scripts/sbom_from_asdf.py new file mode 100644 index 000000000..d2ecafc0d --- /dev/null +++ b/scripts/sbom_from_asdf.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Generate an SBOM-looking document for our asdf dependencies""" + +import json +from pathlib import Path + +import fire + + +def parse_tool_versions(file_path=".tool-versions"): + tools = [] + + if not Path(file_path).exists(): + return tools + + with open(file_path, "r") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + + parts = line.split() + if len(parts) >= 2: + tool_name = parts[0] + version = parts[1] + tools.append({"name": tool_name, "version": version}) + + return tools + + +def generate_asdf_sbom(output_file="sbom-asdf.spdx.json"): + tools = parse_tool_versions() + + print(f"Found {len(tools)} ASDF-managed tools") + + sbom = { + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "asdf-tools", + "packages": [ + { + "name": tool["name"], + "SPDXID": f"SPDXRef-Package-asdf-{tool['name']}-{index}", + "versionInfo": tool["version"], + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "sourceInfo": "ASDF-managed tool: acquired package info from /.tool-versions", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": f"pkg:asdf/{tool['name']}@{tool['version']}", + } + ], + } + for index, tool in enumerate(tools) + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": f"SPDXRef-Package-asdf-{tool['name']}-{index}", + } + for index, tool in enumerate(tools) + ], + } + + with open(output_file, "w") as f: + json.dump(sbom, f, indent=2) + + print(f"Generated SBOM with {len(tools)} ASDF-managed tools") + return output_file + + +if __name__ == "__main__": + fire.Fire(generate_asdf_sbom) diff --git a/scripts/sbom_update.py b/scripts/sbom_update.py new file mode 100644 index 000000000..b40d0e440 --- /dev/null +++ b/scripts/sbom_update.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +""" +Merge two SBOMs together + +packages, files, and relationships from new_sbom will be merged into existing_sbom +""" + +import json +from pathlib import Path + +import fire + + +def update_sbom(new_sbom, existing_sbom="sbom.spdx.json") -> None: + with Path(new_sbom).open("r") as f: + updates = json.load(f) + + with Path(existing_sbom).open("r") as f: + sbom = json.load(f) + + sbom.setdefault("packages", []).extend(updates.setdefault("packages", [])) + sbom.setdefault("files", []).extend(updates.setdefault("files", [])) + sbom.setdefault("relationships", []).extend(updates.setdefault("relationships", [])) + + with Path(existing_sbom).open("w") as f: + json.dump(sbom, f) + + +if __name__ == "__main__": + fire.Fire(update_sbom)