⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions .github/workflows/daily-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected]
with:
repo-path: "./"
105 changes: 101 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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"
+ "<details><summary>Full inventory</summary>\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</details>\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/[email protected]
with:
file: sbom.spdx.json
asset_name: sbom-${{ github.event.release.tag_name }}
tag: ${{ github.ref }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ producer-internal-*.json
producer-public-*.json
consumer-internal-*.json
consumer-public-*.json

# SBOM files
sbom*.spdx.json
9 changes: 9 additions & 0 deletions scripts/sbom-create.sh
Original file line number Diff line number Diff line change
@@ -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"
81 changes: 81 additions & 0 deletions scripts/sbom_from_asdf.py
Original file line number Diff line number Diff line change
@@ -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)
30 changes: 30 additions & 0 deletions scripts/sbom_update.py
Original file line number Diff line number Diff line change
@@ -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)