Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion .github/workflows/govulncheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ name: govulncheck

# Reusable workflow that runs the Go team's vulnerability scanner against
# the caller's module. Detects known CVEs in the import graph (including
# transitive deps) and reports them as workflow annotations.
# transitive deps) and fails when the code calls a vulnerable function.
#
# Findings go to code scanning as SARIF (category govulncheck), with called
# vulnerabilities placed at their call site, so GitHub annotates PRs that add
# or change such a call. Fork PRs can't upload, so they only get the job
# summary.
#
# Pair with the actionlint/zizmor lint workflow for a "static security
# baseline" wave; pair with dependency-review for PR-time gating.
Expand Down Expand Up @@ -35,6 +40,9 @@ permissions:
jobs:
govulncheck:
runs-on: ${{ inputs.runner }}
permissions:
contents: read # baseline for checkout
security-events: write # SARIF upload to GHAS
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand All @@ -44,3 +52,82 @@ jobs:
go-version-file: ${{ inputs.go-version == '' && 'go.mod' || '' }}
go-version-input: ${{ inputs.go-version }}
go-package: ${{ inputs.go-package }}
output-format: sarif
output-file: govulncheck.sarif

# govulncheck puts every finding at go.mod:1, which a PR rarely changes, so
# code scanning never annotates the PR. Move each called finding to its
# call site: the last frame of each trace in the caller's own code.
- name: Point findings at their call sites
run: |
set -euo pipefail
# Fail if the SARIF properties read below move, rather than report nothing.
if ! jq -e '(.runs | type == "array" and length > 0) and all(.runs[]; (.results | type == "array")
and (.tool.driver.rules | type == "array") and ([.tool.driver.rules[].id] as $ids
| all(.tool.driver.rules[]; .fullDescription.text | type == "string")
and all(.results[]; IN(.ruleId; $ids[]) and (.level | type == "string")
and (.message.text | type == "string")
and (.locations[0] | (.message.text | type == "string")
and (.physicalLocation.artifactLocation.uri | type == "string")
and (.physicalLocation.region.startLine | type == "number"))
and (.level != "error" or (.codeFlows | type == "array"))
and all(.codeFlows[]?.threadFlows[].locations[].location.physicalLocation;
(.artifactLocation.uriBaseId | type == "string")
and (.region.startLine | type == "number")))))' govulncheck.sarif > /dev/null; then
echo "::error::Unexpected govulncheck SARIF structure; update this workflow."
exit 1
fi
jq '.runs[].results |= [(. // [])[] | . as $r
| [(.codeFlows // [])[]
| [.threadFlows[].locations[].location
| select(.physicalLocation.artifactLocation.uriBaseId == "%SRCROOT%")]
| last | select(. != null)]
| unique_by(.physicalLocation)
| if length == 0 then $r
else .[] | $r + {locations: [{physicalLocation, message: $r.locations[0].message}]}
end]' govulncheck.sarif > govulncheck.sarif.tmp
mv govulncheck.sarif.tmp govulncheck.sarif

# Called vulnerabilities are SARIF level "error"; imported or required but
# uncalled ones ("warning", "note") stay in code scanning only.
- name: Build report
id: report
env:
BLOB_URL: ${{ github.server_url }}/${{ github.repository }}/blob/${{ github.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
jq -r --arg blob "$BLOB_URL" --arg run "$RUN_URL" '
[.runs[] | (.tool.driver.rules // [] | INDEX(.id)) as $rules
| (.results // [])[] | select(.level == "error")
| {id: .ruleId, rule: $rules[.ruleId], message: .message.text,
path: .locations[0].physicalLocation.artifactLocation.uri,
line: .locations[0].physicalLocation.region.startLine}]
| if length == 0 then "No called vulnerabilities found." else
"Found \(map(.id) | unique | length) called vulnerabilities ([run](\($run))).",
(group_by(.id)[] | "", "### [\(.[0].id)](https://pkg.go.dev/vuln/\(.[0].id))", "",
(.[0].rule.fullDescription.text // .[0].message), "",
(.[] | "- [`\(.path):\(.line)`](\($blob)/\(.path)#L\(.line))"))
end
' govulncheck.sarif > report.md
cat report.md >> "$GITHUB_STEP_SUMMARY"
echo "called=$(jq '[.runs[].results[]? | select(.level == "error")] | length > 0' govulncheck.sarif)" >> "$GITHUB_OUTPUT"

- name: Upload SARIF to GHAS
# Fork PRs get a read-only token, so the upload would fail.
if: >-
github.event.pull_request.head.repo.full_name == github.repository
|| github.event_name != 'pull_request'
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
sarif_file: govulncheck.sarif
category: govulncheck

# SARIF mode exits 0 on findings; fail on called vulnerabilities like the
# default text mode does.
- name: Fail on called vulnerabilities
if: ${{ steps.report.outputs.called == 'true' }}
run: |
echo "::error::govulncheck found called vulnerabilities; see the job summary."
exit 1

10 changes: 10 additions & 0 deletions .github/workflows/lint-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ jobs:
echo "::error::lychee failed (exit ${LYCHEE_EXIT})."
exit 1
fi
# Fail if the report properties read below move, rather than report no
# broken links (and close the issue).
if ! jq -e '(.error_map | type == "object") and (.timeout_map | type == "object")
and all(.error_map[][], .timeout_map[][]; (.url | type == "string")
and (.status.text | type == "string")
and (.span == null or (.span.line | type == "number")))' \
"${RUNNER_TEMP}/lychee.json" > /dev/null; then
echo "::error::Unexpected lychee report structure; update this workflow."
exit 1
fi
# One finding per broken link, with local file URLs made repo-relative.
jq --arg root "file://${GITHUB_WORKSPACE}/" '
[ (.error_map // {} | to_entries[] | .value[] + {file: .key, kind: "error"}),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Reusable GitHub Actions for CI/CD.
- [`.github/workflows/lint-go.yml`](docs/workflows.md#lint-go): Reusable workflow that runs golangci-lint and checks go.mod/go.sum are tidy
- [`.github/workflows/test-unit-go.yml`](docs/workflows.md#test-unit-go): Reusable workflow that runs Go unit tests, optionally reporting coverage to Coveralls
- [`.github/workflows/reuse.yml`](docs/workflows.md#reuse): Reusable workflow that checks REUSE copyright/licensing compliance
- [`.github/workflows/govulncheck.yml`](docs/workflows.md#govulncheck): Reusable workflow that scans Go modules for known CVEs
- [`.github/workflows/govulncheck.yml`](docs/workflows.md#govulncheck): Reusable workflow that scans Go modules for known CVEs (govulncheck), reporting findings to code scanning
- [`.github/workflows/dependency-review.yml`](docs/workflows.md#dependency-review): Reusable workflow that gates PRs introducing CVE-flagged deps
- [`.github/workflows/trivy-image-scan.yml`](docs/workflows.md#trivy-image-scan): Reusable workflow that scans built container images for CVEs
- [`.github/workflows/scorecard.yml`](docs/workflows.md#scorecard): Reusable workflow that runs the OpenSSF Scorecard supply-chain analysis
Expand Down
7 changes: 6 additions & 1 deletion docs/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,9 @@ jobs:
```

### govulncheck
Runs the Go team's vulnerability scanner against the caller's module. Detects known CVEs in the import graph (direct and transitive). Reads the Go version from the caller's `go.mod` by default.
Runs the Go team's vulnerability scanner against the caller's module. Detects known CVEs in the import graph (direct and transitive) and fails when the code calls a vulnerable function. Reads the Go version from the caller's `go.mod` by default.

Findings go to code scanning (category `govulncheck`), with called vulnerabilities placed at their call site, so PRs that add or change such a call get an inline annotation.

**Usage:**
```yaml
Expand All @@ -355,6 +357,9 @@ on:
jobs:
govulncheck:
uses: OpenCHAMI/github-actions/.github/workflows/govulncheck.yml@v4.0
permissions:
contents: read # baseline for checkout
security-events: write # SARIF upload to GHAS
# Optional overrides:
# with:
# go-version: 1.26.7 # default: read from go.mod
Expand Down
Loading