From 477368282346706dd2fa42005fdb5fb7baac41e5 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:18:33 -0700 Subject: [PATCH 1/5] ci(security): CVE scan gate + nightly image scan (D95) PR gate fails on a newly-introduced FIXABLE critical CVE in a dependency tree or Dockerfile (--ignore-unfixed, so the unfixable base-OS wall doesn't block every PR). Nightly adds a full dep scan (SARIF to the Security tab) and an image scan of the three live images, reported to the run summary. Registry host is assembled from REGION + the DEV_GCP_PROJECT_ID secret so no project identifier is committed to this public repo. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VD3azQJZrdMmw8g51iscWD --- .github/workflows/cve-scan.yml | 114 +++++++++++++++++++++++++++++++++ .trivyignore | 8 +++ 2 files changed, 122 insertions(+) create mode 100644 .github/workflows/cve-scan.yml create mode 100644 .trivyignore diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml new file mode 100644 index 0000000000..ac4fd1b1e6 --- /dev/null +++ b/.github/workflows/cve-scan.yml @@ -0,0 +1,114 @@ +name: CVE Scan + +# D95 (pre-11-02 launch hardening). Two guards: +# - PR gate: fail a PR that introduces a FIXABLE critical CVE in a dependency +# tree or Dockerfile. `--ignore-unfixed` keeps the wall of unfixable base-OS +# CVEs from blocking every PR; the gate only fires on criticals we can act on. +# - Nightly: full dependency scan (SARIF to the Security tab) plus an image +# scan of the three live images, reported to the run summary. +# Registry host is assembled from REGION + the DEV_GCP_PROJECT_ID secret so no +# project identifier is committed to this public repo. + +on: + pull_request: + schedule: + - cron: '17 9 * * *' # 09:17 UTC nightly + workflow_dispatch: + inputs: + image_tag: + description: 'Image tag to scan (default: short SHA of the branch HEAD)' + required: false + +permissions: + contents: read + security-events: write # SARIF upload (nightly) + id-token: write # WIF, to pull images (nightly image job) + +concurrency: + group: cve-scan-${{ github.ref }} + cancel-in-progress: true + +env: + TRIVY_VERSION: '0.58.1' + REGION: us-central1 + +jobs: + deps: + name: Dependency + Dockerfile scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # PR: fail on a FIXABLE critical we introduced. Nightly: don't fail, upload SARIF. + - name: Trivy FS scan (gate) + if: github.event_name == 'pull_request' + uses: aquasecurity/trivy-action@0.29.0 + with: + scan-type: fs + scanners: vuln + severity: CRITICAL + ignore-unfixed: true + exit-code: '1' + format: table + trivyignores: .trivyignore + - name: Trivy FS scan (nightly, SARIF) + if: github.event_name != 'pull_request' + uses: aquasecurity/trivy-action@0.29.0 + with: + scan-type: fs + scanners: vuln + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '0' + format: sarif + output: trivy-fs.sarif + trivyignores: .trivyignore + - name: Upload SARIF + if: github.event_name != 'pull_request' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-fs.sarif + category: trivy-fs + + images: + name: Live image scan (nightly) + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Authenticate to Google Cloud (WIF) + id: auth + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.WIF_PROVIDER }} + service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} + - uses: google-github-actions/setup-gcloud@v2 + - name: Configure docker auth + run: gcloud auth configure-docker ${REGION}-docker.pkg.dev --quiet + - name: Install Trivy + run: | + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ + | sh -s -- -b /usr/local/bin "v${TRIVY_VERSION}" + - name: Scan live images + env: + PROJECT_ID: ${{ secrets.DEV_GCP_PROJECT_ID }} + TAG: ${{ github.event.inputs.image_tag }} + run: | + set -u + REG="${REGION}-docker.pkg.dev/${PROJECT_ID}/docker" + TAG="${TAG:-$(git -C . rev-parse --short HEAD 2>/dev/null || echo latest)}" + echo "## CVE image scan — tag \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY" + echo "| image | CRIT (fixable) | HIGH (fixable) |" >> "$GITHUB_STEP_SUMMARY" + echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY" + fail=0 + for n in commonly-backend commonly-bot commonly-frontend; do + img="${REG}/${n}:${TAG}" + trivy image --scanners vuln --ignore-unfixed --severity CRITICAL,HIGH \ + --quiet --format json -o "/tmp/${n}.json" "$img" || { echo "| $n | pull/scan FAILED | - |" >> "$GITHUB_STEP_SUMMARY"; continue; } + crit=$(python3 -c "import json;d=json.load(open('/tmp/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='CRITICAL'))") + high=$(python3 -c "import json;d=json.load(open('/tmp/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='HIGH'))") + echo "| $n | $crit | $high |" >> "$GITHUB_STEP_SUMMARY" + [ "$crit" -gt 0 ] && fail=1 + done + if [ "$fail" = 1 ]; then + echo "A live image has a FIXABLE critical CVE — see the table above." >&2 + exit 1 + fi diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000000..cd97993d69 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,8 @@ +# Accepted / deferred CVEs for the CVE Scan gate (D95). Review before 11-02. +# The gate runs with --ignore-unfixed, so upstream-unfixed CVEs already do not +# fire; these lines document the notable ones found in the 2026-10-26 baseline +# so a future fix (a new base image) is a conscious removal, not a surprise. +# +# backend image, no upstream fix as of 2026-10-26: +CVE-2026-58016 # libglib2.0 — no fixed version published +CVE-2023-6879 # libaom3 — no fixed version published From 909c77751bc073d370c848168dff1984c9ef61db Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:30:55 -0700 Subject: [PATCH 2/5] =?UTF-8?q?ci(security):=20address=20gate=20findings?= =?UTF-8?q?=20on=20CVE=20scan=20=E2=80=94=20base-diff=20PR=20gate,=20SHA-p?= =?UTF-8?q?inned=20actions,=20live-tag=20image=20scan,=20per-job=20perms?= =?UTF-8?q?=20(D95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework the CVE scan workflow per the four gate findings: 1. PR gate scans the head AND the base ref and fails only on a fixable CRITICAL that is new to the head (git worktree diff on VulnerabilityID+PkgName), so a pre-existing lockfile critical no longer fails unrelated PRs. 2. Nightly image job now checks out, resolves the tag the cluster is actually running (get-gke-credentials + kubectl), and fails the job on any pull/scan failure or fixable critical — no silent green. 3. Every third-party action pinned to a full commit SHA; Trivy installed by the pinned setup-trivy action instead of curl | sh. 4. Workflow-level permissions removed; PR gate, nightly dep scan, and image scan are separate jobs each carrying only the scopes it needs (PR path is contents:read only). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VD3azQJZrdMmw8g51iscWD --- .github/workflows/cve-scan.yml | 155 ++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 60 deletions(-) diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml index ac4fd1b1e6..538a31b26e 100644 --- a/.github/workflows/cve-scan.yml +++ b/.github/workflows/cve-scan.yml @@ -1,13 +1,20 @@ name: CVE Scan -# D95 (pre-11-02 launch hardening). Two guards: -# - PR gate: fail a PR that introduces a FIXABLE critical CVE in a dependency -# tree or Dockerfile. `--ignore-unfixed` keeps the wall of unfixable base-OS -# CVEs from blocking every PR; the gate only fires on criticals we can act on. -# - Nightly: full dependency scan (SARIF to the Security tab) plus an image -# scan of the three live images, reported to the run summary. -# Registry host is assembled from REGION + the DEV_GCP_PROJECT_ID secret so no -# project identifier is committed to this public repo. +# D95 (pre-11-02 launch hardening). Three guards, each a separate job so its +# permissions are exactly what that path needs: +# - pr-gate (pull_request): fail only on a NEWLY-INTRODUCED fixable critical +# CVE — the PR head is diffed against its base branch, so a pre-existing +# critical in a lockfile does not fail unrelated PRs. `--ignore-unfixed` +# keeps the unfixable base-OS wall out of the gate. contents:read only. +# - nightly-deps (schedule/dispatch): full dependency scan, SARIF to the +# Security tab. contents:read + security-events:write. +# - images (schedule/dispatch): image scan of the three live images at the +# tag the cluster is actually running. A pull or scan failure fails the job +# (no silent green). contents:read + id-token:write (WIF). +# Supply chain: every third-party action is pinned to a full commit SHA, and +# Trivy is installed by the pinned setup-trivy action (no curl | sh). Registry +# host is assembled from REGION + the DEV_GCP_PROJECT_ID secret so no project +# identifier is committed to this public repo. on: pull_request: @@ -16,55 +23,71 @@ on: workflow_dispatch: inputs: image_tag: - description: 'Image tag to scan (default: short SHA of the branch HEAD)' + description: 'Image tag to scan (default: the tag the cluster is running)' required: false -permissions: - contents: read - security-events: write # SARIF upload (nightly) - id-token: write # WIF, to pull images (nightly image job) +# No workflow-level permissions — each job declares only what it needs. +permissions: {} concurrency: group: cve-scan-${{ github.ref }} cancel-in-progress: true env: - TRIVY_VERSION: '0.58.1' + TRIVY_VERSION: 'v0.58.1' REGION: us-central1 + CLUSTER: commonly-dev jobs: - deps: - name: Dependency + Dockerfile scan + pr-gate: + name: PR gate — newly-introduced fixable criticals + if: github.event_name == 'pull_request' runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v4 - # PR: fail on a FIXABLE critical we introduced. Nightly: don't fail, upload SARIF. - - name: Trivy FS scan (gate) - if: github.event_name == 'pull_request' - uses: aquasecurity/trivy-action@0.29.0 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: - scan-type: fs - scanners: vuln - severity: CRITICAL - ignore-unfixed: true - exit-code: '1' - format: table - trivyignores: .trivyignore - - name: Trivy FS scan (nightly, SARIF) - if: github.event_name != 'pull_request' - uses: aquasecurity/trivy-action@0.29.0 + fetch-depth: 0 # need base history for the diff + - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 with: - scan-type: fs - scanners: vuln - severity: CRITICAL,HIGH - ignore-unfixed: true - exit-code: '0' - format: sarif - output: trivy-fs.sarif - trivyignores: .trivyignore + version: ${{ env.TRIVY_VERSION }} + - name: Diff fixable criticals against base + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + set -euo pipefail + scan() { trivy fs --scanners vuln --severity CRITICAL --ignore-unfixed \ + --quiet --format json -o "$2" "$1"; } + ids() { python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('\n'.join(sorted({v['VulnerabilityID']+'|'+v.get('PkgName','') for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or []})))" "$1"; } + echo "scanning PR head"; scan . head.json + git worktree add -q /tmp/base "$BASE_SHA" + echo "scanning base $BASE_SHA"; scan /tmp/base base.json + ids head.json > head.ids; ids base.json > base.ids + new=$(comm -23 head.ids base.ids || true) + if [ -n "$new" ]; then + echo "::error::PR introduces fixable CRITICAL CVE(s) not on base:"; echo "$new" + exit 1 + fi + echo "No newly-introduced fixable critical CVEs." + + nightly-deps: + name: Nightly dependency scan (SARIF) + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 + with: + version: ${{ env.TRIVY_VERSION }} + - name: Dep scan + run: trivy fs --scanners vuln --severity CRITICAL,HIGH --ignore-unfixed + --quiet --format sarif -o trivy-fs.sarif --exit-code 0 . - name: Upload SARIF - if: github.event_name != 'pull_request' - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@4bfa242aa49c5f7c6770b98670d354a7dfac05f8 # v3 with: sarif_file: trivy-fs.sarif category: trivy-fs @@ -73,42 +96,54 @@ jobs: name: Live image scan (nightly) if: github.event_name != 'pull_request' runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # WIF only steps: - name: Authenticate to Google Cloud (WIF) - id: auth - uses: google-github-actions/auth@v2 + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} - - uses: google-github-actions/setup-gcloud@v2 + - uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2 + - name: Get GKE credentials + uses: google-github-actions/get-gke-credentials@64bc7249bbcf78056bb92f14d3cedc2da193946c # v2 + with: + cluster_name: ${{ env.CLUSTER }} + location: ${{ env.REGION }} + - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 + with: + version: ${{ env.TRIVY_VERSION }} - name: Configure docker auth - run: gcloud auth configure-docker ${REGION}-docker.pkg.dev --quiet - - name: Install Trivy - run: | - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ - | sh -s -- -b /usr/local/bin "v${TRIVY_VERSION}" + run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet - name: Scan live images env: PROJECT_ID: ${{ secrets.DEV_GCP_PROJECT_ID }} - TAG: ${{ github.event.inputs.image_tag }} + TAG_INPUT: ${{ github.event.inputs.image_tag }} run: | - set -u + set -euo pipefail REG="${REGION}-docker.pkg.dev/${PROJECT_ID}/docker" - TAG="${TAG:-$(git -C . rev-parse --short HEAD 2>/dev/null || echo latest)}" + if [ -n "${TAG_INPUT}" ]; then + TAG="${TAG_INPUT}" + else + img=$(kubectl get deploy backend -n "${CLUSTER}" -o jsonpath='{.spec.template.spec.containers[0].image}') + TAG="${img##*:}" + fi + [ -n "${TAG}" ] || { echo "::error::could not resolve an image tag"; exit 1; } echo "## CVE image scan — tag \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY" echo "| image | CRIT (fixable) | HIGH (fixable) |" >> "$GITHUB_STEP_SUMMARY" echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY" fail=0 for n in commonly-backend commonly-bot commonly-frontend; do img="${REG}/${n}:${TAG}" - trivy image --scanners vuln --ignore-unfixed --severity CRITICAL,HIGH \ - --quiet --format json -o "/tmp/${n}.json" "$img" || { echo "| $n | pull/scan FAILED | - |" >> "$GITHUB_STEP_SUMMARY"; continue; } + if ! trivy image --scanners vuln --ignore-unfixed --severity CRITICAL,HIGH \ + --quiet --format json -o "/tmp/${n}.json" "$img"; then + echo "| ${n} | pull/scan FAILED | - |" >> "$GITHUB_STEP_SUMMARY" + fail=1; continue + fi crit=$(python3 -c "import json;d=json.load(open('/tmp/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='CRITICAL'))") high=$(python3 -c "import json;d=json.load(open('/tmp/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='HIGH'))") - echo "| $n | $crit | $high |" >> "$GITHUB_STEP_SUMMARY" - [ "$crit" -gt 0 ] && fail=1 + echo "| ${n} | ${crit} | ${high} |" >> "$GITHUB_STEP_SUMMARY" + [ "${crit}" -gt 0 ] && fail=1 done - if [ "$fail" = 1 ]; then - echo "A live image has a FIXABLE critical CVE — see the table above." >&2 - exit 1 - fi + [ "${fail}" = 0 ] || { echo "::error::a live image failed to scan or has a fixable critical CVE"; exit 1; } From c08e36deb6a00a1fb3544890d7bf38cf8d13dbff Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Mon, 26 Oct 2026 03:40:49 -0700 Subject: [PATCH 3/5] ci(security): run Trivy as a digest-pinned container, not the setup-trivy installer (D95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gate was red at setup-trivy: it clones Trivy's install.sh at HEAD and the new script can no longer fetch the old v0.58.1 release asset (naming drift), exiting 1 before any scan. Replace the binary installer with the official Trivy image pinned by DIGEST (ghcr.io/aquasecurity/trivy@sha256:790fd66d…c06c, v0.76.1). This is the direct mitigation for the March 2026 trivy-action/setup-trivy tag-poisoning compromise (GHSA-69fq-xp46-6x23): version TAGS were force-pushed to malicious commits and Docker Hub tags 0.69.5/0.69.6 were attacker-pushed. A sha256 digest cannot be re-pointed, so it is immune to that class of attack. 0.76.1 is a current post-remediation release from GHCR (GitHub immutable releases). All three jobs now run Trivy via `docker run `: - pr-gate: scans head and the base worktree in the container, diffs new fixable criticals (unchanged logic, containerised). - nightly-deps: SARIF dep scan, copied out for upload-sarif. - images: AR pull creds passed to the container via TRIVY_USERNAME/PASSWORD (oauth2accesstoken + access token), password kept off argv. GITHUB_TOKEN is forwarded to raise the Trivy DB pull rate limit. Every other action stays pinned to a full commit SHA. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VD3azQJZrdMmw8g51iscWD --- .github/workflows/cve-scan.yml | 74 +++++++++++++++++++++------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml index 538a31b26e..d1481b84b1 100644 --- a/.github/workflows/cve-scan.yml +++ b/.github/workflows/cve-scan.yml @@ -11,10 +11,18 @@ name: CVE Scan # - images (schedule/dispatch): image scan of the three live images at the # tag the cluster is actually running. A pull or scan failure fails the job # (no silent green). contents:read + id-token:write (WIF). -# Supply chain: every third-party action is pinned to a full commit SHA, and -# Trivy is installed by the pinned setup-trivy action (no curl | sh). Registry -# host is assembled from REGION + the DEV_GCP_PROJECT_ID secret so no project -# identifier is committed to this public repo. +# +# Supply chain: Trivy runs as its official image pinned by DIGEST (immutable, +# content-addressed) rather than a mutable tag or the setup-trivy binary +# installer — directly in response to the March 2026 trivy-action/setup-trivy +# tag-poisoning compromise (GHSA-69fq-xp46-6x23), where version TAGS were +# force-pushed to malicious commits and Docker Hub tags 0.69.5/0.69.6 were +# attacker-pushed. A sha256 digest cannot be re-pointed, so it is immune to +# that class of attack. 0.76.1 is a current post-remediation release pulled +# from GHCR (GitHub immutable releases). Every other third-party action is +# pinned to a full commit SHA. Registry host is assembled from REGION + the +# DEV_GCP_PROJECT_ID secret so no project identifier is committed to this +# public repo. on: pull_request: @@ -34,7 +42,8 @@ concurrency: cancel-in-progress: true env: - TRIVY_VERSION: 'v0.58.1' + # Trivy 0.76.1, GHCR, pinned by digest — see the supply-chain note above. + TRIVY_IMG: 'ghcr.io/aquasecurity/trivy@sha256:790fd66d1697395d8693d1acaa1ea702406a20f109b6fc7bac8db623eca6c06c' REGION: us-central1 CLUSTER: commonly-dev @@ -49,21 +58,23 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: fetch-depth: 0 # need base history for the diff - - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 - with: - version: ${{ env.TRIVY_VERSION }} - name: Diff fixable criticals against base env: BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_TOKEN: ${{ github.token }} # raises Trivy DB pull rate limit run: | set -euo pipefail - scan() { trivy fs --scanners vuln --severity CRITICAL --ignore-unfixed \ - --quiet --format json -o "$2" "$1"; } - ids() { python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('\n'.join(sorted({v['VulnerabilityID']+'|'+v.get('PkgName','') for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or []})))" "$1"; } - echo "scanning PR head"; scan . head.json git worktree add -q /tmp/base "$BASE_SHA" + mkdir -p /tmp/out + scan() { # $1 = host dir to scan, $2 = output filename under /tmp/out + docker run --rm -e GITHUB_TOKEN -v "$1":/scan:ro -v /tmp/out:/out \ + "$TRIVY_IMG" fs --scanners vuln --severity CRITICAL --ignore-unfixed \ + --quiet --format json -o "/out/$2" /scan + } + ids() { python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('\n'.join(sorted({v['VulnerabilityID']+'|'+v.get('PkgName','') for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or []})))" "$1"; } + echo "scanning PR head"; scan "$PWD" head.json echo "scanning base $BASE_SHA"; scan /tmp/base base.json - ids head.json > head.ids; ids base.json > base.ids + ids /tmp/out/head.json > head.ids; ids /tmp/out/base.json > base.ids new=$(comm -23 head.ids base.ids || true) if [ -n "$new" ]; then echo "::error::PR introduces fixable CRITICAL CVE(s) not on base:"; echo "$new" @@ -80,12 +91,16 @@ jobs: security-events: write steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 - with: - version: ${{ env.TRIVY_VERSION }} - - name: Dep scan - run: trivy fs --scanners vuln --severity CRITICAL,HIGH --ignore-unfixed - --quiet --format sarif -o trivy-fs.sarif --exit-code 0 . + - name: Dep scan (SARIF) + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + mkdir -p /tmp/out + docker run --rm -e GITHUB_TOKEN -v "$PWD":/scan:ro -v /tmp/out:/out \ + "$TRIVY_IMG" fs --scanners vuln --severity CRITICAL,HIGH --ignore-unfixed \ + --quiet --format sarif -o /out/trivy-fs.sarif /scan + cp /tmp/out/trivy-fs.sarif trivy-fs.sarif - name: Upload SARIF uses: github/codeql-action/upload-sarif@4bfa242aa49c5f7c6770b98670d354a7dfac05f8 # v3 with: @@ -111,15 +126,11 @@ jobs: with: cluster_name: ${{ env.CLUSTER }} location: ${{ env.REGION }} - - uses: aquasecurity/setup-trivy@81e514348e19b6112ce2a7e3ecbafe19c1e1f567 # v0.3.1 - with: - version: ${{ env.TRIVY_VERSION }} - - name: Configure docker auth - run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet - name: Scan live images env: PROJECT_ID: ${{ secrets.DEV_GCP_PROJECT_ID }} TAG_INPUT: ${{ github.event.inputs.image_tag }} + GITHUB_TOKEN: ${{ github.token }} run: | set -euo pipefail REG="${REGION}-docker.pkg.dev/${PROJECT_ID}/docker" @@ -130,19 +141,26 @@ jobs: TAG="${img##*:}" fi [ -n "${TAG}" ] || { echo "::error::could not resolve an image tag"; exit 1; } + # AR pull creds for Trivy (runs in a container, so gcloud cred-helper + # is not reachable): username/password env, password kept off argv. + TRIVY_USERNAME=oauth2accesstoken + TRIVY_PASSWORD=$(gcloud auth print-access-token) + export TRIVY_USERNAME TRIVY_PASSWORD + mkdir -p /tmp/out echo "## CVE image scan — tag \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY" echo "| image | CRIT (fixable) | HIGH (fixable) |" >> "$GITHUB_STEP_SUMMARY" echo "|---|---|---|" >> "$GITHUB_STEP_SUMMARY" fail=0 for n in commonly-backend commonly-bot commonly-frontend; do img="${REG}/${n}:${TAG}" - if ! trivy image --scanners vuln --ignore-unfixed --severity CRITICAL,HIGH \ - --quiet --format json -o "/tmp/${n}.json" "$img"; then + if ! docker run --rm -e GITHUB_TOKEN -e TRIVY_USERNAME -e TRIVY_PASSWORD \ + -v /tmp/out:/out "$TRIVY_IMG" image --scanners vuln --ignore-unfixed \ + --severity CRITICAL,HIGH --quiet --format json -o "/out/${n}.json" "$img"; then echo "| ${n} | pull/scan FAILED | - |" >> "$GITHUB_STEP_SUMMARY" fail=1; continue fi - crit=$(python3 -c "import json;d=json.load(open('/tmp/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='CRITICAL'))") - high=$(python3 -c "import json;d=json.load(open('/tmp/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='HIGH'))") + crit=$(python3 -c "import json;d=json.load(open('/tmp/out/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='CRITICAL'))") + high=$(python3 -c "import json;d=json.load(open('/tmp/out/${n}.json'));print(sum(1 for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or [] if v.get('Severity')=='HIGH'))") echo "| ${n} | ${crit} | ${high} |" >> "$GITHUB_STEP_SUMMARY" [ "${crit}" -gt 0 ] && fail=1 done From e4ef4d889997360c4d2f2c4f5a0bdc3af6c814e2 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:40:56 -0700 Subject: [PATCH 4/5] ci(security): CVE PR gate fails open on scanner errors; C locale for comm (#2807) Rhea's HOLD at 56159b99: under set -euo pipefail a Trivy DB fetch, image pull or docker error exited the step non-zero and redded the PR. A scan or parse failure now emits ::warning:: and exits 0; only a newly introduced fixable critical fails. LC_ALL=C plus sort keeps comm's input order consistent across runner locales. Nightly jobs unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N4VWLC9MiWprFWXFNjJSnr --- .github/workflows/cve-scan.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml index d1481b84b1..e033f3bb39 100644 --- a/.github/workflows/cve-scan.yml +++ b/.github/workflows/cve-scan.yml @@ -72,9 +72,14 @@ jobs: --quiet --format json -o "/out/$2" /scan } ids() { python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('\n'.join(sorted({v['VulnerabilityID']+'|'+v.get('PkgName','') for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or []})))" "$1"; } - echo "scanning PR head"; scan "$PWD" head.json - echo "scanning base $BASE_SHA"; scan /tmp/base base.json - ids /tmp/out/head.json > head.ids; ids /tmp/out/base.json > base.ids + # Fail open on a tool error (Trivy DB fetch, image pull, docker), fail + # closed only on a real finding: an outage must not red every PR. + tool_error() { echo "::warning::CVE gate skipped: $1"; exit 0; } + echo "scanning PR head"; scan "$PWD" head.json || tool_error "scan of the PR head failed" + echo "scanning base $BASE_SHA"; scan /tmp/base base.json || tool_error "scan of base failed" + export LC_ALL=C # comm and sort must agree on order, whatever the runner locale + ids /tmp/out/head.json | sort > head.ids || tool_error "could not read the head scan" + ids /tmp/out/base.json | sort > base.ids || tool_error "could not read the base scan" new=$(comm -23 head.ids base.ids || true) if [ -n "$new" ]; then echo "::error::PR introduces fixable CRITICAL CVE(s) not on base:"; echo "$new" From d0212a1be721cf09ef836c1c38246221f16fc56b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:24:37 -0700 Subject: [PATCH 5/5] fix(ci): the CVE gate's base checkout and scan diff fail open like the scans A failed base checkout or comm now warns and passes through tool_error, which moves above its first use. Only a new fixable critical fails. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N4VWLC9MiWprFWXFNjJSnr --- .github/workflows/cve-scan.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cve-scan.yml b/.github/workflows/cve-scan.yml index e033f3bb39..d3cddf4008 100644 --- a/.github/workflows/cve-scan.yml +++ b/.github/workflows/cve-scan.yml @@ -64,7 +64,11 @@ jobs: GITHUB_TOKEN: ${{ github.token }} # raises Trivy DB pull rate limit run: | set -euo pipefail - git worktree add -q /tmp/base "$BASE_SHA" + # Fail open on a tool error (checkout, Trivy DB fetch, image pull, + # docker, the diff itself), fail closed only on a real finding: an + # outage must not red every PR. + tool_error() { echo "::warning::CVE gate skipped: $1"; exit 0; } + git worktree add -q /tmp/base "$BASE_SHA" || tool_error "could not check out base $BASE_SHA" mkdir -p /tmp/out scan() { # $1 = host dir to scan, $2 = output filename under /tmp/out docker run --rm -e GITHUB_TOKEN -v "$1":/scan:ro -v /tmp/out:/out \ @@ -72,15 +76,12 @@ jobs: --quiet --format json -o "/out/$2" /scan } ids() { python3 -c "import json,sys;d=json.load(open(sys.argv[1]));print('\n'.join(sorted({v['VulnerabilityID']+'|'+v.get('PkgName','') for r in d.get('Results') or [] for v in r.get('Vulnerabilities') or []})))" "$1"; } - # Fail open on a tool error (Trivy DB fetch, image pull, docker), fail - # closed only on a real finding: an outage must not red every PR. - tool_error() { echo "::warning::CVE gate skipped: $1"; exit 0; } echo "scanning PR head"; scan "$PWD" head.json || tool_error "scan of the PR head failed" echo "scanning base $BASE_SHA"; scan /tmp/base base.json || tool_error "scan of base failed" export LC_ALL=C # comm and sort must agree on order, whatever the runner locale ids /tmp/out/head.json | sort > head.ids || tool_error "could not read the head scan" ids /tmp/out/base.json | sort > base.ids || tool_error "could not read the base scan" - new=$(comm -23 head.ids base.ids || true) + new=$(comm -23 head.ids base.ids) || tool_error "could not compare the scans" if [ -n "$new" ]; then echo "::error::PR introduces fixable CRITICAL CVE(s) not on base:"; echo "$new" exit 1