feat(ci): add nightly OSV-Scanner vulnerability scan workflow (#3518)

* feat: add nightly OSV-Scanner vulnerability scan workflow

Adds a nightly GitHub Actions workflow that scans go.mod for known
vulnerabilities using OSV-Scanner, uploads SARIF results to the
Security tab, and auto-creates a PR with dependency fixes.

Signed-off-by: Fiona-Waters <[email protected]>

* fix: address review feedback on osv-scanner workflow

- Use sort -V for semver-aware version deduplication instead of
  jq tonumber which mishandles pre-release suffixes
- Snapshot go.mod/go.sum per iteration so a failed fix only reverts
  itself, not previously successful updates

Signed-off-by: Fiona-Waters <[email protected]>

* fix(ci): Address review comments for osv-scanner workflow

- Add set -euo pipefail to catch pipeline and unset variable errors
- Remove 2>/dev/null from jq call so schema drift is a hard failure
- Normalize OSV fix versions with sub("^v"; "") to prevent double v prefix
- Use ::warning:: annotations so skipped fixes surface in Actions UI
- Fix go list -m fallback to record fix version instead of silently skipping
- Remove redundant jq empty pre-check

Signed-off-by: Fiona-Waters <[email protected]>

---------

Signed-off-by: Fiona-Waters <[email protected]>

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
Fiona Waters
2026-05-19 13:30:04 +00:00
committed by GitHub
co-authored by Hanzo Dev
parent dc8e6782d1
commit 8704ba0bd4
+211
View File
@@ -0,0 +1,211 @@
name: OSV-Scanner Vulnerability Scan
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
osv-scan:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Install OSV-Scanner
# Bump cadence: check https://github.com/google/osv-scanner/releases monthly
# Dependabot cannot bump curl-installed binaries — update version + checksum manually
run: |
OSV_VERSION="2.3.8"
EXPECTED_SHA="bc98e15319ed0d515e3f9235287ba53cdc5535d576d24fd573978ecfe9ab92dc"
curl -fsSL "https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/osv-scanner_linux_amd64" -o osv-scanner
ACTUAL_SHA=$(sha256sum osv-scanner | awk '{print $1}')
if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then
echo "::error::OSV-Scanner checksum mismatch! Expected ${EXPECTED_SHA}, got ${ACTUAL_SHA}"
exit 1
fi
chmod +x osv-scanner
sudo mv osv-scanner /usr/local/bin/
- name: Run OSV-Scanner (SARIF for Security Tab)
# continue-on-error so a SARIF failure doesn't block the JSON scan + auto-fix below
continue-on-error: true
run: |
rc=0
osv-scanner scan \
--format sarif \
--output osv-results.sarif \
-L go.mod || rc=$?
# Exit code 1 = vulns found (expected), 0 = clean. Any other code is a real failure.
if [ "$rc" -ne 0 ] && [ "$rc" -ne 1 ]; then
echo "::error::OSV-Scanner failed with unexpected exit code $rc"
exit 1
fi
- name: Upload SARIF to GitHub Security tab
if: always() && hashFiles('osv-results.sarif') != ''
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: osv-results.sarif
category: kubeflow-trainer-osv-scanner
- name: Run OSV-Scanner (JSON for auto-fix)
run: |
rc=0
osv-scanner scan \
--format json \
--output osv-results.json \
-L go.mod || rc=$?
if [ "$rc" -ne 0 ] && [ "$rc" -ne 1 ]; then
echo "::error::OSV-Scanner failed with unexpected exit code $rc"
exit 1
fi
- name: Process vulnerabilities and apply fixes
id: fixer
run: |
set -euo pipefail
if [ ! -f osv-results.json ] || [ ! -s osv-results.json ]; then
echo "No scan results found."
echo "updates_found=false" >> $GITHUB_OUTPUT
exit 0
fi
# Extract fixable vulnerabilities: package name, current version, fix version, advisory ID, URL
# Use group_by + last to deduplicate: keep the highest fix version per package+advisory pair
FIX_DATA=$(jq -r '
[
.results[]?.packages[]? |
.package as $pkg |
.vulnerabilities[]? |
. as $vuln |
.affected[]? |
select(.package.name == $pkg.name) |
.ranges[]? |
.events[] |
select(.fixed != null) |
{
pkg: $pkg.name,
ver: $pkg.version,
fix: (.fixed | sub("^v"; "")),
id: $vuln.id,
url: ($vuln.references[0].url // "https://osv.dev/vulnerability/\($vuln.id)")
}
] |
.[] |
"\(.pkg)|\(.ver)|\(.fix)|\(.id)|\(.url)"
' osv-results.json \
| sort -t'|' -k1,1 -k4,4 -k3,3Vr \
| awk -F'|' '!seen[$1,$4]++' \
)
if [ -z "$FIX_DATA" ]; then
echo "No fixable vulnerabilities found."
echo "updates_found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "Fixable vulnerabilities found:"
echo "$FIX_DATA"
echo ""
rm -f applied_fixes.txt
while IFS='|' read -r pkg current_ver fix_ver advisory_id advisory_url; do
[ -z "$pkg" ] && continue
echo ""
echo "=== Processing: $pkg $current_ver -> $fix_ver ($advisory_id) ==="
# Snapshot go.mod/go.sum so a failure only reverts this iteration, not prior fixes
cp go.mod go.mod.bak
cp go.sum go.sum.bak
echo "Attempting: go get ${pkg}@v${fix_ver}"
GET_RC=0
go get "${pkg}@v${fix_ver}" 2>&1 || GET_RC=$?
if [ "$GET_RC" -ne 0 ]; then
echo "::warning::go get failed for $pkg (exit code $GET_RC), skipping"
mv go.mod.bak go.mod
mv go.sum.bak go.sum
continue
fi
TIDY_RC=0
go mod tidy 2>&1 || TIDY_RC=$?
if [ "$TIDY_RC" -ne 0 ]; then
echo "::warning::go mod tidy failed after updating $pkg (exit code $TIDY_RC), skipping"
mv go.mod.bak go.mod
mv go.sum.bak go.sum
continue
fi
rm -f go.mod.bak go.sum.bak
# Verify the installed version meets the fix
INSTALLED_VER=$(go list -m -f '{{.Version}}' "$pkg" | sed 's/^v//' || true)
if [ -z "$INSTALLED_VER" ]; then
echo "::warning::Could not determine installed version for $pkg after upgrade, recording fix version"
INSTALLED_VER="$fix_ver"
fi
echo "Installed version: v${INSTALLED_VER}"
printf '%s\n' "- ${pkg}@v${INSTALLED_VER} | [$advisory_id]($advisory_url)" >> applied_fixes.txt
done <<< "$FIX_DATA"
if [ ! -s applied_fixes.txt ]; then
echo "No fixes were applied."
echo "updates_found=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "updates_found=true" >> $GITHUB_OUTPUT
{
echo "fix_details<<EOF"
cat applied_fixes.txt
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Create Pull Request
if: steps.fixer.outputs.updates_found == 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "fix: nightly automated dependency update for security vulnerabilities"
title: "fix: nightly security dependency updates"
add-paths: |
go.mod
go.sum
body: |
## Security Update
This is an automated PR triggered by the nightly OSV-Scanner security scan.
The following dependencies were updated to resolve known vulnerabilities:
| Package & Version | Advisory Link |
| :--- | :--- |
${{ steps.fixer.outputs.fix_details }}
**Verification:** Updated via `go get <pkg>@v<version>` and `go mod tidy`.
> **Note:** This PR was created with `GITHUB_TOKEN`, which does not trigger `pull_request` workflows.
> CI checks will not run automatically. Push an empty commit (`git commit --allow-empty -m "trigger CI"`)
> to kick off CI before merging.
>
> **TODO:** Migrate to a GitHub App token via `actions/create-github-app-token` to trigger CI automatically.
branch: security-nightly-updates
delete-branch: true
labels: |
"area/security"