Modernize PastureStack CLI #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security release gate | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: | |
| - 'verification/cli-*' | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: cli-security-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| build-test-scan: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 75 | |
| env: | |
| DAPPER_IMAGE: pasturestack/cli-dapper:${{ github.sha }} | |
| RUNTIME_IMAGE: pasturestack/cli-runtime:${{ github.sha }} | |
| TRIVY_IMAGE: aquasec/trivy:0.73.0@sha256:7cced7cae583819fc7806d4cbc0dbbc7cad18b99f7d3e235192e6da8c091045c | |
| VERSION_OVERRIDE: 0.6.15 | |
| CROSS: 'true' | |
| SKIP_IMAGE_BUILD: 'true' | |
| steps: | |
| - name: Check out candidate | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Record candidate identity | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test -z "$(git status --porcelain)" | |
| mkdir -p evidence | |
| git rev-parse HEAD > evidence/source-revision.txt | |
| sha256sum Dockerfile.dapper package/Dockerfile ubuntu-apt.lock vendor.conf \ | |
| security/dapper.openvex.json > evidence/source-locks.sha256 | |
| - name: Build, test, validate, and package twice | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker build \ | |
| --build-arg DAPPER_HOST_ARCH=amd64 \ | |
| --tag "$DAPPER_IMAGE" \ | |
| --file Dockerfile.dapper \ | |
| . | |
| source_path="$GITHUB_WORKSPACE" | |
| source_epoch="$(git show -s --format=%ct HEAD)" | |
| run_ci() { | |
| docker run --rm \ | |
| --volume "${source_path}:/go/src/github.com/PastureStack/cli" \ | |
| --volume /var/run/docker.sock:/var/run/docker.sock \ | |
| --env "DAPPER_UID=$(id -u)" \ | |
| --env "DAPPER_GID=$(id -g)" \ | |
| --env "GOCACHE=/tmp/go-build-cache-${GITHUB_RUN_ID}" \ | |
| --env "XDG_CONFIG_HOME=/tmp/go-config-${GITHUB_RUN_ID}" \ | |
| --env "GIT_CONFIG_GLOBAL=/tmp/gitconfig-${GITHUB_RUN_ID}" \ | |
| --env "VERSION_OVERRIDE=$VERSION_OVERRIDE" \ | |
| --env "CROSS=$CROSS" \ | |
| --env "SKIP_IMAGE_BUILD=$SKIP_IMAGE_BUILD" \ | |
| --env "SOURCE_DATE_EPOCH=${source_epoch}" \ | |
| "$DAPPER_IMAGE" ci | |
| } | |
| run_ci | |
| artifact="dist/artifacts/0.6.15/pasturestack-linux-amd64-0.6.15.tar.gz" | |
| test -s "$artifact" | |
| find dist/artifacts/0.6.15 -type f -print0 \ | |
| | LC_ALL=C sort -z \ | |
| | xargs -0 sha256sum > /tmp/first-artifacts.sha256 | |
| rm -rf bin build dist | |
| run_ci | |
| find dist/artifacts/0.6.15 -type f -print0 \ | |
| | LC_ALL=C sort -z \ | |
| | xargs -0 sha256sum > /tmp/second-artifacts.sha256 | |
| diff -u /tmp/first-artifacts.sha256 /tmp/second-artifacts.sha256 | |
| ./scripts/check-pasturestack-source | |
| mkdir -p evidence/product | |
| tar -xzf "$artifact" -C evidence/product --strip-components=1 | |
| test -x evidence/product/pasturestack | |
| evidence/product/pasturestack --version | grep -F '0.6.15' >/dev/null | |
| find dist/artifacts/0.6.15 -type f -print0 \ | |
| | LC_ALL=C sort -z \ | |
| | xargs -0 sha256sum > evidence/artifacts.sha256 | |
| docker run --rm --entrypoint go \ | |
| --volume "$PWD:/work:ro" \ | |
| "$DAPPER_IMAGE" \ | |
| version -m /work/evidence/product/pasturestack \ | |
| > evidence/product-go-version.txt | |
| - name: Build and smoke-test runtime image | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker build \ | |
| --build-arg IMAGE_VERSION="$VERSION_OVERRIDE" \ | |
| --tag "$RUNTIME_IMAGE" \ | |
| --file package/Dockerfile \ | |
| . | |
| docker run --rm "$RUNTIME_IMAGE" --version | grep -F '0.6.15' >/dev/null | |
| - name: Record resolved build inputs | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker inspect "$DAPPER_IMAGE" > evidence/dapper-image-inspect.json | |
| docker inspect "$RUNTIME_IMAGE" > evidence/runtime-image-inspect.json | |
| docker run --rm --entrypoint sh "$DAPPER_IMAGE" -lc \ | |
| "printf 'package\\tversion\\n'; dpkg-query -W -f='\${binary:Package}\\t\${Version}\\n' | LC_ALL=C sort" \ | |
| > evidence/dapper-dpkg.tsv | |
| docker run --rm --entrypoint sh "$RUNTIME_IMAGE" -lc \ | |
| "printf 'package\\tversion\\n'; dpkg-query -W -f='\${binary:Package}\\t\${Version}\\n' | LC_ALL=C sort" \ | |
| > evidence/runtime-dpkg.tsv | |
| cp vendor.conf evidence/vendor.conf | |
| cp ubuntu-apt.lock evidence/ubuntu-apt.lock | |
| - name: Scan source, product, and build image | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| docker pull "$TRIVY_IMAGE" | |
| trivy_cache="$RUNNER_TEMP/trivy-cache" | |
| source_tree="$(mktemp -d)" | |
| trap 'rm -rf "$source_tree"' EXIT | |
| mkdir -p "$trivy_cache" | |
| git archive --format=tar HEAD | tar -xf - -C "$source_tree" | |
| docker run --rm \ | |
| -v "$source_tree:/scan:ro" \ | |
| -v "$PWD/evidence:/evidence" \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" fs \ | |
| --scanners vuln,secret --format json \ | |
| --output /evidence/source-security.json /scan | |
| docker run --rm \ | |
| -v "$PWD/evidence:/evidence" \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" rootfs \ | |
| --scanners vuln,secret --format json \ | |
| --output /evidence/product-security.json /evidence/product | |
| docker run --rm \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -v "$PWD:/work" -w /work \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" image \ | |
| --scanners vuln,secret --format json \ | |
| --output /work/evidence/dapper-image-raw.json "$DAPPER_IMAGE" | |
| docker run --rm \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -v "$PWD:/work" -w /work \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" image \ | |
| --scanners vuln,secret --format json \ | |
| --output /work/evidence/runtime-image-security.json "$RUNTIME_IMAGE" | |
| jq -r ' | |
| .Results[]?.Vulnerabilities[]? | |
| | select(.Severity == "CRITICAL" or .Severity == "HIGH") | |
| | [.VulnerabilityID, .PkgIdentifier.PURL] | |
| | @tsv | |
| ' evidence/dapper-image-raw.json | LC_ALL=C sort -u \ | |
| > /tmp/dapper-critical-high.tsv | |
| jq -r ' | |
| .statements[]? | |
| | select( | |
| .status == "not_affected" | |
| and .justification == "vulnerable_code_not_present" | |
| ) | |
| | .vulnerability.name as $id | |
| | .products[]? | |
| | [$id, .["@id"]] | |
| | @tsv | |
| ' security/dapper.openvex.json | LC_ALL=C sort -u \ | |
| > /tmp/dapper-vex.tsv | |
| test -s /tmp/dapper-critical-high.tsv | |
| diff -u /tmp/dapper-critical-high.tsv /tmp/dapper-vex.tsv | |
| test "$(jq '.statements | length' security/dapper.openvex.json)" \ | |
| -eq "$(wc -l < /tmp/dapper-vex.tsv)" | |
| jq -e ' | |
| all( | |
| .statements[]; | |
| .status == "not_affected" | |
| and .justification == "vulnerable_code_not_present" | |
| and ((.impact_statement | type) == "string") | |
| and ((.impact_statement | length) > 0) | |
| ) | |
| ' security/dapper.openvex.json >/dev/null | |
| grep -F $'build\tCGO_ENABLED=0' evidence/product-go-version.txt >/dev/null | |
| if grep -Eq '^linux-(image|modules)([-:]|[[:space:]])' evidence/dapper-dpkg.tsv; then | |
| printf 'Unexpected Linux kernel runtime package in Dapper image\n' >&2 | |
| exit 1 | |
| fi | |
| docker run --rm \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -v "$PWD:/work" -w /work \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" image \ | |
| --scanners vuln,secret \ | |
| --vex /work/security/dapper.openvex.json --show-suppressed \ | |
| --format json \ | |
| --output /work/evidence/dapper-image-applicable.json "$DAPPER_IMAGE" | |
| docker run --rm \ | |
| -v "$source_tree:/scan:ro" \ | |
| -v "$PWD/evidence:/evidence" \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" fs \ | |
| --format cyclonedx --output /evidence/source.cdx.json /scan | |
| docker run --rm \ | |
| -v "$PWD/evidence:/evidence" \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" rootfs \ | |
| --format cyclonedx --output /evidence/product.cdx.json /evidence/product | |
| docker run --rm \ | |
| -v /var/run/docker.sock:/var/run/docker.sock \ | |
| -v "$PWD:/work" -w /work \ | |
| -v "$trivy_cache:/root/.cache/trivy" \ | |
| "$TRIVY_IMAGE" image \ | |
| --format cyclonedx \ | |
| --output /work/evidence/runtime.cdx.json "$RUNTIME_IMAGE" | |
| - name: Summarize and enforce candidate security | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| count() { | |
| jq "$2" "$1" | |
| } | |
| source_secrets=$(count evidence/source-security.json '[.Results[]?.Secrets[]?] | length') | |
| source_critical=$(count evidence/source-security.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length') | |
| source_high=$(count evidence/source-security.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length') | |
| product_secrets=$(count evidence/product-security.json '[.Results[]?.Secrets[]?] | length') | |
| product_critical=$(count evidence/product-security.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length') | |
| product_high=$(count evidence/product-security.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length') | |
| dapper_raw_critical=$(count evidence/dapper-image-raw.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length') | |
| dapper_raw_high=$(count evidence/dapper-image-raw.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length') | |
| dapper_applicable_critical=$(count evidence/dapper-image-applicable.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL" and .Status != "not_affected")] | length') | |
| dapper_applicable_high=$(count evidence/dapper-image-applicable.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH" and .Status != "not_affected")] | length') | |
| dapper_secrets=$(count evidence/dapper-image-applicable.json '[.Results[]?.Secrets[]?] | length') | |
| runtime_secrets=$(count evidence/runtime-image-security.json '[.Results[]?.Secrets[]?] | length') | |
| runtime_critical=$(count evidence/runtime-image-security.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "CRITICAL")] | length') | |
| runtime_high=$(count evidence/runtime-image-security.json '[.Results[]?.Vulnerabilities[]? | select(.Severity == "HIGH")] | length') | |
| { | |
| printf 'source_secrets=%s\n' "$source_secrets" | |
| printf 'source_critical=%s\n' "$source_critical" | |
| printf 'source_high=%s\n' "$source_high" | |
| printf 'product_secrets=%s\n' "$product_secrets" | |
| printf 'product_critical=%s\n' "$product_critical" | |
| printf 'product_high=%s\n' "$product_high" | |
| printf 'dapper_raw_critical=%s\n' "$dapper_raw_critical" | |
| printf 'dapper_raw_high=%s\n' "$dapper_raw_high" | |
| printf 'dapper_applicable_critical=%s\n' "$dapper_applicable_critical" | |
| printf 'dapper_applicable_high=%s\n' "$dapper_applicable_high" | |
| printf 'dapper_secrets=%s\n' "$dapper_secrets" | |
| printf 'runtime_secrets=%s\n' "$runtime_secrets" | |
| printf 'runtime_critical=%s\n' "$runtime_critical" | |
| printf 'runtime_high=%s\n' "$runtime_high" | |
| } | tee evidence/security-summary.txt | |
| test "$source_secrets" -eq 0 | |
| test "$source_critical" -eq 0 | |
| test "$source_high" -eq 0 | |
| test "$product_secrets" -eq 0 | |
| test "$product_critical" -eq 0 | |
| test "$product_high" -eq 0 | |
| test "$dapper_applicable_critical" -eq 0 | |
| test "$dapper_applicable_high" -eq 0 | |
| test "$dapper_secrets" -eq 0 | |
| test "$runtime_secrets" -eq 0 | |
| test "$runtime_critical" -eq 0 | |
| test "$runtime_high" -eq 0 | |
| - name: Upload review evidence | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: cli-security-${{ github.sha }} | |
| path: evidence/ | |
| if-no-files-found: error | |
| retention-days: 30 | |
| - 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 rm -f $(docker ps -aq --filter ancestor="$RUNTIME_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 evidence/product | |
| true |