Skip to content

Commit 2c7af4e

Browse files
InkByteStudioclaude
andcommitted
Harden supply-chain security: SHA-pin actions, tighten verification, strengthen SBOM checks
Pin all GitHub Actions to commit SHAs, add --signer-workflow to attestation verification, replace shallow SBOM length check with name/version/license policy checks, fix Dockerfile mislabeled as lockfile, add jq and registry-auth prerequisites, and add CI self-test workflow (actionlint, shellcheck, YAML validation). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 095a9bc commit 2c7af4e

6 files changed

Lines changed: 89 additions & 9 deletions

File tree

.github/workflows/ci.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
8+
permissions:
9+
contents: read
10+
11+
jobs:
12+
lint:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- name: Check out source
17+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
18+
19+
- name: Install actionlint
20+
run: |
21+
ACTIONLINT_VERSION="1.7.7"
22+
curl -fsSL -o /tmp/actionlint.tar.gz \
23+
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
24+
tar -xzf /tmp/actionlint.tar.gz -C /tmp
25+
sudo mv /tmp/actionlint /usr/local/bin/actionlint
26+
27+
- name: Lint GitHub Actions workflows
28+
run: actionlint
29+
30+
- name: Lint shell scripts
31+
run: |
32+
sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck >/dev/null
33+
shellcheck scripts/*.sh
34+
35+
- name: Syntax-check shell scripts
36+
run: |
37+
for f in scripts/*.sh; do
38+
bash -n "$f"
39+
echo " $f: syntax OK"
40+
done
41+
42+
- name: Validate YAML templates
43+
run: |
44+
python3 -c "
45+
import yaml, glob, sys
46+
errors = 0
47+
for f in glob.glob('templates/*.yaml'):
48+
try:
49+
yaml.safe_load(open(f))
50+
print(f' {f}: valid YAML')
51+
except yaml.YAMLError as e:
52+
print(f' {f}: INVALID - {e}', file=sys.stderr)
53+
errors += 1
54+
sys.exit(errors)
55+
"

.github/workflows/release.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ jobs:
2121

2222
steps:
2323
- name: Check out source
24-
uses: actions/checkout@v4
24+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
2525

2626
- name: Log in to GHCR
2727
run: echo "${{ github.token }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
2828

2929
- name: Build and push image
3030
id: build
31-
uses: docker/build-push-action@v6
31+
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
3232
with:
3333
context: .
3434
push: true
@@ -49,7 +49,7 @@ jobs:
4949
cosign sign --yes "${IMAGE_URI}"
5050
5151
- name: Generate build provenance attestation
52-
uses: actions/attest-build-provenance@v3
52+
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
5353
with:
5454
subject-name: ${{ env.IMAGE_NAME }}
5555
subject-digest: ${{ steps.build.outputs.digest }}
@@ -71,7 +71,7 @@ jobs:
7171
syft "${IMAGE_URI}" -o spdx-json=sbom.spdx.json
7272
7373
- name: Generate signed SBOM attestation
74-
uses: actions/attest-sbom@v4
74+
uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4
7575
with:
7676
subject-name: ${{ env.IMAGE_NAME }}
7777
subject-digest: ${{ steps.build.outputs.digest }}

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Source → Build → Sign (Cosign) → Attest Provenance → Generate SBOM (Syft
2929
- [Syft](https://github.com/anchore/syft) SBOM generator
3030
- A container registry (the example uses GHCR)
3131
- GitHub CLI (`gh`) for attestation verification
32+
- [`jq`](https://jqlang.github.io/jq/) for SBOM policy evaluation in deploy-verify.sh
33+
- **Registry authentication**`gh attestation verify` for OCI images requires read access to the container registry. Run `docker login` before invoking the verification scripts (for GHCR: `echo $GITHUB_TOKEN | docker login ghcr.io -u <user> --password-stdin`)
3234

3335
## Quick Start
3436

scripts/deploy-verify.sh

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,52 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
IMAGE_URI="$1"
4+
IMAGE_URI="${1:?Usage: deploy-verify.sh IMAGE_URI}"
55
REPO="acme/payments-api"
6+
SIGNER_WORKFLOW="acme/payments-api/.github/workflows/release.yml"
67

78
echo "1) Verify Sigstore signature"
89
cosign verify \
910
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
10-
--certificate-identity-regexp "https://github.com/acme/payments-api/.github/workflows/release.yml@refs/heads/main" \
11+
--certificate-identity-regexp "https://github.com/${SIGNER_WORKFLOW}@refs/heads/main" \
1112
"${IMAGE_URI}" >/dev/null
1213

14+
# Verification uses GitHub API lookup by default (requires network access to
15+
# api.github.com). For air-gapped or OCI-native verification, add
16+
# --bundle-from-oci instead of -R. See:
17+
# https://cli.github.com/manual/gh_attestation_verify
1318
echo "2) Verify build provenance"
1419
gh attestation verify \
1520
"oci://${IMAGE_URI}" \
16-
-R "${REPO}" >/dev/null
21+
-R "${REPO}" \
22+
--signer-workflow "${SIGNER_WORKFLOW}" >/dev/null
1723

1824
echo "3) Verify SBOM attestation"
1925
gh attestation verify \
2026
"oci://${IMAGE_URI}" \
2127
-R "${REPO}" \
28+
--signer-workflow "${SIGNER_WORKFLOW}" \
2229
--predicate-type "https://spdx.dev/Document/v2.3" >/dev/null
2330

2431
echo "4) Pull SBOM payload for policy checks"
2532
gh attestation verify \
2633
"oci://${IMAGE_URI}" \
2734
-R "${REPO}" \
35+
--signer-workflow "${SIGNER_WORKFLOW}" \
2836
--predicate-type "https://spdx.dev/Document/v2.3" \
2937
--format json \
3038
--jq '.[].verificationResult.statement.predicate' > verified-sbom.json
3139

32-
# Check that the SBOM contains package data (SPDX uses .packages, CycloneDX uses .components)
33-
jq -e '.packages // .components | length > 0' verified-sbom.json >/dev/null
40+
echo "4a) Check SBOM contains package data"
41+
jq -e '(.packages // .components) | length > 0' verified-sbom.json >/dev/null
42+
43+
echo "4b) Check all packages have non-empty names"
44+
jq -e '[(.packages // .components)[] | select(.name == null or .name == "")] | length == 0' verified-sbom.json >/dev/null
45+
46+
echo "4c) Check all packages have versions"
47+
jq -e '[(.packages // .components)[] | select(.versionInfo == null and .version == null)] | length == 0' verified-sbom.json >/dev/null
48+
49+
echo "4d) Check license information is present"
50+
jq -e '[(.packages // .components)[] | select(.licenseConcluded != null or .licenseDeclared != null or .licenses != null)] | length > 0' verified-sbom.json >/dev/null
3451

3552
echo "All supply-chain checks passed"

templates/pipeline-inventory.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ build:
1010
dependencies:
1111
lockfiles:
1212
- package-lock.json
13+
build_manifests:
1314
- Dockerfile
1415
third_party_actions:
1516
- actions/checkout
1617
- docker/build-push-action
18+
- actions/attest-build-provenance
19+
- actions/attest-sbom
1720
artifact:
1821
type: container-image
1922
registry: ghcr.io/acme/payments-api

templates/supply-chain-policy.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ provenance:
1414
sbom:
1515
predicate_type: https://spdx.dev/Document/v2.3
1616
must_include_dependencies: true
17+
require_package_names: true
18+
require_package_versions: true
19+
require_license_info: true
1720

1821
exceptions:
1922
max_duration_hours: 24

0 commit comments

Comments
 (0)