Skip to content

Modernize and secure PastureStack node agent #2

Modernize and secure PastureStack node agent

Modernize and secure PastureStack node agent #2

name: Security release gate
on:
push:
branches:
- 'verification/node-agent-*'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: node-agent-security-${{ github.ref }}
cancel-in-progress: false
jobs:
build-test-scan:
runs-on: ubuntu-24.04
timeout-minutes: 120
env:
DAPPER_IMAGE: pasturestack/node-agent-dapper:${{ github.sha }}
RUNTIME_IMAGE: pasturestack/node-agent:v0.13.23
TRIVY_IMAGE: aquasec/trivy:0.73.0@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c
VERSION_OVERRIDE: v0.13.23
steps:
- name: Check out candidate
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
with:
fetch-depth: 0
persist-credentials: false
- name: Record immutable source identity
shell: bash
run: |
set -euo pipefail
test -z "$(git status --porcelain)"
test "$(git rev-list --count c8663d12dd253ef13258750dca056d4b1219fc10..HEAD)" -eq 1
mkdir -p evidence
git rev-parse HEAD > evidence/source-revision.txt
sha256sum Dockerfile.dapper package/Dockerfile tests/requirements.lock \
.github/workflows/codeql-verification.yml \
.github/workflows/security-release-gate.yml \
> evidence/source-locks.sha256
- name: Run complete CI and package runtime image
shell: bash
run: |
set -euo pipefail
CROSS=1 VERSION_OVERRIDE="$VERSION_OVERRIDE" make DAPPER_IMAGE="$DAPPER_IMAGE" ci
make DAPPER_IMAGE="$DAPPER_IMAGE" IMAGE_NAME=pasturestack/node-agent \
TAG=v0.13.23 VERSION_OVERRIDE="$VERSION_OVERRIDE" package-image
test -s dist/artifacts/node-agent-0.13.23.tar.gz
test -s dist/artifacts/node-agent-0.13.23-windows-amd64.zip
test "$(cat dist/image)" = "$RUNTIME_IMAGE"
gzip -t dist/artifacts/node-agent-0.13.23.tar.gz
unzip -t dist/artifacts/node-agent-0.13.23-windows-amd64.zip
sha256sum dist/artifacts/node-agent-0.13.23.tar.gz \
dist/artifacts/node-agent-0.13.23-windows-amd64.zip \
bin/node-agent > evidence/product-artifacts.sha256
docker image inspect "$RUNTIME_IMAGE" > evidence/runtime-image-inspect.json
docker run --rm --entrypoint /usr/bin/node-agent "$RUNTIME_IMAGE" --version \
> evidence/runtime-version.txt
grep -Fxq 'node-agent version v0.13.23' evidence/runtime-version.txt
- name: Verify reproducible Linux binary
shell: bash
run: |
set -euo pipefail
cp bin/node-agent /tmp/node-agent.first
rm -f bin/node-agent
VERSION_OVERRIDE="$VERSION_OVERRIDE" make DAPPER_IMAGE="$DAPPER_IMAGE" build
cmp /tmp/node-agent.first bin/node-agent
docker run --rm --entrypoint go -v "$PWD:/work:ro" "$DAPPER_IMAGE" \
version -m /work/bin/node-agent > evidence/product-go-version.txt
grep -F $'build\tCGO_ENABLED=0' evidence/product-go-version.txt >/dev/null
- name: Scan source, product, runtime, 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/node-agent "$product_tree/node-agent"
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/runtime-security.json "$RUNTIME_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 --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/runtime.cdx.json "$RUNTIME_IMAGE"
- name: Enforce complete zero-Critical-or-High evidence
shell: bash
run: |
set -euo pipefail
: > evidence/security-summary.txt
for scope in source product runtime dapper; 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
python3 - <<'PY'
import json
with open('evidence/runtime.cdx.json', encoding='utf-8') as stream:
bom = json.load(stream)
assert bom.get('bomFormat') == 'CycloneDX'
assert str(bom.get('specVersion')) == '1.6'
assert len(bom.get('components', [])) > 0
print('runtime_components={}'.format(len(bom['components'])))
PY
- name: Upload reviewed evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: node-agent-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 rm -f $(docker ps -aq --filter ancestor="$DAPPER_IMAGE") 2>/dev/null
docker image rm -f "$DAPPER_IMAGE" "$RUNTIME_IMAGE" "$TRIVY_IMAGE" 2>/dev/null
docker builder prune --all --force >/dev/null 2>&1
sudo rm -rf bin build dist