Skip to content
Draft
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
238 changes: 235 additions & 3 deletions .github/workflows/govulncheck-cron-schedule.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
# allow manually triggering workflow
workflow_dispatch:

# deny-all default; the job below grants only the scopes it needs
# deny-all default; each job below grants only the scopes it needs
permissions: {}

jobs:
Expand All @@ -20,6 +20,7 @@ jobs:
name: Run govulncheck
permissions:
contents: read
pull-requests: read # to check for an already-open fix PR

strategy:
fail-fast: false
Expand All @@ -46,10 +47,10 @@ jobs:
repo-checkout: false # will auto-checkout the default branch if left on true
output-format: 'text' # other values will always result in successful completion of the action, we need it fail on vulnerabilities

- name: notify slack
- name: notify slack (master)
# this uses our own 'Github notifications' app in slack
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
if: ${{ failure() }} # only run this steps if one of the previous steps has failed
if: ${{ failure() && matrix.branches == 'master' }}
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL_NUTS_CORE_TEAM }} # webhook is linked to a specific slack channel
webhook-type: incoming-webhook
Expand Down Expand Up @@ -79,3 +80,234 @@ jobs:
}
]
}

# V5.4/V6.2 don't get proactive Dependabot bumps (#4545), so a finding here
# needs a fix PR, not just an alert. The text-format run above only tells us
# *that* something is wrong; re-run in JSON to get which module/version to
# bump. JSON format never fails the step, so this only runs after the text
# run already failed.
- name: Get vulnerability details (JSON)
if: ${{ failure() && matrix.branches != 'master' }}
uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1
with:
go-version-input: ''
go-version-file: 'go.mod'
go-package: ./...
repo-checkout: false
output-format: 'json'
output-file: 'govulncheck.json'

# Reduce the JSON report to the findings that actually need a fix PR: one
# per (osv, module) - govulncheck can report the same vulnerability via
# multiple call traces - skipping stdlib/toolchain findings (need a Go
# version bump, not `go get`), findings with no fixed version yet, and
# anything that already has an open PR. `open_fix_prs` (below) can't tell
# any of this apart from a "nothing to do" run, so it's filtered out here.
- name: Extract actionable findings
id: findings
if: ${{ failure() && matrix.branches != 'master' }}
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ matrix.branches }}
REPO: ${{ github.repository }}
shell: bash
run: |
set -uo pipefail

actionable="[]"
findings=$(jq -c 'select(.finding != null) | .finding | select(.trace[0].module != null) | {osv, fixed_version, module: .trace[0].module}' govulncheck.json \
| jq -s 'unique_by(.osv, .module)' | jq -c '.[]')

while IFS= read -r finding; do
[ -z "$finding" ] && continue
osv=$(echo "$finding" | jq -r '.osv')
module=$(echo "$finding" | jq -r '.module')
fixed=$(echo "$finding" | jq -r '.fixed_version')

if [ -z "$fixed" ] || [ "$fixed" = "null" ]; then
echo "::warning::${osv} (${module}) on ${BRANCH}: no fixed version published yet, skipping"
continue
fi
# stdlib/toolchain findings need a Go version bump, not `go get`.
if [ "$module" = "stdlib" ] || [ "$module" = "toolchain" ]; then
echo "::warning::${osv} on ${BRANCH}: affects the Go toolchain/stdlib, needs a manual Go version bump"
continue
fi

# keyed on (branch, advisory ID): the same CVE can affect both release
# branches (one PR each, not deduped against each other), and a module
# can have more than one open advisory at once.
existing=$(gh pr list --repo "$REPO" --base "$BRANCH" --state open --search "\"${osv}\" in:title" --json url --jq '.[0].url // empty')
if [ -n "$existing" ]; then
echo "PR already open for ${osv} on ${BRANCH}: ${existing}"
continue
fi

actionable=$(echo "$actionable" | jq --arg branch "$BRANCH" --arg osv "$osv" --arg module "$module" --arg fixed "$fixed" \
'. + [{branch: $branch, osv: $osv, module: $module, fixed: $fixed}]')
done <<< "$findings"

count=$(echo "$actionable" | jq 'length')
echo "count=${count}" >> "$GITHUB_OUTPUT"
if [ "$count" -gt 0 ]; then
echo "$actionable" | jq -c . > findings.json
fi

- name: Upload actionable findings
if: ${{ failure() && matrix.branches != 'master' && steps.findings.outputs.count > 0 }}
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: findings-${{ matrix.branches }}
path: findings.json
retention-days: 1

# Nothing left to open a fix PR for (already open, no fixed version yet, or
# a toolchain/stdlib finding) - still alert, since the branch is genuinely
# vulnerable and this run won't produce a PR for it.
- name: notify slack (release branch, no auto-fix)
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
if: ${{ failure() && matrix.branches != 'master' && steps.findings.outputs.count == 0 }}
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL_NUTS_CORE_TEAM }}
webhook-type: incoming-webhook
payload: |
{
"text": "GitHub Action failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Vulnerabilities detected on ${{ matrix.branches }}* :rotating_light:\n govulncheck found vulnerabilities but none could be auto-fixed (already an open PR, no fixed version yet, or a toolchain/stdlib finding). See workflow for more info."
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": ":github: Failed workflow"
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}

# Findings are collected per-branch by the matrixed job above (one artifact
# each). A `uses:` action step can't be looped over a variable-length list
# within a single job, so the fix itself happens in a *dynamic* matrix
# (open_fix_prs, below) - this job's only purpose is merging the per-branch
# artifacts into the single JSON array that matrix needs.
prepare_fix_matrix:
runs-on: ubuntu-latest
name: Merge findings into a fix matrix
needs: govulncheck_job
if: ${{ needs.govulncheck_job.result == 'failure' }}
permissions: {}
outputs:
matrix: ${{ steps.merge.outputs.matrix }}
has_findings: ${{ steps.merge.outputs.has_findings }}
steps:
- name: Download findings
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
pattern: findings-*
continue-on-error: true # no artifacts at all if nothing was actionable

- name: Merge into one matrix
id: merge
shell: bash
run: |
set -uo pipefail
shopt -s nullglob
files=(findings-*/findings.json)
if [ "${#files[@]}" -eq 0 ]; then
echo "matrix=[]" >> "$GITHUB_OUTPUT"
echo "has_findings=false" >> "$GITHUB_OUTPUT"
exit 0
fi
merged=$(jq -s 'add' "${files[@]}")
echo "matrix=$(echo "$merged" | jq -c .)" >> "$GITHUB_OUTPUT"
if [ "$(echo "$merged" | jq 'length')" -gt 0 ]; then
echo "has_findings=true" >> "$GITHUB_OUTPUT"
else
echo "has_findings=false" >> "$GITHUB_OUTPUT"
fi

# Every branch in this repo requires signed commits (org-wide ruleset), and
# the runner has no signing key for a bot identity. create-pull-request's
# `sign-commits: true` (with the default GITHUB_TOKEN) signs the commit as
# github-actions[bot] via GitHub's API instead of a local `git commit` +
# `git push`, so no key management is needed.
open_fix_prs:
runs-on: ubuntu-latest
name: 'Fix ${{ matrix.finding.osv }} on ${{ matrix.finding.branch }}'
needs: prepare_fix_matrix
if: ${{ needs.prepare_fix_matrix.outputs.has_findings == 'true' }}
permissions:
contents: write
pull-requests: write
strategy:
fail-fast: false
matrix:
finding: ${{ fromJson(needs.prepare_fix_matrix.outputs.matrix) }}
steps:
- name: Checkout branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ matrix.finding.branch }}
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: 'go.mod'

- name: Bump the vulnerable module
run: |
go get "${{ matrix.finding.module }}@${{ matrix.finding.fixed }}"
go mod tidy

- name: Open fix PR
id: cpr
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8
with:
token: ${{ github.token }}
sign-commits: true
base: ${{ matrix.finding.branch }}
branch: security/${{ matrix.finding.branch }}-${{ matrix.finding.osv }}
commit-message: |
fix(${{ matrix.finding.module }}): bump to ${{ matrix.finding.fixed }} (${{ matrix.finding.osv }})

govulncheck flagged this vulnerability on ${{ matrix.finding.branch }}. See https://pkg.go.dev/vuln/${{ matrix.finding.osv }}
title: '${{ matrix.finding.branch }}: fix ${{ matrix.finding.osv }} (${{ matrix.finding.module }})'
body: |
govulncheck flagged `${{ matrix.finding.module }}` (${{ matrix.finding.osv }}) as vulnerable on `${{ matrix.finding.branch }}`.

Bumps to `${{ matrix.finding.module }}@${{ matrix.finding.fixed }}`.

See https://pkg.go.dev/vuln/${{ matrix.finding.osv }}

- name: notify slack (fix PR opened)
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
if: ${{ steps.cpr.outputs.pull-request-operation == 'created' }}
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL_NUTS_CORE_TEAM }}
webhook-type: incoming-webhook
payload: |
{
"text": "GitHub Action opened a security fix PR",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Fix PR opened for ${{ matrix.finding.branch }}* :rotating_light:\n${{ matrix.finding.osv }} (`${{ matrix.finding.module }}`) - <${{ steps.cpr.outputs.pull-request-url }}|review the PR>"
}
}
]
}
Loading