Add OSV-Scanner-based security workflow#388
Conversation
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>
|
▎ .max_severity // "0" only substitutes when max_severity is null/false, NOT when it is an empty string. OSV-Scanner emits max_severity as "" for advisories lacking a CVSS vector (common for GHSA-only advisories). Such a finding gets severity="" -> tonumber? // 0 -> 0, so a genuinely HIGH vuln never trips the CVSS>=7 PR gate (fail-open). Validator: confirm OSV v2.3.8 emits max_severity="" for scoreless advisories and that the JDBC bouncycastle-style GHSA findings carry no numeric max_severity. ▎ Collect findings runs set -uo pipefail (no -e). If /tmp/osv-out.json is malformed JSON, the ALL_FINDINGS jq exits 5, ALL_FINDINGS='', then TOTAL_FINDINGS='' and HIGH_COUNT='' are written to GITHUB_OUTPUT as empty strings. Confirmed locally: [ "$TOTAL_FINDINGS" -gt 0 ] on line 142 errors with 'integer expression expected'. Downstream high_count != '0' becomes '' != '0' (true). Validator: trace empty-output propagation to the gate steps. 📄 .github/workflows/securityScan.yml
▎ || true swallows every osv-scanner failure, and the only backstop ([ ! -s ]) misses a partial write. Line 89's || true was intended to tolerate osv-scanner's exit-1-on-any-finding, but it also masks genuine failures: a network error reaching OSV.dev, a malformed osv-scanner.toml, a wrong-arch/corrupt binary, or a crash mid-write to --output-file. The sole thing distinguishing those from a clean scan is the line-91 [ ! -s /tmp/osv-out.json ] guard, which only rejects a zero-byte file. A truncated-but-non-empty output file (reproduced: a 140-byte partial JSON) passes the guard, then the Collect-findings jq (line 110) fails to parse it and empty counts propagate to the gates -- an errored scan becomes indistinguishable from a clean one (fail-open on the scheduled path) or emits misleading counts. (Note: the sibling concern that scan source --lockfile --config --format=json --output-file is invalid v2.3.8 CLI is not a bug -- that syntax is valid and was verified against the v2.3.8 binary.) |
Brings the Node OSV-Scanner workflow in line with the merged Go driver
workflow (databricks-sql-go). Two fail-open holes closed + delivery model
switched from per-repo email to artifact+cross-repo collator.
Fail-closed hardening (identical logic to Go, verified against synthetic
OSV JSON):
- Scanner errors no longer pass the gate: replace the blanket `|| true`
with `|| scan_rc=$?`, tolerate only exit 0 (clean) / 1 (findings), and
reject partial/corrupt output via `jq -e 'has("results") and
(.results|type=="array")'`. Any other exit or unparseable JSON fails
the job closed instead of reporting zero findings.
- Empty `max_severity` no longer scores 0: resolve severity as group
max_severity -> else max CVSS across the group's vulnerabilities'
.severities[].score -> else the "UNKNOWN" sentinel. jq uses
`try (x|tonumber) catch null` (not `tonumber?`, which yields EMPTY and
drops the row inside a binding).
- UNKNOWN is BLOCKING. Unlike Go (whose scoreless findings are mostly
stdlib advisories delivered via GOTOOLCHAIN, hence report-only), npm
advisories reliably carry CVSS, so a scoreless finding means a
GHSA-only / MAL-* malware advisory on a real dep -> fail closed. No
is_stdlib carve-out. Gate in both Collect and Fail-on-PR steps is
`(CVSS>=7) OR UNKNOWN`; added an `unknown_count` output.
- Integer-count guard: fail closed if total/high counts don't parse to
integers (a failed jq must never read as "clean").
Delivery model:
- Drop the SMTP steps (Compose email body + dawidd6/action-send-mail).
Weekly/manual runs now fail closed and upload osv-out.json +
all-findings.json; a separate cross-repo action collates artifacts
across all driver repos and sends one digest. Removes the need for
SMTP_USERNAME / SMTP_PASSWORD / EMAIL_RECIPIENTS secrets.
osv-scanner.toml: document the suppression convention -- every
[[IgnoredVulns]] must set an ~6-month `ignoreUntil` so suppressions
re-surface for re-review rather than lingering silently.
Co-authored-by: Isaac
Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
The workflow was ported from the Go repo, whose prettier config differs; Node's prettier normalizes the cron entry's comment spacing. No logic change. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
Summary
.github/workflows/securityScan.yml— single workflow, single job, three triggers (PR / weekly cron / manual). PR runs fail on CVSS ≥ 7 or any unscored (UNKNOWN) finding; weekly/manual runs report all findings, fail closed, and upload the raw scan output as an artifact.osv-scanner.toml— starts empty; documents the suppression convention (every[[IgnoredVulns]]entry sets a ~6-monthignoreUntilso it re-surfaces for re-review)../.github/actions/setup-jfrogcomposite action — no duplicate OIDC-token logic; gated to skip on fork PRs.Mirrors the merged Go driver workflow (databricks-sql-go), which itself descends from the JDBC driver's workflow (databricks-jdbc#1460), adapted for Node.js: reads
package-lock.jsonnatively via OSV-Scanner (no separate SBOM tool needed).Fail-closed hardening (ported from the Go driver)
The naive version of this gate had two fail-open holes; both are closed here (logic is byte-identical to Go, verified against synthetic OSV JSON):
|| true+ zero-byte guard let an errored-but-non-empty output sail through as "clean." Now we capture the exit code, tolerate only0(clean) /1(findings), reject anything else, and validate the output is well-formed JSON with a.resultsarray (jq -e). Any scanner failure or partial write → job fails closed.max_severityno longer scores 0. OSV emitsmax_severity: ""for advisory groups without a CVSS vector (GHSA-only,MAL-*malware)."" | tonumber? // 0silently scored those 0 → a real HIGH could go green. Severity now resolves groupmax_severity→ else max CVSS across the group's vulnerabilities → else theUNKNOWNsentinel, never 0.UNKNOWN is blocking. Unlike the Go driver (whose scoreless findings are mostly Go-stdlib advisories delivered via
GOTOOLCHAINand therefore report-only), npm advisories reliably carry a CVSS score — so a scoreless finding here means a GHSA-only or malware advisory on a real dependency and must fail closed. Nois_stdlibcarve-out. The PR gate is(CVSS ≥ 7) OR UNKNOWN.Delivery: artifact + cross-repo collator (no per-repo email)
Weekly/manual runs no longer send email directly. They fail closed and upload
osv-out.json+all-findings.json; a separate cross-repo action collates findings across all driver repos and sends a single digest. This removes theSMTP_USERNAME/SMTP_PASSWORD/EMAIL_RECIPIENTSsecret requirement.Day-one results
The workflow is not yet wired into branch protection, so its first PR-time runs are advisory. OSV scans both runtime and devDependencies (it treats everything in
package-lock.jsonequally), so many day-one findings are dev-only (the mocha/nyc/eslint toolchain) and don't reachdist/. Triage each into either:npm updateaway), or[[IgnoredVulns]]entry withignoreUntil, justifying why it's dev-only / unreachable.The security-bump PRs (#390 → 2.0.0, and the merged 1.17.0) already drive the lockfile toward 0 OSV findings.
Test plan
workflow_dispatchafter merge exercises the weekly path + artifact uploadThis pull request was AI-assisted by Isaac.