From dc701ced9d3bb6c4432b952f1c00419760bdc09a Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Mar 2026 15:21:36 +0100 Subject: [PATCH 1/4] ci(govulncheck): add PR checks, SARIF Code Scanning, and automated remediation Replace Dependabot Security Alerts with govulncheck-based scanning. Dependabot flags every CVE in transitive deps regardless of reachability, generating alert fatigue. govulncheck uses call graph analysis to only report vulnerabilities in code paths that are actually called. Changes: - govulncheck.yml: add pull_request trigger (blocks PRs with reachable vulns), SARIF upload to GitHub Code Scanning via the official golang/govulncheck-action, always installs govulncheck@latest, adds concurrency group, pins all actions to latest SHA - govulncheck-fix.yml: new weekly remediation workflow that scans all modules (core + 65+ contrib), identifies fixes, updates go.mod files, and opens a PR automatically - govulncheck-fix.sh: remediation script that parses govulncheck JSON output, extracts module@fixedVersion pairs, runs go get + go mod tidy across all affected modules Next step: disable Dependabot Security Alerts for Go in GitHub repo settings once this merges (Settings > Code security > Dependabot alerts). --- .github/workflows/apps/govulncheck-fix.sh | 163 ++++++++++++++++++++++ .github/workflows/govulncheck-fix.yml | 48 +++++++ .github/workflows/govulncheck.yml | 60 ++++++-- 3 files changed, 257 insertions(+), 14 deletions(-) create mode 100755 .github/workflows/apps/govulncheck-fix.sh create mode 100644 .github/workflows/govulncheck-fix.yml diff --git a/.github/workflows/apps/govulncheck-fix.sh b/.github/workflows/apps/govulncheck-fix.sh new file mode 100755 index 00000000000..42533950a2c --- /dev/null +++ b/.github/workflows/apps/govulncheck-fix.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# govulncheck-fix.sh: Scan all modules for reachable vulnerabilities, apply +# available fixes by updating affected go.mod files, and report results. +# +# Relies on govulncheck@latest being installed and on PATH. +# +# Outputs (via GITHUB_OUTPUT): +# has_fixes=true|false +# +# Side effects: +# - Modifies go.mod/go.sum files for modules with vulnerable dependencies +# - Writes /tmp/govulncheck-fix-commit.txt (used by git commit -F) +# - Writes /tmp/govulncheck-fix-body.md (used as PR description) + +set -euo pipefail + +FIXES_FILE=$(mktemp) +readonly FIXES_FILE +GITHUB_OUTPUT="${GITHUB_OUTPUT:-/dev/null}" + +# ── parse_findings ───────────────────────────────────────────────────────────── +# Reads govulncheck streaming JSON from stdin and extracts "module fixedVersion" +# pairs for findings that have an available fix. +# +# govulncheck JSON emits a series of Message objects (one per line). Each +# Message may contain a Finding. Finding.trace[-1] is the vulnerable symbol; +# its .module is the third-party module that contains the vulnerability and +# should be upgraded to .fixed_version. +# +# We skip stdlib vulnerabilities (module == "stdlib") since those are fixed +# by upgrading Go itself, not via go get. +parse_findings() { + jq -r ' + select(.finding != null) | + select(.finding.fixed_version != null and .finding.fixed_version != "") | + select(.finding.trace != null and (.finding.trace | length) > 0) | + select(.finding.trace[-1].module != null and .finding.trace[-1].module != "") | + select(.finding.trace[-1].module != "stdlib") | + .finding.trace[-1].module + " " + .finding.fixed_version + ' +} + +# ── Scan core packages ───────────────────────────────────────────────────────── +echo "==> Scanning core packages..." +govulncheck -json \ + ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... \ + 2>/dev/null | parse_findings >> "${FIXES_FILE}" || true + +# ── Scan contrib modules ─────────────────────────────────────────────────────── +echo "==> Scanning contrib modules..." +while IFS= read -r -d '' gomod; do + dir=$(dirname "${gomod}") + + # govulncheck requires at least one .go file in the target directory. + go_files=$(find "${dir}" -maxdepth 1 -type f -name '*.go' | wc -l) + [[ "${go_files}" -eq 0 ]] && dir=$(realpath "$(find "${dir}" -mindepth 1 -maxdepth 1 -type d | head -1)") + + echo " Checking ${dir}" + govulncheck -C "${dir}" -json . 2>/dev/null | parse_findings >> "${FIXES_FILE}" || true +done < <(find ./contrib -mindepth 2 -type f -name go.mod -print0) + +# ── Check for fixes ──────────────────────────────────────────────────────────── +if [[ ! -s "${FIXES_FILE}" ]]; then + echo "No reachable vulnerabilities with available fixes found." + echo "has_fixes=false" >> "${GITHUB_OUTPUT}" + rm -f "${FIXES_FILE}" + exit 0 +fi + +# Deduplicate: for the same module, keep only the highest fixed version. +# sort -k2,2V sorts by semver (GNU sort extension, available on Ubuntu runners). +sort -u "${FIXES_FILE}" \ + | sort -t' ' -k1,1 -k2,2Vr \ + | awk '!seen[$1]++' \ + > "${FIXES_FILE}.dedup" +mv "${FIXES_FILE}.dedup" "${FIXES_FILE}" + +echo "" +echo "==> Vulnerabilities with fixes:" +cat "${FIXES_FILE}" +echo "" + +# ── Apply fixes ──────────────────────────────────────────────────────────────── +echo "==> Applying fixes..." +while IFS=' ' read -r module fixed_version; do + echo " Updating ${module} → ${fixed_version}" + + # Update the module in every go.mod file that references it. + while IFS= read -r gomod; do + dir=$(dirname "${gomod}") + echo " → ${dir}" + go -C "${dir}" get "${module}@${fixed_version}" || { + echo " ✗ Failed to update ${module} in ${dir}, skipping" + } + done < <(grep -rl "${module}" --include='go.mod' .) + +done < "${FIXES_FILE}" + +# ── Tidy all modified modules ───────────────────────────────────────────────── +echo "==> Running go mod tidy..." +while IFS= read -r gomod; do + dir=$(dirname "${gomod}") + go -C "${dir}" mod tidy || true +done < <(find . -name go.mod \ + -not -path './.git/*' \ + -not -path './_tools/*' \ + -not -path './scripts/*') + +# ── Check if anything changed ───────────────────────────────────────────────── +if git diff --quiet; then + echo "No changes after applying fixes (all dependencies already at required versions)." + echo "has_fixes=false" >> "${GITHUB_OUTPUT}" + rm -f "${FIXES_FILE}" + exit 0 +fi + +# ── Generate PR body ─────────────────────────────────────────────────────────── +{ + cat << 'HEADER' +## Summary + +This PR was automatically generated by the [Govulncheck Remediation](.github/workflows/govulncheck-fix.yml) workflow. + +`govulncheck` identified the following reachable vulnerabilities with available fixes and updated the affected `go.mod` files. + +> Unlike Dependabot, `govulncheck` uses call graph analysis to only report vulnerabilities in code paths that are **actually reachable** from this module, eliminating false positives from transitively-imported-but-never-called code. + +### Updated Dependencies + +HEADER + + while IFS=' ' read -r module fixed_version; do + echo "- \`${module}\` → \`${fixed_version}\`" + done < "${FIXES_FILE}" + + cat << 'FOOTER' + +## Test Plan + +- [ ] CI passes with updated dependencies +- [ ] `govulncheck-tests` job reports no remaining vulnerabilities for updated packages + +## References + +- [Filippo Valsorda: Turn Dependabot Off](https://words.filippo.io/dependabot/) — rationale for govulncheck over Dependabot alerts +- [golang/govulncheck-action](https://github.com/golang/govulncheck-action) — official Go team action with SARIF support +- [govulncheck documentation](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) — JSON output format +FOOTER +} > /tmp/govulncheck-fix-body.md + +# ── Generate commit message file (used by git commit -F) ───────────────────── +{ + echo "fix(deps): update dependencies with reachable vulnerabilities" + echo "" + echo "Updated by govulncheck remediation workflow." + echo "" + while IFS=' ' read -r module fixed_version; do + echo "- ${module} -> ${fixed_version}" + done < "${FIXES_FILE}" +} > /tmp/govulncheck-fix-commit.txt + +echo "has_fixes=true" >> "${GITHUB_OUTPUT}" +rm -f "${FIXES_FILE}" diff --git a/.github/workflows/govulncheck-fix.yml b/.github/workflows/govulncheck-fix.yml new file mode 100644 index 00000000000..dcb70224775 --- /dev/null +++ b/.github/workflows/govulncheck-fix.yml @@ -0,0 +1,48 @@ +name: Govulncheck Remediation + +on: + schedule: + - cron: '00 06 * * 1' # Mondays at 6 AM UTC + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + govulncheck-fix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + with: + go-version: stable + cache-dependency-path: '**/go.sum' + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + - name: Scan and fix vulnerabilities + id: fix + run: ./.github/workflows/apps/govulncheck-fix.sh + - name: Configure git + if: steps.fix.outputs.has_fixes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + gh auth setup-git + env: + GH_TOKEN: ${{ github.token }} + - name: Create Pull Request + if: steps.fix.outputs.has_fixes == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + branch="automation/govulncheck-fix/${{ github.run_id }}" + git checkout -b "${branch}" + git add -- '**/go.mod' '**/go.sum' + git commit -F /tmp/govulncheck-fix-commit.txt + git push origin "${branch}" + gh pr create \ + --title "fix(deps): update dependencies with reachable vulnerabilities" \ + --body-file /tmp/govulncheck-fix-body.md \ + --head="${branch}" diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 0ccddc955ae..c224255c622 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -1,5 +1,12 @@ -name: Nightly govulncheck +name: Govulncheck on: + pull_request: + paths: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" + - ".github/workflows/govulncheck.yml" + - ".github/workflows/apps/govulncheck-contribs-v2.sh" workflow_call: # allows to reuse this workflow inputs: ref: @@ -20,27 +27,52 @@ on: - cron: '00 00 * * *' workflow_dispatch: +concurrency: + group: govulncheck-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + permissions: contents: read + security-events: write # required for SARIF upload to GitHub Code Scanning jobs: + # Non-blocking: uploads SARIF to GitHub Code Scanning (replaces Dependabot Security Alerts). + # Uses the official golang/govulncheck-action which always uses the latest govulncheck. + govulncheck-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} + - name: Run govulncheck + uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 + with: + go-version-input: stable + output-format: sarif + output-file: govulncheck.sarif + - name: Upload SARIF to GitHub Code Scanning + if: always() + uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + with: + sarif_file: govulncheck.sarif + category: govulncheck + + # Blocking: fails the build if any reachable vulnerability is found. + # Scans both core packages and all contrib modules (each with its own go.mod). govulncheck-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - ref: ${{ inputs.ref || github.ref }} - - name: Setup Go and development tools - uses: ./.github/actions/setup-go + ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: go-version: stable - tools-dir: ${{ github.workspace }}/_tools - tools-bin: ${{ github.workspace }}/bin - - name: Run govulncheck - run: |- - export PATH="${{ github.workspace }}/bin:${PATH}" - govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... - - name: Run govulncheck-contribs - run: |- - export PATH="${{ github.workspace }}/bin:${PATH}" - ./.github/workflows/apps/govulncheck-contribs-v2.sh + cache-dependency-path: '**/go.sum' + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + - name: Run govulncheck (core) + run: govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... + - name: Run govulncheck (contrib) + run: ./.github/workflows/apps/govulncheck-contribs-v2.sh From 5bd1ecd73694902337555d4a80ad6abeebb22ea5 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Mar 2026 15:44:22 +0100 Subject: [PATCH 2/4] ci(govulncheck): replace golang/govulncheck-action with direct invocation golang/govulncheck-action is blocked by the DataDog enterprise action allowlist (it internally uses actions/checkout@v4.1.1 and actions/setup-go@v5.0.0 which are not in the allowlist). Replace with: setup-go + go install govulncheck@latest + govulncheck -format sarif, which uses only GitHub-created actions (allowed). --- .github/workflows/govulncheck.yml | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index c224255c622..901aac80201 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -36,20 +36,30 @@ permissions: security-events: write # required for SARIF upload to GitHub Code Scanning jobs: - # Non-blocking: uploads SARIF to GitHub Code Scanning (replaces Dependabot Security Alerts). - # Uses the official golang/govulncheck-action which always uses the latest govulncheck. + # Non-blocking: generates SARIF and uploads to GitHub Code Scanning. + # Replaces Dependabot Security Alerts with reachability-aware findings. + # Uses govulncheck@latest installed directly (golang/govulncheck-action is + # not in the DataDog enterprise action allowlist). govulncheck-analysis: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} - - name: Run govulncheck - uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 + - name: Setup Go + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: - go-version-input: stable - output-format: sarif - output-file: govulncheck.sarif + go-version: stable + cache-dependency-path: '**/go.sum' + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + - name: Run govulncheck (SARIF) + # -format sarif exits 0 even when vulnerabilities are found, so the + # upload step always runs. The blocking check is in govulncheck-tests. + run: |- + govulncheck -format sarif \ + ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... \ + > govulncheck.sarif || true - name: Upload SARIF to GitHub Code Scanning if: always() uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 From 169fc6596cb683fd4b73b2827abe49cd3f4bffa2 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Thu, 26 Mar 2026 11:05:49 +0100 Subject: [PATCH 3/4] ci(govulncheck): add go.work/go.work.sum to path filters, document contrib SARIF gap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add go.work and go.work.sum to pull_request.paths so workspace-level dependency changes (replacements, workspace pins) trigger govulncheck instead of silently bypassing it. Add a TODO comment documenting that contrib module vulnerabilities are caught by the blocking govulncheck-tests check but do not appear in GitHub Code Scanning alerts — contrib SARIF upload is a follow-up improvement requiring multi-SARIF tooling. Addresses Codex review feedback on PR #4595. --- .github/workflows/govulncheck.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index 901aac80201..afc6e1d6aeb 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -5,6 +5,8 @@ on: - "**/*.go" - "**/go.mod" - "**/go.sum" + - "go.work" + - "go.work.sum" - ".github/workflows/govulncheck.yml" - ".github/workflows/apps/govulncheck-contribs-v2.sh" workflow_call: # allows to reuse this workflow @@ -40,6 +42,11 @@ jobs: # Replaces Dependabot Security Alerts with reachability-aware findings. # Uses govulncheck@latest installed directly (golang/govulncheck-action is # not in the DataDog enterprise action allowlist). + # NOTE: Only core packages are scanned here. Contrib modules (each with + # their own go.mod) are scanned in govulncheck-tests, which blocks merges, + # but contrib vulnerabilities do not appear in GitHub Code Scanning alerts. + # TODO: extend SARIF upload to cover contrib modules for full Code Scanning + # visibility — requires multi-SARIF upload or a contrib-specific scan script. govulncheck-analysis: runs-on: ubuntu-latest steps: From 31f881414181742c7a9d86a7399cee24a7aafe63 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Wed, 25 Mar 2026 16:18:31 +0100 Subject: [PATCH 4/4] ci(govulncheck): add persist-credentials: false to checkout steps Revokes the GitHub token from the runner filesystem after checkout, preventing any code running in the job from reading it. This includes govulncheck downloading the vuln DB, go get fetching modules, and any transitive dependency code executed during analysis. The govulncheck-fix.yml workflow still works correctly: its push step uses gh auth setup-git with GH_TOKEN env var, which configures its own credential helper independently of the checkout-persisted credential. Ref: https://words.filippo.io/dependabot/ --- .github/workflows/govulncheck-fix.yml | 2 ++ .github/workflows/govulncheck.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/govulncheck-fix.yml b/.github/workflows/govulncheck-fix.yml index dcb70224775..06edfa123ce 100644 --- a/.github/workflows/govulncheck-fix.yml +++ b/.github/workflows/govulncheck-fix.yml @@ -14,6 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml index afc6e1d6aeb..14806110217 100644 --- a/.github/workflows/govulncheck.yml +++ b/.github/workflows/govulncheck.yml @@ -53,6 +53,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} + persist-credentials: false - name: Setup Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: @@ -82,6 +83,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }} + persist-credentials: false - name: Setup Go uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 with: