Skip to content

Pin Ubuntu build snapshot #19

Pin Ubuntu build snapshot

Pin Ubuntu build snapshot #19

name: Security release gate
on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: host-provisioner-security-${{ github.ref }}
cancel-in-progress: false
jobs:
scan:
runs-on: ubuntu-24.04
timeout-minutes: 120
env:
DAPPER_IMAGE: pasturestack/host-provisioner-dapper:${{ github.sha }}
TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
VERSION_OVERRIDE: v0.39.5
SOURCE_DATE_EPOCH: '0'
steps:
- name: Check out candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Record immutable source identity
shell: bash
run: |
set -euo pipefail
test -z "$(git status --porcelain)"
git merge-base --is-ancestor 22d7baca609c18589f08225a97bf8c5e00109204 HEAD
mkdir -p evidence
git rev-parse HEAD > evidence/source-revision.txt
sha256sum Dockerfile.dapper Makefile .github/workflows/*.yml > evidence/source-locks.sha256
- name: Build and test immutable product
shell: bash
run: |
set -euo pipefail
make DAPPER_IMAGE="$DAPPER_IMAGE" ci
test -s dist/artifacts/host-provisioner-0.39.5-linux-amd64.tar.xz
package_dir='host-provisioner-0.39.5-linux-amd64'
tar -tJf dist/artifacts/host-provisioner-0.39.5-linux-amd64.tar.xz | grep -Fx "${package_dir}/host-provisioner"
tar -tJf dist/artifacts/host-provisioner-0.39.5-linux-amd64.tar.xz | grep -Fx "${package_dir}/LICENSE"
tar -tJf dist/artifacts/host-provisioner-0.39.5-linux-amd64.tar.xz | grep -Fx "${package_dir}/ORIGIN.md"
expected_legal=$(find vendor -type f \( \
-name 'LICENSE' -o -name 'LICENSE.*' -o \
-name 'NOTICE' -o -name 'NOTICE.*' -o \
-name 'COPYING' -o -name 'COPYING.*' -o \
-name 'AUTHORS' -o -name 'AUTHORS.*' \
\) | wc -l)
actual_legal=$(tar -tJf dist/artifacts/host-provisioner-0.39.5-linux-amd64.tar.xz | grep -Ec "^${package_dir}/licenses/.+/(LICENSE|LICENSE\..*|NOTICE|NOTICE\..*|COPYING|COPYING\..*|AUTHORS|AUTHORS\..*)$")
test "$expected_legal" -gt 0
test "$actual_legal" -eq "$expected_legal"
sha256sum dist/artifacts/host-provisioner-0.39.5-linux-amd64.tar.xz bin/host-provisioner > evidence/product-artifacts.sha256
docker run --rm --entrypoint go -v "$PWD:/work:ro" "$DAPPER_IMAGE" \
version -m /work/bin/host-provisioner > evidence/product-go-version.txt
grep -F $'build\tCGO_ENABLED=0' evidence/product-go-version.txt >/dev/null
- name: Scan source, product, and build image
shell: bash
run: |
set -euo pipefail
docker pull "$TRIVY_IMAGE"
cache="$RUNNER_TEMP/trivy-cache"
source_tree="$(mktemp -d)"
product_tree="$(mktemp -d)"
mkdir -p "$cache" "$product_tree"
trap 'rm -rf "$source_tree" "$product_tree"' EXIT
git archive --format=tar HEAD | tar -xf - -C "$source_tree"
cp bin/host-provisioner "$product_tree/host-provisioner"
docker run --rm -v "$cache:/root/.cache/trivy" "$TRIVY_IMAGE" image --download-db-only
docker run --rm -v "$source_tree:/scan:ro" -v "$PWD/evidence:/evidence" -v "$cache:/root/.cache/trivy" "$TRIVY_IMAGE" fs \
--skip-db-update --offline-scan --scanners vuln,secret --format json --output /evidence/source-security.json /scan
docker run --rm -v "$product_tree:/scan:ro" -v "$PWD/evidence:/evidence" -v "$cache:/root/.cache/trivy" "$TRIVY_IMAGE" rootfs \
--skip-db-update --offline-scan --scanners vuln,secret --format json --output /evidence/product-security.json /scan
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:/work" -w /work -v "$cache:/root/.cache/trivy" "$TRIVY_IMAGE" image \
--skip-db-update --offline-scan --scanners vuln,secret --format json --output /work/evidence/dapper-security.json "$DAPPER_IMAGE"
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:/work" -w /work -v "$cache:/root/.cache/trivy" "$TRIVY_IMAGE" image \
--skip-db-update --offline-scan --format cyclonedx --output /work/evidence/dapper.cdx.json "$DAPPER_IMAGE"
- name: Enforce product safety and review build-only headers
shell: bash
run: |
set -euo pipefail
: > evidence/security-summary.txt
for scope in source product; do
report="evidence/${scope}-security.json"
secrets=$(jq '[.Results[]?.Secrets[]?] | length' "$report")
critical=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' "$report")
high=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' "$report")
printf '%s_secrets=%s\n%s_critical=%s\n%s_high=%s\n' "$scope" "$secrets" "$scope" "$critical" "$scope" "$high" | tee -a evidence/security-summary.txt
test "$secrets" -eq 0
test "$critical" -eq 0
test "$high" -eq 0
done
dapper_secrets=$(jq '[.Results[]?.Secrets[]?] | length' evidence/dapper-security.json)
dapper_raw_critical=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length' evidence/dapper-security.json)
dapper_raw_high=$(jq '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length' evidence/dapper-security.json)
jq '[
.Results[]?.Vulnerabilities[]?
| select(.Severity == "CRITICAL" or .Severity == "HIGH")
| {
vulnerabilityId: .VulnerabilityID,
severity: .Severity,
package: .PkgName,
installedVersion: .InstalledVersion,
fixedVersion: (.FixedVersion // ""),
status: .Status,
purl: (.PkgIdentifier.PURL // "")
}
]' evidence/dapper-security.json > evidence/dapper-critical-high-review.json
dapper_unreviewed_critical_high=$(jq '[
.[]
| select(
.package != "linux-libc-dev"
or .status != "affected"
or .fixedVersion != ""
or (.purl | startswith("pkg:deb/ubuntu/linux-libc-dev@") | not)
)
] | length' evidence/dapper-critical-high-review.json)
printf 'dapper_secrets=%s\ndapper_raw_critical=%s\ndapper_raw_high=%s\ndapper_reviewed_build_only_headers=%s\ndapper_unreviewed_critical_high=%s\n' \
"$dapper_secrets" "$dapper_raw_critical" "$dapper_raw_high" "$((dapper_raw_critical + dapper_raw_high))" "$dapper_unreviewed_critical_high" | tee -a evidence/security-summary.txt
test "$dapper_secrets" -eq 0
test "$dapper_unreviewed_critical_high" -eq 0
python3 - <<'PY'
import json
with open('evidence/dapper.cdx.json', encoding='utf-8') as stream:
bom = json.load(stream)
assert bom.get('bomFormat') == 'CycloneDX'
assert str(bom.get('specVersion')) == '1.7'
assert len(bom.get('components', [])) > 0
print('dapper_components={}'.format(len(bom['components'])))
PY
- name: Upload reviewed evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: host-provisioner-security-${{ github.sha }}
path: evidence/
if-no-files-found: error
retention-days: 30
compression-level: 9
- name: Clean runner resources
if: always()
shell: bash
run: |
set +e
docker image rm -f "$DAPPER_IMAGE" "$TRIVY_IMAGE" 2>/dev/null
docker builder prune --all --force >/dev/null 2>&1
sudo rm -rf bin dist