Skip to content

Commit ee8dce2

Browse files
kakkoyundarccio
andauthored
feat(ci): govulncheck - add PR checks, SARIF Code Scanning, and automated remediation (#4595)
### What does this PR do? Migrates vulnerability scanning from Dependabot Security Alerts to \`govulncheck\`-based scanning. Three changes: 1. **\`govulncheck.yml\`** (updated): adds a \`pull_request\` trigger so govulncheck now gates PRs; adds a SARIF upload job that feeds into GitHub Code Scanning (replaces Dependabot alerts in the Security tab); always installs \`govulncheck@latest\`; adds a concurrency group to cancel stale runs; pins all actions to latest SHA. 2. **\`govulncheck-fix.yml\`** (new): weekly remediation workflow (Mondays 6 AM UTC, also \`workflow_dispatch\`) that scans all modules, identifies reachable vulnerabilities with available fixes, updates the affected \`go.mod\` files, and opens a PR automatically. 3. **\`govulncheck-fix.sh\`** (new): remediation script that parses \`govulncheck -json\` streaming output, extracts \`(module, fixedVersion)\` pairs from \`finding.trace[-1]\`, runs \`go get module@version\` across all affected \`go.mod\` files (core + all 65+ contrib modules), and runs \`go mod tidy\`. ### Motivation Dependabot Security Alerts flag every CVE in transitive dependencies regardless of whether the vulnerable code is actually reachable — the push that triggered this PR showed 32 alerts. \`govulncheck\` uses Go call graph analysis to only report vulnerabilities in code paths that are **actually called**, eliminating false positives. After this merges, Dependabot Security Alerts for Go can be disabled in repo settings (Settings → Code security → Dependabot alerts). The \`dependabot.yml\` config is kept as-is since it only manages GitHub Actions version updates, which is still useful. ### How SARIF + GitHub Code Scanning works The \`govulncheck-analysis\` job runs \`govulncheck -format sarif\` and uploads the result via \`github/codeql-action/upload-sarif\`. Here is what happens end-to-end: 1. **SARIF file generated**: \`govulncheck -format sarif\` emits a structured JSON file (Static Analysis Results Interchange Format) describing each reachable vulnerability — including file path, line number, severity, CVE reference, and the full call chain that reaches the vulnerable symbol. Critically, \`-format sarif\` exits 0 even when vulnerabilities are found, so the upload step always runs regardless of findings. 2. **Uploaded to GitHub Code Scanning**: The SARIF is posted to the GitHub Code Scanning API under the \`govulncheck\` category. GitHub parses it and creates persistent alerts in **Security → Code scanning alerts**. Each alert includes the call stack showing exactly how the vulnerable code is reached from this module. 3. **De-duplication across runs**: If the same vulnerability appears in multiple scans, GitHub tracks it as a single alert (not a new one each time). When a dependency is updated and the vulnerability disappears from the SARIF, GitHub automatically closes the alert. 4. **PR annotations**: When a PR introduces a new reachable vulnerability, GitHub adds an inline annotation on the diff pointing to the call site. 5. **Two-job design — soft gate + hard gate**: - \`govulncheck-analysis\`: non-blocking, uploads SARIF for Code Scanning visibility. Always succeeds so the upload always runs. - \`govulncheck-tests\`: blocking PR check, runs \`govulncheck\` in text mode (exits non-zero on any reachable finding). This is the gate that fails the PR. > **Why not use \`golang/govulncheck-action\`?** The official action is blocked by the DataDog enterprise action allowlist (it internally uses \`actions/checkout@v4\` and \`actions/setup-go@v5\` which are not in the allowlist pattern). The \`govulncheck -format sarif\` flag is available natively in \`govulncheck@latest\`, so we install it directly via \`go install\` instead. ### Reviewer's Checklist - [ ] Changed code has unit tests for its functionality at or near 100% coverage. - [ ] [System-Tests](https://github.com/DataDog/system-tests/) covering this feature have been added and enabled with the va.b.c-dev version tag. - [ ] There is a benchmark for any new code, or changes to existing code. - [ ] If this interacts with the agent in a new way, a system test has been added. - [ ] New code is free of linting errors. You can check this by running \`make lint\` locally. - [ ] New code doesn't break existing tests. You can check this by running \`make test\` locally. - [ ] Add an appropriate team label so this PR gets put in the right place for the release notes. - [ ] All generated files are up to date. You can check this by running \`make generate\` locally. - [ ] Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild. Make sure all nested modules are up to date by running \`make fix-modules\` locally. ### References - [Filippo Valsorda: Turn Dependabot Off](https://words.filippo.io/dependabot/) — rationale for replacing Dependabot with govulncheck - [golang/govulncheck-action](https://github.com/golang/govulncheck-action) — official Go team action with SARIF support - [Setting up govulncheck with GitHub Code Scanning](https://www.jvt.me/posts/2025/09/11/govulncheck-github-actions/) — detailed integration walkthrough - [govulncheck JSON output format](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) — streaming JSON format used by the remediation script - [CodeQL Action v4](https://github.com/github/codeql-action) — SARIF upload action (v3 deprecated Dec 2026) Co-authored-by: dario.castane <[email protected]>
1 parent 41183c0 commit ee8dce2

3 files changed

Lines changed: 275 additions & 14 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#!/usr/bin/env bash
2+
# govulncheck-fix.sh: Scan all modules for reachable vulnerabilities, apply
3+
# available fixes by updating affected go.mod files, and report results.
4+
#
5+
# Relies on govulncheck@latest being installed and on PATH.
6+
#
7+
# Outputs (via GITHUB_OUTPUT):
8+
# has_fixes=true|false
9+
#
10+
# Side effects:
11+
# - Modifies go.mod/go.sum files for modules with vulnerable dependencies
12+
# - Writes /tmp/govulncheck-fix-commit.txt (used by git commit -F)
13+
# - Writes /tmp/govulncheck-fix-body.md (used as PR description)
14+
15+
set -euo pipefail
16+
17+
FIXES_FILE=$(mktemp)
18+
readonly FIXES_FILE
19+
GITHUB_OUTPUT="${GITHUB_OUTPUT:-/dev/null}"
20+
21+
# ── parse_findings ─────────────────────────────────────────────────────────────
22+
# Reads govulncheck streaming JSON from stdin and extracts "module fixedVersion"
23+
# pairs for findings that have an available fix.
24+
#
25+
# govulncheck JSON emits a series of Message objects (one per line). Each
26+
# Message may contain a Finding. Finding.trace[-1] is the vulnerable symbol;
27+
# its .module is the third-party module that contains the vulnerability and
28+
# should be upgraded to .fixed_version.
29+
#
30+
# We skip stdlib vulnerabilities (module == "stdlib") since those are fixed
31+
# by upgrading Go itself, not via go get.
32+
parse_findings() {
33+
jq -r '
34+
select(.finding != null) |
35+
select(.finding.fixed_version != null and .finding.fixed_version != "") |
36+
select(.finding.trace != null and (.finding.trace | length) > 0) |
37+
select(.finding.trace[-1].module != null and .finding.trace[-1].module != "") |
38+
select(.finding.trace[-1].module != "stdlib") |
39+
.finding.trace[-1].module + " " + .finding.fixed_version
40+
'
41+
}
42+
43+
# ── Scan core packages ─────────────────────────────────────────────────────────
44+
echo "==> Scanning core packages..."
45+
govulncheck -json \
46+
./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... \
47+
2>/dev/null | parse_findings >> "${FIXES_FILE}" || true
48+
49+
# ── Scan contrib modules ───────────────────────────────────────────────────────
50+
echo "==> Scanning contrib modules..."
51+
while IFS= read -r -d '' gomod; do
52+
dir=$(dirname "${gomod}")
53+
54+
# govulncheck requires at least one .go file in the target directory.
55+
go_files=$(find "${dir}" -maxdepth 1 -type f -name '*.go' | wc -l)
56+
[[ "${go_files}" -eq 0 ]] && dir=$(realpath "$(find "${dir}" -mindepth 1 -maxdepth 1 -type d | head -1)")
57+
58+
echo " Checking ${dir}"
59+
govulncheck -C "${dir}" -json . 2>/dev/null | parse_findings >> "${FIXES_FILE}" || true
60+
done < <(find ./contrib -mindepth 2 -type f -name go.mod -print0)
61+
62+
# ── Check for fixes ────────────────────────────────────────────────────────────
63+
if [[ ! -s "${FIXES_FILE}" ]]; then
64+
echo "No reachable vulnerabilities with available fixes found."
65+
echo "has_fixes=false" >> "${GITHUB_OUTPUT}"
66+
rm -f "${FIXES_FILE}"
67+
exit 0
68+
fi
69+
70+
# Deduplicate: for the same module, keep only the highest fixed version.
71+
# sort -k2,2V sorts by semver (GNU sort extension, available on Ubuntu runners).
72+
sort -u "${FIXES_FILE}" \
73+
| sort -t' ' -k1,1 -k2,2Vr \
74+
| awk '!seen[$1]++' \
75+
> "${FIXES_FILE}.dedup"
76+
mv "${FIXES_FILE}.dedup" "${FIXES_FILE}"
77+
78+
echo ""
79+
echo "==> Vulnerabilities with fixes:"
80+
cat "${FIXES_FILE}"
81+
echo ""
82+
83+
# ── Apply fixes ────────────────────────────────────────────────────────────────
84+
echo "==> Applying fixes..."
85+
while IFS=' ' read -r module fixed_version; do
86+
echo " Updating ${module}${fixed_version}"
87+
88+
# Update the module in every go.mod file that references it.
89+
while IFS= read -r gomod; do
90+
dir=$(dirname "${gomod}")
91+
echo "${dir}"
92+
go -C "${dir}" get "${module}@${fixed_version}" || {
93+
echo " ✗ Failed to update ${module} in ${dir}, skipping"
94+
}
95+
done < <(grep -rl "${module}" --include='go.mod' .)
96+
97+
done < "${FIXES_FILE}"
98+
99+
# ── Tidy all modified modules ─────────────────────────────────────────────────
100+
echo "==> Running go mod tidy..."
101+
while IFS= read -r gomod; do
102+
dir=$(dirname "${gomod}")
103+
go -C "${dir}" mod tidy || true
104+
done < <(find . -name go.mod \
105+
-not -path './.git/*' \
106+
-not -path './_tools/*' \
107+
-not -path './scripts/*')
108+
109+
# ── Check if anything changed ─────────────────────────────────────────────────
110+
if git diff --quiet; then
111+
echo "No changes after applying fixes (all dependencies already at required versions)."
112+
echo "has_fixes=false" >> "${GITHUB_OUTPUT}"
113+
rm -f "${FIXES_FILE}"
114+
exit 0
115+
fi
116+
117+
# ── Generate PR body ───────────────────────────────────────────────────────────
118+
{
119+
cat << 'HEADER'
120+
## Summary
121+
122+
This PR was automatically generated by the [Govulncheck Remediation](.github/workflows/govulncheck-fix.yml) workflow.
123+
124+
`govulncheck` identified the following reachable vulnerabilities with available fixes and updated the affected `go.mod` files.
125+
126+
> 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.
127+
128+
### Updated Dependencies
129+
130+
HEADER
131+
132+
while IFS=' ' read -r module fixed_version; do
133+
echo "- \`${module}\`\`${fixed_version}\`"
134+
done < "${FIXES_FILE}"
135+
136+
cat << 'FOOTER'
137+
138+
## Test Plan
139+
140+
- [ ] CI passes with updated dependencies
141+
- [ ] `govulncheck-tests` job reports no remaining vulnerabilities for updated packages
142+
143+
## References
144+
145+
- [Filippo Valsorda: Turn Dependabot Off](https://words.filippo.io/dependabot/) — rationale for govulncheck over Dependabot alerts
146+
- [golang/govulncheck-action](https://github.com/golang/govulncheck-action) — official Go team action with SARIF support
147+
- [govulncheck documentation](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) — JSON output format
148+
FOOTER
149+
} > /tmp/govulncheck-fix-body.md
150+
151+
# ── Generate commit message file (used by git commit -F) ─────────────────────
152+
{
153+
echo "fix(deps): update dependencies with reachable vulnerabilities"
154+
echo ""
155+
echo "Updated by govulncheck remediation workflow."
156+
echo ""
157+
while IFS=' ' read -r module fixed_version; do
158+
echo "- ${module} -> ${fixed_version}"
159+
done < "${FIXES_FILE}"
160+
} > /tmp/govulncheck-fix-commit.txt
161+
162+
echo "has_fixes=true" >> "${GITHUB_OUTPUT}"
163+
rm -f "${FIXES_FILE}"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Govulncheck Remediation
2+
3+
on:
4+
schedule:
5+
- cron: '00 06 * * 1' # Mondays at 6 AM UTC
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
govulncheck-fix:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17+
with:
18+
persist-credentials: false
19+
- name: Setup Go
20+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
21+
with:
22+
go-version: stable
23+
cache-dependency-path: '**/go.sum'
24+
- name: Install govulncheck
25+
run: go install golang.org/x/vuln/cmd/govulncheck@latest
26+
- name: Scan and fix vulnerabilities
27+
id: fix
28+
run: ./.github/workflows/apps/govulncheck-fix.sh
29+
- name: Configure git
30+
if: steps.fix.outputs.has_fixes == 'true'
31+
run: |
32+
git config user.name "github-actions[bot]"
33+
git config user.email "github-actions[bot]@users.noreply.github.com"
34+
gh auth setup-git
35+
env:
36+
GH_TOKEN: ${{ github.token }}
37+
- name: Create Pull Request
38+
if: steps.fix.outputs.has_fixes == 'true'
39+
env:
40+
GH_TOKEN: ${{ github.token }}
41+
run: |
42+
branch="automation/govulncheck-fix/${{ github.run_id }}"
43+
git checkout -b "${branch}"
44+
git add -- '**/go.mod' '**/go.sum'
45+
git commit -F /tmp/govulncheck-fix-commit.txt
46+
git push origin "${branch}"
47+
gh pr create \
48+
--title "fix(deps): update dependencies with reachable vulnerabilities" \
49+
--body-file /tmp/govulncheck-fix-body.md \
50+
--head="${branch}"

.github/workflows/govulncheck.yml

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1-
name: Nightly govulncheck
1+
name: Govulncheck
22
on:
3+
pull_request:
4+
paths:
5+
- "**/*.go"
6+
- "**/go.mod"
7+
- "**/go.sum"
8+
- "go.work"
9+
- "go.work.sum"
10+
- ".github/workflows/govulncheck.yml"
11+
- ".github/workflows/apps/govulncheck-contribs-v2.sh"
312
workflow_call: # allows to reuse this workflow
413
inputs:
514
ref:
@@ -20,28 +29,67 @@ on:
2029
- cron: '00 00 * * *'
2130
workflow_dispatch:
2231

32+
concurrency:
33+
group: govulncheck-${{ github.event.pull_request.number || github.ref }}
34+
cancel-in-progress: true
35+
2336
permissions:
2437
contents: read
38+
security-events: write # required for SARIF upload to GitHub Code Scanning
2539

2640
jobs:
27-
govulncheck-tests:
41+
# Non-blocking: generates SARIF and uploads to GitHub Code Scanning.
42+
# Replaces Dependabot Security Alerts with reachability-aware findings.
43+
# Uses govulncheck@latest installed directly (golang/govulncheck-action is
44+
# not in the DataDog enterprise action allowlist).
45+
# NOTE: Only core packages are scanned here. Contrib modules (each with
46+
# their own go.mod) are scanned in govulncheck-tests, which blocks merges,
47+
# but contrib vulnerabilities do not appear in GitHub Code Scanning alerts.
48+
govulncheck-analysis:
2849
runs-on: ubuntu-latest
2950
steps:
3051
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3152
with:
53+
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
3254
persist-credentials: false
33-
ref: ${{ inputs.ref || github.ref }}
34-
- name: Setup Go and development tools
35-
uses: ./.github/actions/setup-go
55+
- name: Setup Go
56+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
3657
with:
3758
go-version: stable
38-
tools-dir: ${{ github.workspace }}/_tools
39-
tools-bin: ${{ github.workspace }}/bin
40-
- name: Run govulncheck
59+
cache-dependency-path: '**/go.sum'
60+
- name: Install govulncheck
61+
run: go install golang.org/x/vuln/cmd/govulncheck@latest
62+
- name: Run govulncheck (SARIF)
63+
# -format sarif exits 0 even when vulnerabilities are found, so the
64+
# upload step always runs. The blocking check is in govulncheck-tests.
4165
run: |-
42-
export PATH="${{ github.workspace }}/bin:${PATH}"
43-
govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/...
44-
- name: Run govulncheck-contribs
45-
run: |-
46-
export PATH="${{ github.workspace }}/bin:${PATH}"
47-
./.github/workflows/apps/govulncheck-contribs-v2.sh
66+
govulncheck -format sarif \
67+
./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/... \
68+
> govulncheck.sarif || true
69+
- name: Upload SARIF to GitHub Code Scanning
70+
if: always()
71+
uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
72+
with:
73+
sarif_file: govulncheck.sarif
74+
category: govulncheck
75+
76+
# Blocking: fails the build if any reachable vulnerability is found.
77+
# Scans both core packages and all contrib modules (each with its own go.mod).
78+
govulncheck-tests:
79+
runs-on: ubuntu-latest
80+
steps:
81+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
82+
with:
83+
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
84+
persist-credentials: false
85+
- name: Setup Go
86+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
87+
with:
88+
go-version: stable
89+
cache-dependency-path: '**/go.sum'
90+
- name: Install govulncheck
91+
run: go install golang.org/x/vuln/cmd/govulncheck@latest
92+
- name: Run govulncheck (core)
93+
run: govulncheck ./ddtrace/... ./appsec/... ./profiler/... ./internal/... ./instrumentation/...
94+
- name: Run govulncheck (contrib)
95+
run: ./.github/workflows/apps/govulncheck-contribs-v2.sh

0 commit comments

Comments
 (0)