Skip to content

Commit e0bce2b

Browse files
Add OSV-Scanner-based security workflow
Single workflow, single job, three triggers: - pull_request to main: fails on CVSS >= 7 findings only (HIGH/CRITICAL block merges; MED/LOW visible but non-blocking) - cron weekly (Sunday 00:00 UTC): reports ALL findings via email - workflow_dispatch: behaves like cron Mirrors the JDBC driver's security workflow (databricks-jdbc#1460) adapted for Node.js: - Reads package-lock.json natively via OSV-Scanner --lockfile (no separate SBOM tool needed) - Reuses the existing ./.github/actions/setup-jfrog composite action for parity with main.yml (the workflow functionally doesn't need JFrog since OSV reads the lockfile directly, but keeping the composite action preserves the established pattern) - Suppressions in osv-scanner.toml ([[IgnoredVulns]] schema) The workflow is not yet wired into branch protection. Day-one scan against current main surfaces 22 HIGH / 15 MED / 5 LOW (42 total). Many are in dev dependencies (mocha/nyc/eslint chains). The team can either bump the offending deps or add documented [[IgnoredVulns]] entries for dev-only findings that don't reach `dist/`. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
1 parent 6a4a7c4 commit e0bce2b

2 files changed

Lines changed: 268 additions & 0 deletions

File tree

.github/workflows/securityScan.yml

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
name: Security Scan
2+
3+
# Single workflow, single job. Triggered three ways with DIFFERENT
4+
# thresholds:
5+
#
6+
# - pull_request to main: fail the job on any unsuppressed
7+
# CVSS >= 7 finding (HIGH+). MEDIUM/LOW findings show in the step
8+
# summary but don't block merges. Not yet required-to-merge in
9+
# branch protection.
10+
#
11+
# - cron (weekly): report ALL findings regardless of severity. Sends
12+
# an email with the full sorted list and fails the job on any
13+
# finding. The intent is full situational awareness for the team --
14+
# emerging MEDIUM risks should be visible before they cross the PR
15+
# gate, and the weekly is read by humans, not enforced by code.
16+
#
17+
# - workflow_dispatch: behaves like the cron run (full reporting).
18+
#
19+
# Scanner: OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates GHSA,
20+
# NVD, npm advisory DB, RustSec, Go vuln DB, PyPA). Reads
21+
# `package-lock.json` natively -- no separate SBOM tool needed.
22+
#
23+
# NOTE: this scans BOTH runtime and devDependencies (OSV treats
24+
# everything in package-lock.json equally). If a finding is dev-only
25+
# and shouldn't block merges, suppress it via osv-scanner.toml with a
26+
# justification ("dev-only, not shipped in dist/").
27+
#
28+
# Suppressions live in `osv-scanner.toml` as [[IgnoredVulns]] entries
29+
# (CVE-id global; OSV-Scanner v2.3.8 doesn't support per-package CVE
30+
# scoping). Each entry has a justification comment.
31+
32+
on:
33+
pull_request:
34+
branches: [main]
35+
schedule:
36+
- cron: '0 0 * * 0' # Run every Sunday at midnight UTC
37+
workflow_dispatch:
38+
39+
permissions:
40+
id-token: write
41+
contents: read
42+
43+
jobs:
44+
security-scan:
45+
name: Security Scan
46+
runs-on:
47+
group: databricks-protected-runner-group
48+
labels: linux-ubuntu-latest
49+
50+
steps:
51+
- name: Checkout repository
52+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
53+
54+
# JFrog OIDC + npm registry: skipped on fork PRs (no OIDC token
55+
# from GitHub's perspective). OSV-Scanner reads package-lock.json
56+
# directly without fetching from the npm registry, so fork PRs
57+
# still work; we keep setup-jfrog here only for parity with the
58+
# other workflows in this repo. If you remove it later, also
59+
# remove the `id-token: write` permission above.
60+
- name: Setup JFrog
61+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
62+
uses: ./.github/actions/setup-jfrog
63+
64+
- name: Install osv-scanner
65+
run: |
66+
set -euo pipefail
67+
curl -fsSL -o /tmp/osv-scanner \
68+
https://github.com/google/osv-scanner/releases/download/v2.3.8/osv-scanner_linux_amd64
69+
chmod +x /tmp/osv-scanner
70+
/tmp/osv-scanner --version
71+
72+
- name: Run OSV-Scanner
73+
# Drop -e because osv-scanner exits 1 on ANY finding regardless of
74+
# severity. The severity >= 7 filter below is our actual gate, so
75+
# we explicitly tolerate osv-scanner's non-zero exit via `|| true`.
76+
run: |
77+
set -uo pipefail
78+
79+
if [ ! -f package-lock.json ]; then
80+
echo "::error::package-lock.json not found at repo root."
81+
exit 1
82+
fi
83+
84+
/tmp/osv-scanner scan source \
85+
--lockfile=package-lock.json \
86+
--config=osv-scanner.toml \
87+
--format=json \
88+
--output-file=/tmp/osv-out.json \
89+
|| true
90+
91+
if [ ! -s /tmp/osv-out.json ]; then
92+
echo "::error::OSV-Scanner did not produce an output file."
93+
exit 1
94+
fi
95+
96+
# Parse OSV's JSON into job outputs. The terminal steps below
97+
# (PR-fail and email) consume these outputs.
98+
#
99+
# Two thresholds: PR gating uses CVSS >= 7 (high_count) so we don't
100+
# block merges on MEDIUM/LOW noise; the weekly email reports
101+
# everything (total_findings) so the team has full situational
102+
# awareness of emerging risk before it crosses the gate.
103+
- name: Collect findings
104+
id: findings
105+
run: |
106+
set -uo pipefail
107+
108+
# All findings (sorted by severity desc). Anything missing a
109+
# CVSS score sorts to 0 -- visible in the report but not silent.
110+
ALL_FINDINGS=$(jq -c '[
111+
.results[].packages[]? |
112+
.package as $pkg |
113+
.groups[]? |
114+
{pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: (.max_severity // "0")}
115+
] | sort_by(- (.severity | tonumber? // 0))' /tmp/osv-out.json)
116+
TOTAL_FINDINGS=$(echo "$ALL_FINDINGS" | jq 'length')
117+
118+
# High findings (CVSS >= 7). Both counters are logged so a
119+
# mismatch (e.g. 50 total / 0 high) is visible -- protects
120+
# against silent fail-open if OSV ever changes its severity
121+
# format (e.g. emits "HIGH" instead of a number, which
122+
# `tonumber? // 0` would mask).
123+
HIGH_FINDINGS=$(echo "$ALL_FINDINGS" | jq -c '[.[] | select((.severity | tonumber? // 0) >= 7)]')
124+
HIGH_COUNT=$(echo "$HIGH_FINDINGS" | jq 'length')
125+
126+
# Persist the full findings list to a file rather than a job
127+
# output -- GitHub Actions outputs are size-capped at 1 MB and
128+
# the formatted email body can be larger than that for big
129+
# finding lists.
130+
echo "$ALL_FINDINGS" > /tmp/all-findings.json
131+
132+
echo "total_findings=$TOTAL_FINDINGS" >> "$GITHUB_OUTPUT"
133+
echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT"
134+
135+
# Step summary so findings are visible in the GH Actions UI
136+
# without downloading artifacts.
137+
{
138+
echo "## OSV-Scanner Findings"
139+
echo ""
140+
echo "- Total findings (any severity): \`$TOTAL_FINDINGS\`"
141+
echo "- High findings (CVSS >= 7, PR-blocking): \`$HIGH_COUNT\`"
142+
if [ "$TOTAL_FINDINGS" -gt 0 ]; then
143+
echo ""
144+
echo "All findings (sorted by severity desc):"
145+
echo ""
146+
echo "| Severity | Package | IDs |"
147+
echo "|---|---|---|"
148+
echo "$ALL_FINDINGS" | jq -r '.[] | "| \(.severity) | \(.pkg) | \(.ids | join(",")) |"'
149+
fi
150+
} >> "$GITHUB_STEP_SUMMARY"
151+
152+
# Also dump the findings to the job log so they're visible in
153+
# the default "Logs" view, not just the step summary panel.
154+
echo "OSV: $TOTAL_FINDINGS total findings, $HIGH_COUNT at CVSS>=7"
155+
if [ "$TOTAL_FINDINGS" -gt 0 ]; then
156+
echo ""
157+
echo "All findings (sorted by severity desc):"
158+
echo "$ALL_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"'
159+
fi
160+
161+
# --- Terminal: PR event ---
162+
# Fail the job so the PR's check goes red. No email.
163+
# PR gate is CVSS >= 7 only; MEDIUM/LOW findings show up in the
164+
# step summary but don't block merges.
165+
- name: Fail on findings (PR)
166+
if: github.event_name == 'pull_request' && steps.findings.outputs.high_count != '0'
167+
run: |
168+
set -uo pipefail
169+
# List the actual HIGH findings inline so the author sees what
170+
# needs fixing without clicking through to the step summary
171+
# panel or downloading artifacts.
172+
HIGH_FINDINGS=$(jq -c '[.[] | select((.severity | tonumber? // 0) >= 7)]' /tmp/all-findings.json)
173+
174+
echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed CVSS>=7 finding(s) in this PR:"
175+
echo ""
176+
echo "$HIGH_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"'
177+
echo ""
178+
echo "Fix by either:"
179+
echo " 1. Bumping the affected dependency to a patched version, or"
180+
echo " 2. Adding a documented [[IgnoredVulns]] entry to osv-scanner.toml"
181+
echo " with a clear justification for why the CVE doesn't apply to our usage."
182+
echo ""
183+
echo "Full step summary: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
184+
exit 1
185+
186+
# --- Terminal: scheduled/manual event ---
187+
# Weekly reports ALL findings (not just CVSS >= 7) so the team sees
188+
# emerging risk before it crosses the PR gate. PR-time is narrower
189+
# to avoid blocking on MEDIUM/LOW noise; weekly is broader because
190+
# it's read by humans, not enforced.
191+
- name: Compose email body
192+
if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0'
193+
run: |
194+
set -uo pipefail
195+
{
196+
echo "<!DOCTYPE html><html><head><title>SQL Node.js Driver Security Scan Results</title>"
197+
echo "<style>"
198+
echo " body { font-family: -apple-system, sans-serif; }"
199+
echo " table { border-collapse: collapse; margin-top: 1em; }"
200+
echo " th, td { border: 1px solid #ddd; padding: 6px 12px; text-align: left; }"
201+
echo " th { background: #f5f5f5; }"
202+
echo " tr.high { background: #ffe5e5; }"
203+
echo " tr.medium { background: #fff5e5; }"
204+
echo "</style></head><body>"
205+
echo "<h1>Security Vulnerabilities Found</h1>"
206+
echo "<p><b>${{ steps.findings.outputs.total_findings }}</b> total finding(s) on main; <b>${{ steps.findings.outputs.high_count }}</b> are CVSS &gt;= 7 (PR-blocking).</p>"
207+
echo "<p>Full reports are attached to the GitHub Actions run as artifacts: <a href='https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}'>View Artifacts</a></p>"
208+
echo "<table><tr><th>Severity</th><th>Package</th><th>IDs</th></tr>"
209+
jq -r '.[] |
210+
(if (.severity | tonumber? // 0) >= 7 then "high"
211+
elif (.severity | tonumber? // 0) >= 4 then "medium"
212+
else "" end) as $cls |
213+
"<tr class=\"\($cls)\"><td>\(.severity)</td><td>\(.pkg)</td><td>\(.ids | join(", "))</td></tr>"
214+
' /tmp/all-findings.json
215+
echo "</table>"
216+
echo "</body></html>"
217+
} > security-scan-report.html
218+
219+
- name: Send Email
220+
if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0'
221+
uses: dawidd6/action-send-mail@4226df7daafa6fc901a43789c49bf7ab309066e7 # v3
222+
with:
223+
server_address: smtp.gmail.com
224+
server_port: 465
225+
username: ${{ secrets.SMTP_USERNAME }}
226+
password: ${{ secrets.SMTP_PASSWORD }}
227+
subject: OSS SQL Node.js Driver Security Scan - 🚨 Vulnerabilities Found
228+
html_body: file://security-scan-report.html
229+
to: ${{ secrets.EMAIL_RECIPIENTS }}
230+
from: SQL Node.js Driver Security Scanner
231+
content_type: text/html
232+
233+
- name: Fail on findings (scheduled/manual)
234+
if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0'
235+
run: |
236+
echo "::error::${{ steps.findings.outputs.total_findings }} OSV finding(s) on main (${{ steps.findings.outputs.high_count }} at CVSS>=7). Email sent."
237+
exit 1
238+
239+
# Always upload artifacts so triagers can pull the full reports
240+
# without having to rerun anything.
241+
- name: Upload reports
242+
if: always()
243+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
244+
with:
245+
name: security-scan-reports
246+
path: |
247+
/tmp/osv-out.json
248+
security-scan-report.html
249+
if-no-files-found: ignore

osv-scanner.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# OSV-Scanner suppressions for the databricks-sql-nodejs security gate.
2+
#
3+
# Each entry suppresses a CVE that is a documented false positive
4+
# against an artifact we ship, or is a dev-only finding that doesn't
5+
# reach the shipped `dist/`. Every entry has a justification.
6+
#
7+
# Trade-off worth noting: [[IgnoredVulns]] entries are CVE-id global --
8+
# they ignore the CVE across all packages OSV reports it against, not
9+
# just the artifact we have in mind. The alternative
10+
# ([[PackageOverrides]] with `vulnerability.ignore = true`) is
11+
# per-package but blanket-ignores ALL vulnerabilities on that package,
12+
# which is much worse. OSV-Scanner v2.3.8 does NOT support an
13+
# intersection ("this CVE on this package only").
14+
#
15+
# See google.github.io/osv-scanner/configuration/ for the schema.
16+
#
17+
# This file starts empty -- populate iteratively as the first scan run
18+
# surfaces real false positives or dev-only findings worth excluding.
19+
# Do not pre-populate with speculative suppressions.

0 commit comments

Comments
 (0)