From 463a71b26a02d01d78cb7c91762475d299a9fb86 Mon Sep 17 00:00:00 2001 From: initializ-mk Date: Mon, 14 Sep 2026 12:33:17 -0400 Subject: [PATCH 1/3] chore(ci): add grype vulnerability scan on every PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit grype (anchore/scan-action) on each PR + integration-branch push: fail on a HIGH/CRITICAL vuln that has a fix (only-fixed), SARIF to the Security tab. .grype.yaml excludes node_modules — prebuilt tool binaries (esbuild) generate un-actionable stdlib CVEs; the real dependency surface is cataloged from the lockfiles/go.mod. --- .github/workflows/security-scan.yml | 32 +++++++++++++++++++++++++++++ .grype.yaml | 5 +++++ 2 files changed, 37 insertions(+) create mode 100644 .github/workflows/security-scan.yml create mode 100644 .grype.yaml diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..7aaf4ca --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,32 @@ +name: security-scan + +# Grype vulnerability scan on every PR (and pushes to the integration branches). +# Fails on a HIGH or CRITICAL vuln that HAS a fix (only-fixed) — un-actionable +# no-fix advisories don't block. Findings also land in the Security tab (SARIF). +on: + pull_request: + push: + branches: [develop, main] + +permissions: + contents: read + security-events: write + +jobs: + grype: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - id: scan + uses: anchore/scan-action@v6 + with: + path: "." + fail-build: true + severity-cutoff: high + only-fixed: true + - name: Upload SARIF + if: always() + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ steps.scan.outputs.sarif }} + category: grype diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 0000000..2120b9b --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,5 @@ +# Grype config for the security-scan CI + local `grype dir:.`. +# Exclude installed node_modules: prebuilt tool binaries (e.g. esbuild) generate +# un-actionable stdlib CVEs; the real npm dependency surface comes from lockfiles. +exclude: + - "./**/node_modules/**" From f3fb5750b62d322bc6f86141ad5185620badb34a Mon Sep 17 00:00:00 2001 From: initializ-mk Date: Mon, 14 Sep 2026 12:51:54 -0400 Subject: [PATCH 2/3] fix(ci): make grype SARIF upload best-effort MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upload SARIF needs GHAS code scanning, which private repos lack — the always()-upload failed the whole grype check on a clean scan. continue-on-error keeps grype fail-build as the real gate. --- .github/workflows/security-scan.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 7aaf4ca..d3d86ac 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -25,6 +25,7 @@ jobs: severity-cutoff: high only-fixed: true - name: Upload SARIF + continue-on-error: true # SARIF upload needs GHAS code scanning; best-effort so a private repo without it does not red the check if: always() uses: github/codeql-action/upload-sarif@v3 with: From 91ee4ba8d940896b019ae112dee02fd07033535d Mon Sep 17 00:00:00 2001 From: initializ-mk Date: Mon, 14 Sep 2026 13:02:36 -0400 Subject: [PATCH 3/3] ci(grype): post scan findings as a sticky PR comment Install grype (pinned) and render --only-fixed as a table, then upsert one sticky PR comment via marocchino/sticky-pull-request-comment (SHA-pinned to v3.0.5). Best-effort (continue-on-error) so it never gates the check; gives finding visibility on private repos without GHAS code scanning. --- .github/workflows/security-scan.yml | 38 +++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index d3d86ac..e7cf886 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -2,7 +2,9 @@ name: security-scan # Grype vulnerability scan on every PR (and pushes to the integration branches). # Fails on a HIGH or CRITICAL vuln that HAS a fix (only-fixed) — un-actionable -# no-fix advisories don't block. Findings also land in the Security tab (SARIF). +# no-fix advisories don't block. All fixable findings post as a sticky PR-comment +# table; the SARIF upload is best-effort (needs GHAS code scanning, absent on +# private repos). on: pull_request: push: @@ -11,6 +13,7 @@ on: permissions: contents: read security-events: write + pull-requests: write jobs: grype: @@ -25,9 +28,40 @@ jobs: severity-cutoff: high only-fixed: true - name: Upload SARIF - continue-on-error: true # SARIF upload needs GHAS code scanning; best-effort so a private repo without it does not red the check if: always() + continue-on-error: true # needs GHAS code scanning; best-effort so a private repo without it does not red the check uses: github/codeql-action/upload-sarif@v3 with: sarif_file: ${{ steps.scan.outputs.sarif }} category: grype + - name: Render grype table + if: always() && github.event_name == 'pull_request' + continue-on-error: true + env: + GRYPE_VERSION: v0.97.1 + run: | + curl -sSfL "https://raw.githubusercontent.com/anchore/grype/${GRYPE_VERSION}/install.sh" | sh -s -- -b "$RUNNER_TEMP" "$GRYPE_VERSION" + "$RUNNER_TEMP/grype" dir:. --only-fixed -q -o json > "$RUNNER_TEMP/grype.json" 2>/dev/null || echo '{"matches":[]}' > "$RUNNER_TEMP/grype.json" + n=$(jq '.matches | length' "$RUNNER_TEMP/grype.json") + { + if [ "$n" -eq 0 ]; then + echo "### 🔎 Grype — no fixable vulnerabilities ✅" + else + echo "### 🔎 Grype — $n fixable finding(s)" + echo + jq -r ' + "| Name | Installed | Fixed In | Type | Vulnerability | Severity | EPSS | Risk |", + "| --- | --- | --- | --- | --- | --- | --- | --- |", + (.matches[] | "| \(.artifact.name) | \(.artifact.version) | \((.vulnerability.fix.versions // []) | join(", ")) | \(.artifact.type) | \(.vulnerability.id) | \(.vulnerability.severity) | \((.vulnerability.epss[0].epss // null) | if . == null then "N/A" else ((. * 1000 | round / 10) | tostring) + "%" end) | \((.vulnerability.risk // 0) * 10 | round / 10) |") + ' "$RUNNER_TEMP/grype.json" + echo + echo "_HIGH+ with a fix fail the check; lower severities shown for visibility._" + fi + } > grype-report.txt + - name: Post findings on PR + if: always() && github.event_name == 'pull_request' + continue-on-error: true + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 + with: + header: grype + path: grype-report.txt