Skip to content

Commit e1de9a9

Browse files
Merge pull request #1142 from NHSDigital/NRL-1928-better-sbom
NRL-1928 better SBOM
2 parents 8f71171 + 3357d12 commit e1de9a9

File tree

6 files changed

+224
-19
lines changed

6 files changed

+224
-19
lines changed

.github/workflows/daily-build.yml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,3 @@ jobs:
6767
with:
6868
key: ${{ github.run_id }}-nrlf-permissions
6969
path: dist/nrlf_permissions.zip
70-
71-
sbom:
72-
name: Generate SBOM - ${{ github.ref }}
73-
runs-on: ubuntu-latest
74-
75-
steps:
76-
- name: Git clone - ${{ github.ref }}
77-
uses: actions/checkout@v4
78-
with:
79-
ref: ${{ github.ref }}
80-
81-
- name: Generate SBOM
82-
uses: nhs-england-tools/trivy-action/[email protected]
83-
with:
84-
repo-path: "./"

.github/workflows/release.yml

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
name: Release
1+
name: Release Published
22
run-name: Release NRL ${{ github.event.release.name }}
33
permissions:
44
id-token: write
5-
contents: read
5+
contents: write
66
actions: write
77

8+
env:
9+
SYFT_VERSION: "1.27.1"
10+
811
on:
912
release:
1013
types: [published]
@@ -15,11 +18,105 @@ on:
1518

1619
jobs:
1720
sbom:
18-
name: Generate SBOM - ${{ github.ref }}
19-
runs-on: ubuntu-latest
21+
name: Generate Software Bill of Materials - ${{ github.event.release.name }}
22+
runs-on: codebuild-nhsd-nrlf-ci-build-project-${{ github.run_id }}-${{ github.run_attempt }}
2023

2124
steps:
2225
- name: Git clone - ${{ github.ref }}
2326
uses: actions/checkout@v4
2427
with:
2528
ref: ${{ github.ref }}
29+
30+
- name: Setup environment
31+
run: |
32+
echo "${HOME}/.asdf/bin" >> $GITHUB_PATH
33+
poetry install --no-root
34+
35+
- name: Configure Management Credentials
36+
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a #v4.3.1
37+
with:
38+
aws-region: eu-west-2
39+
role-to-assume: ${{ secrets.MGMT_ROLE_ARN }}
40+
role-session-name: github-actions-ci-release-tag-${{ github.run_id }}
41+
42+
- name: Terraform Init
43+
run: |
44+
terraform -chdir=terraform/account-wide-infrastructure/mgmt init
45+
terraform -chdir=terraform/account-wide-infrastructure/dev init
46+
terraform -chdir=terraform/account-wide-infrastructure/test init
47+
terraform -chdir=terraform/account-wide-infrastructure/prod init
48+
terraform -chdir=terraform/backup-infrastructure/test init
49+
terraform -chdir=terraform/backup-infrastructure/prod init
50+
terraform -chdir=terraform/bastion init
51+
terraform -chdir=terraform/infrastructure init
52+
53+
- name: Set architecture variable
54+
id: os-arch
55+
run: |
56+
case "${{ runner.arch }}" in
57+
X64) ARCH="amd64" ;;
58+
ARM64) ARCH="arm64" ;;
59+
esac
60+
echo "arch=${ARCH}" >> $GITHUB_OUTPUT
61+
62+
- name: Download and setup Syft
63+
run: |
64+
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"
65+
echo "Downloading: ${DOWNLOAD_URL}"
66+
curl -L -o syft.tar.gz "${DOWNLOAD_URL}"
67+
tar -xzf syft.tar.gz
68+
chmod +x syft
69+
# Add to PATH for subsequent steps
70+
echo "$(pwd)" >> $GITHUB_PATH
71+
72+
- name: Create SBOM
73+
run: bash scripts/sbom-create.sh
74+
75+
- name: Upload SBOM artifact
76+
uses: actions/upload-artifact@v4
77+
with:
78+
name: sbom-${{ github.sha }}
79+
path: sbom.spdx.json
80+
81+
- name: Append SBOM inventory to summary
82+
if: always()
83+
shell: bash
84+
run: |
85+
cat > sbom_to_summary.jq <<'JQ'
86+
def clean: (.|tostring) | gsub("\\|"; "\\|") | gsub("\r?\n"; " ");
87+
def purl: ((.externalRefs[]? | select(.referenceType=="purl") | .referenceLocator) // "");
88+
def license: (.licenseConcluded // .licenseDeclared // "");
89+
def supplier: ((.supplier // "") | sub("^Person: *|^Organization: *";""));
90+
if (has("spdxVersion") | not) then
91+
"### SBOM Inventory (SPDX)\n\nSBOM is not SPDX JSON."
92+
else
93+
.packages as $pkgs
94+
| "### SBOM Inventory (SPDX)\n\n"
95+
+ "| Metric | Value |\n|---|---|\n"
96+
+ "| Packages | " + ($pkgs|length|tostring) + " |\n\n"
97+
+ "<details><summary>Full inventory</summary>\n\n"
98+
+ "| Package | Version | Supplier | License | PURL |\n|---|---|---|---|---|\n"
99+
+ (
100+
$pkgs
101+
| map("| "
102+
+ ((.name // .SPDXID) | clean)
103+
+ " | " + ((.versionInfo // "") | clean)
104+
+ " | " + (supplier | clean)
105+
+ " | " + (license | clean)
106+
+ " | " + (purl | clean)
107+
+ " |")
108+
| join("\n")
109+
)
110+
+ "\n\n</details>\n"
111+
end
112+
JQ
113+
jq -r -f sbom_to_summary.jq sbom.spdx.json >> "$GITHUB_STEP_SUMMARY"
114+
115+
- name: Upload SBOM to release
116+
if: ${{ github.event.release.tag_name }}
117+
uses: svenstaro/[email protected]
118+
with:
119+
file: sbom.spdx.json
120+
asset_name: sbom-${{ github.event.release.tag_name }}
121+
tag: ${{ github.ref }}
122+
repo_token: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ producer-internal-*.json
8484
producer-public-*.json
8585
consumer-internal-*.json
8686
consumer-public-*.json
87+
88+
# SBOM files
89+
sbom*.spdx.json

scripts/sbom-create.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
REPO_ROOT=$(git rev-parse --show-toplevel)
2+
3+
syft -o spdx-json . > sbom.spdx.json
4+
5+
ASDF_SBOM="sbom-asdf.spdx.json"
6+
7+
poetry run python "$REPO_ROOT/scripts/sbom_from_asdf.py" $ASDF_SBOM
8+
9+
poetry run python "$REPO_ROOT/scripts/sbom_update.py" $ASDF_SBOM "sbom.spdx.json"

scripts/sbom_from_asdf.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
#!/usr/bin/env python3
2+
"""Generate an SBOM-looking document for our asdf dependencies"""
3+
4+
import json
5+
from pathlib import Path
6+
7+
import fire
8+
9+
10+
def parse_tool_versions(file_path=".tool-versions"):
11+
tools = []
12+
13+
if not Path(file_path).exists():
14+
return tools
15+
16+
with open(file_path, "r") as f:
17+
for line in f:
18+
line = line.strip()
19+
if not line or line.startswith("#"):
20+
continue
21+
22+
parts = line.split()
23+
if len(parts) >= 2:
24+
tool_name = parts[0]
25+
version = parts[1]
26+
tools.append({"name": tool_name, "version": version})
27+
28+
return tools
29+
30+
31+
def generate_asdf_sbom(output_file="sbom-asdf.spdx.json"):
32+
tools = parse_tool_versions()
33+
34+
print(f"Found {len(tools)} ASDF-managed tools")
35+
36+
sbom = {
37+
"spdxVersion": "SPDX-2.3",
38+
"dataLicense": "CC0-1.0",
39+
"SPDXID": "SPDXRef-DOCUMENT",
40+
"name": "asdf-tools",
41+
"packages": [
42+
{
43+
"name": tool["name"],
44+
"SPDXID": f"SPDXRef-Package-asdf-{tool['name']}-{index}",
45+
"versionInfo": tool["version"],
46+
"supplier": "NOASSERTION",
47+
"downloadLocation": "NOASSERTION",
48+
"filesAnalyzed": False,
49+
"sourceInfo": "ASDF-managed tool: acquired package info from /.tool-versions",
50+
"licenseConcluded": "NOASSERTION",
51+
"licenseDeclared": "NOASSERTION",
52+
"copyrightText": "NOASSERTION",
53+
"externalRefs": [
54+
{
55+
"referenceCategory": "PACKAGE-MANAGER",
56+
"referenceType": "purl",
57+
"referenceLocator": f"pkg:asdf/{tool['name']}@{tool['version']}",
58+
}
59+
],
60+
}
61+
for index, tool in enumerate(tools)
62+
],
63+
"relationships": [
64+
{
65+
"spdxElementId": "SPDXRef-DOCUMENT",
66+
"relationshipType": "DESCRIBES",
67+
"relatedSpdxElement": f"SPDXRef-Package-asdf-{tool['name']}-{index}",
68+
}
69+
for index, tool in enumerate(tools)
70+
],
71+
}
72+
73+
with open(output_file, "w") as f:
74+
json.dump(sbom, f, indent=2)
75+
76+
print(f"Generated SBOM with {len(tools)} ASDF-managed tools")
77+
return output_file
78+
79+
80+
if __name__ == "__main__":
81+
fire.Fire(generate_asdf_sbom)

scripts/sbom_update.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Merge two SBOMs together
4+
5+
packages, files, and relationships from new_sbom will be merged into existing_sbom
6+
"""
7+
8+
import json
9+
from pathlib import Path
10+
11+
import fire
12+
13+
14+
def update_sbom(new_sbom, existing_sbom="sbom.spdx.json") -> None:
15+
with Path(new_sbom).open("r") as f:
16+
updates = json.load(f)
17+
18+
with Path(existing_sbom).open("r") as f:
19+
sbom = json.load(f)
20+
21+
sbom.setdefault("packages", []).extend(updates.setdefault("packages", []))
22+
sbom.setdefault("files", []).extend(updates.setdefault("files", []))
23+
sbom.setdefault("relationships", []).extend(updates.setdefault("relationships", []))
24+
25+
with Path(existing_sbom).open("w") as f:
26+
json.dump(sbom, f)
27+
28+
29+
if __name__ == "__main__":
30+
fire.Fire(update_sbom)

0 commit comments

Comments
 (0)