ci: gate releases on a vulnerability scan of the shipped bundle #6
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: 'Release vulnerability scan' | |
| # Scans the binaries Kairos actually ships, for the kairos-init version this | |
| # repo pins. Refs https://github.com/kairos-io/kairos/issues/3985. | |
| # | |
| # Why this exists as a separate job rather than relying on the scanning already | |
| # in release.yaml: kairos-init UPX-compresses the bundled binaries, and a | |
| # UPX-packed Go binary no longer exposes its module metadata, so the | |
| # dependencies we ship are invisible to a scanner. This job rebuilds the bundle | |
| # with SKIP_UPX=true purely so it can be read. | |
| # | |
| # The scanned artifact is therefore NOT byte-identical to the shipped one. It is | |
| # composed from the same sources at the same pins, which is the property that | |
| # matters for dependency scanning. | |
| # | |
| # Triggers are deliberately narrow. This is a release gate: release.yaml and | |
| # release-arm.yaml call it via workflow_call, and every publishing job depends | |
| # on it, so a release with unignored advisories never publishes. It does NOT run on every pull request: the | |
| # findings belong to the pinned dependency set, not to the change under review, | |
| # so an unrelated PR -- including an external contributor's -- would get a red | |
| # check for CVEs it did not introduce and cannot fix. | |
| # | |
| # The pull_request trigger is scoped to the files that actually determine the | |
| # result: the kairos-init pin, the ignore list, and this workflow itself. | |
| on: | |
| workflow_call: | |
| workflow_dispatch: | |
| pull_request: | |
| paths: | |
| - 'images/Dockerfile' | |
| - 'osv-scanner.toml' | |
| - '.github/workflows/release-scan.yml' | |
| permissions: | |
| contents: read | |
| jobs: | |
| scan-shipped-bundle: | |
| name: scan shipped bundle | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout kairos | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Resolve the pinned kairos-init version | |
| id: pin | |
| run: | | |
| set -euo pipefail | |
| VERSION="$(grep -m1 '^ARG KAIROS_INIT=' images/Dockerfile | cut -d= -f2)" | |
| if [ -z "${VERSION}" ]; then | |
| echo "::error::could not read ARG KAIROS_INIT from images/Dockerfile" | |
| exit 1 | |
| fi | |
| echo "version=${VERSION}" >> "${GITHUB_OUTPUT}" | |
| echo "Scanning the bundle composed by kairos-init ${VERSION}" | |
| - name: Checkout kairos-init at that version | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| repository: kairos-io/kairos-init | |
| ref: ${{ steps.pin.outputs.version }} | |
| path: kairos-init | |
| # SKIP_UPX is the whole point of this job -- see the header comment. | |
| - name: Download the shipped binaries, uncompressed | |
| working-directory: kairos-init | |
| run: SKIP_UPX=true make download | |
| - name: Install osv-scanner | |
| env: | |
| OSV_SCANNER_VERSION: v2.5.0 | |
| run: | | |
| set -euo pipefail | |
| curl -sSfL --retry 5 --retry-all-errors --retry-delay 2 \ | |
| "https://github.com/google/osv-scanner/releases/download/${OSV_SCANNER_VERSION}/osv-scanner_linux_amd64" \ | |
| -o /usr/local/bin/osv-scanner | |
| chmod +x /usr/local/bin/osv-scanner | |
| osv-scanner --version | |
| # Two flags are load-bearing. Without either, the scan finds nothing and | |
| # reports success -- a false green, which is worse than no gate at all. | |
| # | |
| # --no-ignore kairos-init's .gitignore excludes | |
| # pkg/bundled/binaries/, and osv-scanner | |
| # honours .gitignore by default. | |
| # --experimental-plugins artifact the default plugin set | |
| # (lockfile, sbom, directory) cannot | |
| # read Go binaries at all. | |
| # --all-packages without it, a scan with no findings | |
| # emits an EMPTY results array, which | |
| # is indistinguishable from a scan that | |
| # read nothing -- and would trip the | |
| # false-green guard below on every | |
| # clean release. | |
| # | |
| # continue-on-error because osv-scanner exits non-zero when it finds | |
| # vulnerabilities; the reporting step below decides what that means. | |
| - name: Scan | |
| continue-on-error: true | |
| run: | | |
| set -uo pipefail | |
| osv-scanner scan source \ | |
| --experimental-plugins artifact \ | |
| --no-ignore \ | |
| --recursive \ | |
| --all-packages \ | |
| --config osv-scanner.toml \ | |
| --format json \ | |
| --output-file osv.json \ | |
| kairos-init/pkg/bundled/binaries | |
| # A clean result only means something if the scanner actually read the | |
| # binaries. Zero extracted packages is a broken scan, not a pass. | |
| - name: Guard against a false green | |
| run: | | |
| set -euo pipefail | |
| if [ ! -s osv.json ]; then | |
| echo "::error::osv-scanner produced no output file" | |
| exit 1 | |
| fi | |
| PACKAGES="$(jq '[.results[].packages[]] | length' osv.json)" | |
| echo "extracted packages: ${PACKAGES}" | |
| if [ "${PACKAGES}" -eq 0 ]; then | |
| echo "::error::scanner extracted 0 packages -- it did not read the shipped binaries. Treating as a failure, not as clean." | |
| exit 1 | |
| fi | |
| - name: Report and enforce | |
| run: | | |
| set -euo pipefail | |
| # One advisory can affect several module versions in the same bundle | |
| # (different binaries pin different versions), so a finding is an | |
| # (advisory, module@version) pair and the two counts differ. Report | |
| # both rather than calling every row an advisory. | |
| jq -r ' | |
| [ .results[].packages[] | |
| | . as $pkg | |
| | .vulnerabilities[]? | |
| | "| `\(.id)` | `\($pkg.package.name)@\($pkg.package.version)` |" | |
| ] | unique | .[]' osv.json > findings.md || true | |
| FINDINGS="$(wc -l < findings.md)" | |
| ADVISORIES="$(jq -r '[.results[].packages[].vulnerabilities[]?.id] | unique | length' osv.json)" | |
| { | |
| echo "## Vulnerability scan of the shipped bundle" | |
| echo | |
| echo "kairos-init \`${{ steps.pin.outputs.version }}\`, scanned uncompressed." | |
| echo | |
| if [ "${FINDINGS}" -eq 0 ]; then | |
| echo "No unignored advisories." | |
| else | |
| echo "**${ADVISORIES} advisories**, ${FINDINGS} findings across module versions." | |
| echo | |
| echo "| advisory | module |" | |
| echo "|---|---|" | |
| cat findings.md | |
| fi | |
| } >> "${GITHUB_STEP_SUMMARY}" | |
| if [ "${FINDINGS}" -gt 0 ]; then | |
| echo "::error::${ADVISORIES} unignored advisories (${FINDINGS} findings) in the shipped bundle. Bump the affected component, or add a dated entry to osv-scanner.toml with a reason." | |
| exit 1 | |
| fi | |
| - name: Upload scan output | |
| if: always() | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: osv-scan | |
| path: osv.json | |
| if-no-files-found: warn |