From e0bce2b65206a5cfb137e9a23a3e22aa318e69ea Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Thu, 21 May 2026 09:34:05 +0000 Subject: [PATCH 1/3] 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 --- .github/workflows/securityScan.yml | 249 +++++++++++++++++++++++++++++ osv-scanner.toml | 19 +++ 2 files changed, 268 insertions(+) create mode 100644 .github/workflows/securityScan.yml create mode 100644 osv-scanner.toml diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml new file mode 100644 index 00000000..6aa9b751 --- /dev/null +++ b/.github/workflows/securityScan.yml @@ -0,0 +1,249 @@ +name: Security Scan + +# Single workflow, single job. Triggered three ways with DIFFERENT +# thresholds: +# +# - pull_request to main: fail the job on any unsuppressed +# CVSS >= 7 finding (HIGH+). MEDIUM/LOW findings show in the step +# summary but don't block merges. Not yet required-to-merge in +# branch protection. +# +# - cron (weekly): report ALL findings regardless of severity. Sends +# an email with the full sorted list and fails the job on any +# finding. The intent is full situational awareness for the team -- +# emerging MEDIUM risks should be visible before they cross the PR +# gate, and the weekly is read by humans, not enforced by code. +# +# - workflow_dispatch: behaves like the cron run (full reporting). +# +# Scanner: OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates GHSA, +# NVD, npm advisory DB, RustSec, Go vuln DB, PyPA). Reads +# `package-lock.json` natively -- no separate SBOM tool needed. +# +# NOTE: this scans BOTH runtime and devDependencies (OSV treats +# everything in package-lock.json equally). If a finding is dev-only +# and shouldn't block merges, suppress it via osv-scanner.toml with a +# justification ("dev-only, not shipped in dist/"). +# +# Suppressions live in `osv-scanner.toml` as [[IgnoredVulns]] entries +# (CVE-id global; OSV-Scanner v2.3.8 doesn't support per-package CVE +# scoping). Each entry has a justification comment. + +on: + pull_request: + branches: [main] + schedule: + - cron: '0 0 * * 0' # Run every Sunday at midnight UTC + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + security-scan: + name: Security Scan + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + # JFrog OIDC + npm registry: skipped on fork PRs (no OIDC token + # from GitHub's perspective). OSV-Scanner reads package-lock.json + # directly without fetching from the npm registry, so fork PRs + # still work; we keep setup-jfrog here only for parity with the + # other workflows in this repo. If you remove it later, also + # remove the `id-token: write` permission above. + - name: Setup JFrog + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: ./.github/actions/setup-jfrog + + - name: Install osv-scanner + run: | + set -euo pipefail + curl -fsSL -o /tmp/osv-scanner \ + https://github.com/google/osv-scanner/releases/download/v2.3.8/osv-scanner_linux_amd64 + chmod +x /tmp/osv-scanner + /tmp/osv-scanner --version + + - name: Run OSV-Scanner + # Drop -e because osv-scanner exits 1 on ANY finding regardless of + # severity. The severity >= 7 filter below is our actual gate, so + # we explicitly tolerate osv-scanner's non-zero exit via `|| true`. + run: | + set -uo pipefail + + if [ ! -f package-lock.json ]; then + echo "::error::package-lock.json not found at repo root." + exit 1 + fi + + /tmp/osv-scanner scan source \ + --lockfile=package-lock.json \ + --config=osv-scanner.toml \ + --format=json \ + --output-file=/tmp/osv-out.json \ + || true + + if [ ! -s /tmp/osv-out.json ]; then + echo "::error::OSV-Scanner did not produce an output file." + exit 1 + fi + + # Parse OSV's JSON into job outputs. The terminal steps below + # (PR-fail and email) consume these outputs. + # + # Two thresholds: PR gating uses CVSS >= 7 (high_count) so we don't + # block merges on MEDIUM/LOW noise; the weekly email reports + # everything (total_findings) so the team has full situational + # awareness of emerging risk before it crosses the gate. + - name: Collect findings + id: findings + run: | + set -uo pipefail + + # All findings (sorted by severity desc). Anything missing a + # CVSS score sorts to 0 -- visible in the report but not silent. + ALL_FINDINGS=$(jq -c '[ + .results[].packages[]? | + .package as $pkg | + .groups[]? | + {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: (.max_severity // "0")} + ] | sort_by(- (.severity | tonumber? // 0))' /tmp/osv-out.json) + TOTAL_FINDINGS=$(echo "$ALL_FINDINGS" | jq 'length') + + # High findings (CVSS >= 7). Both counters are logged so a + # mismatch (e.g. 50 total / 0 high) is visible -- protects + # against silent fail-open if OSV ever changes its severity + # format (e.g. emits "HIGH" instead of a number, which + # `tonumber? // 0` would mask). + HIGH_FINDINGS=$(echo "$ALL_FINDINGS" | jq -c '[.[] | select((.severity | tonumber? // 0) >= 7)]') + HIGH_COUNT=$(echo "$HIGH_FINDINGS" | jq 'length') + + # Persist the full findings list to a file rather than a job + # output -- GitHub Actions outputs are size-capped at 1 MB and + # the formatted email body can be larger than that for big + # finding lists. + echo "$ALL_FINDINGS" > /tmp/all-findings.json + + echo "total_findings=$TOTAL_FINDINGS" >> "$GITHUB_OUTPUT" + echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT" + + # Step summary so findings are visible in the GH Actions UI + # without downloading artifacts. + { + echo "## OSV-Scanner Findings" + echo "" + echo "- Total findings (any severity): \`$TOTAL_FINDINGS\`" + echo "- High findings (CVSS >= 7, PR-blocking): \`$HIGH_COUNT\`" + if [ "$TOTAL_FINDINGS" -gt 0 ]; then + echo "" + echo "All findings (sorted by severity desc):" + echo "" + echo "| Severity | Package | IDs |" + echo "|---|---|---|" + echo "$ALL_FINDINGS" | jq -r '.[] | "| \(.severity) | \(.pkg) | \(.ids | join(",")) |"' + fi + } >> "$GITHUB_STEP_SUMMARY" + + # Also dump the findings to the job log so they're visible in + # the default "Logs" view, not just the step summary panel. + echo "OSV: $TOTAL_FINDINGS total findings, $HIGH_COUNT at CVSS>=7" + if [ "$TOTAL_FINDINGS" -gt 0 ]; then + echo "" + echo "All findings (sorted by severity desc):" + echo "$ALL_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"' + fi + + # --- Terminal: PR event --- + # Fail the job so the PR's check goes red. No email. + # PR gate is CVSS >= 7 only; MEDIUM/LOW findings show up in the + # step summary but don't block merges. + - name: Fail on findings (PR) + if: github.event_name == 'pull_request' && steps.findings.outputs.high_count != '0' + run: | + set -uo pipefail + # List the actual HIGH findings inline so the author sees what + # needs fixing without clicking through to the step summary + # panel or downloading artifacts. + HIGH_FINDINGS=$(jq -c '[.[] | select((.severity | tonumber? // 0) >= 7)]' /tmp/all-findings.json) + + echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed CVSS>=7 finding(s) in this PR:" + echo "" + echo "$HIGH_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"' + echo "" + echo "Fix by either:" + echo " 1. Bumping the affected dependency to a patched version, or" + echo " 2. Adding a documented [[IgnoredVulns]] entry to osv-scanner.toml" + echo " with a clear justification for why the CVE doesn't apply to our usage." + echo "" + echo "Full step summary: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + exit 1 + + # --- Terminal: scheduled/manual event --- + # Weekly reports ALL findings (not just CVSS >= 7) so the team sees + # emerging risk before it crosses the PR gate. PR-time is narrower + # to avoid blocking on MEDIUM/LOW noise; weekly is broader because + # it's read by humans, not enforced. + - name: Compose email body + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' + run: | + set -uo pipefail + { + echo "SQL Node.js Driver Security Scan Results" + echo "" + echo "

Security Vulnerabilities Found

" + echo "

${{ steps.findings.outputs.total_findings }} total finding(s) on main; ${{ steps.findings.outputs.high_count }} are CVSS >= 7 (PR-blocking).

" + echo "

Full reports are attached to the GitHub Actions run as artifacts: View Artifacts

" + echo "" + jq -r '.[] | + (if (.severity | tonumber? // 0) >= 7 then "high" + elif (.severity | tonumber? // 0) >= 4 then "medium" + else "" end) as $cls | + "" + ' /tmp/all-findings.json + echo "
SeverityPackageIDs
\(.severity)\(.pkg)\(.ids | join(", "))
" + echo "" + } > security-scan-report.html + + - name: Send Email + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' + uses: dawidd6/action-send-mail@4226df7daafa6fc901a43789c49bf7ab309066e7 # v3 + with: + server_address: smtp.gmail.com + server_port: 465 + username: ${{ secrets.SMTP_USERNAME }} + password: ${{ secrets.SMTP_PASSWORD }} + subject: OSS SQL Node.js Driver Security Scan - 🚨 Vulnerabilities Found + html_body: file://security-scan-report.html + to: ${{ secrets.EMAIL_RECIPIENTS }} + from: SQL Node.js Driver Security Scanner + content_type: text/html + + - name: Fail on findings (scheduled/manual) + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' + run: | + echo "::error::${{ steps.findings.outputs.total_findings }} OSV finding(s) on main (${{ steps.findings.outputs.high_count }} at CVSS>=7). Email sent." + exit 1 + + # Always upload artifacts so triagers can pull the full reports + # without having to rerun anything. + - name: Upload reports + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: security-scan-reports + path: | + /tmp/osv-out.json + security-scan-report.html + if-no-files-found: ignore diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 00000000..c82f8de4 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,19 @@ +# OSV-Scanner suppressions for the databricks-sql-nodejs security gate. +# +# Each entry suppresses a CVE that is a documented false positive +# against an artifact we ship, or is a dev-only finding that doesn't +# reach the shipped `dist/`. Every entry has a justification. +# +# Trade-off worth noting: [[IgnoredVulns]] entries are CVE-id global -- +# they ignore the CVE across all packages OSV reports it against, not +# just the artifact we have in mind. The alternative +# ([[PackageOverrides]] with `vulnerability.ignore = true`) is +# per-package but blanket-ignores ALL vulnerabilities on that package, +# which is much worse. OSV-Scanner v2.3.8 does NOT support an +# intersection ("this CVE on this package only"). +# +# See google.github.io/osv-scanner/configuration/ for the schema. +# +# This file starts empty -- populate iteratively as the first scan run +# surfaces real false positives or dev-only findings worth excluding. +# Do not pre-populate with speculative suppressions. From 684fea027eee95ff725b210c32980f34a75ef71c Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Tue, 14 Jul 2026 06:45:02 +0000 Subject: [PATCH 2/3] security-scan: port Go driver's fail-closed hardening + drop SMTP email 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 --- .github/workflows/securityScan.yml | 227 +++++++++++++++++------------ osv-scanner.toml | 13 ++ 2 files changed, 150 insertions(+), 90 deletions(-) diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml index 6aa9b751..ca02dbe6 100644 --- a/.github/workflows/securityScan.yml +++ b/.github/workflows/securityScan.yml @@ -4,15 +4,16 @@ name: Security Scan # thresholds: # # - pull_request to main: fail the job on any unsuppressed -# CVSS >= 7 finding (HIGH+). MEDIUM/LOW findings show in the step -# summary but don't block merges. Not yet required-to-merge in -# branch protection. +# CVSS >= 7 finding (HIGH+), or any unscored finding. MEDIUM/LOW +# findings show in the step summary but don't block merges. Not yet +# required-to-merge in branch protection. # -# - cron (weekly): report ALL findings regardless of severity. Sends -# an email with the full sorted list and fails the job on any -# finding. The intent is full situational awareness for the team -- -# emerging MEDIUM risks should be visible before they cross the PR -# gate, and the weekly is read by humans, not enforced by code. +# - cron (weekly): report ALL findings regardless of severity, fail the +# job on any finding, and upload the raw scan output as an artifact. +# The intent is full situational awareness -- emerging MEDIUM risks +# should be visible before they cross the PR gate. A separate +# cross-repo action collates the uploaded artifacts across all driver +# repos and sends a single digest; this job does NOT email directly. # # - workflow_dispatch: behaves like the cron run (full reporting). # @@ -27,7 +28,8 @@ name: Security Scan # # Suppressions live in `osv-scanner.toml` as [[IgnoredVulns]] entries # (CVE-id global; OSV-Scanner v2.3.8 doesn't support per-package CVE -# scoping). Each entry has a justification comment. +# scoping). Each entry has a justification comment and an `ignoreUntil` +# expiry so suppressions re-surface for re-review rather than lingering. on: pull_request: @@ -70,9 +72,13 @@ jobs: /tmp/osv-scanner --version - name: Run OSV-Scanner - # Drop -e because osv-scanner exits 1 on ANY finding regardless of - # severity. The severity >= 7 filter below is our actual gate, so - # we explicitly tolerate osv-scanner's non-zero exit via `|| true`. + # osv-scanner's exit codes are meaningful and we must NOT blanket + # `|| true` them: exit 0 = no vulns, exit 1 = vulns found (expected + # -- our real gate is the CVSS>=7 filter below), any OTHER non-zero + # (126/127/128+, network error reaching OSV.dev, corrupt binary, + # partial DB load) = a scanner error that must fail the job CLOSED. + # A blanket `|| true` + zero-byte guard let an errored-but-non-empty + # output pass as "clean" (fail-open); capture and classify the code. run: | set -uo pipefail @@ -81,56 +87,130 @@ jobs: exit 1 fi + scan_rc=0 /tmp/osv-scanner scan source \ --lockfile=package-lock.json \ --config=osv-scanner.toml \ --format=json \ --output-file=/tmp/osv-out.json \ - || true + || scan_rc=$? + + # Tolerate only 0 (clean) and 1 (findings present). Anything else + # is a scanner failure -> fail closed rather than reporting zero. + if [ "$scan_rc" -ne 0 ] && [ "$scan_rc" -ne 1 ]; then + echo "::error::OSV-Scanner exited $scan_rc (scanner error, not a findings result). Failing closed." + exit 1 + fi if [ ! -s /tmp/osv-out.json ]; then echo "::error::OSV-Scanner did not produce an output file." exit 1 fi + # Validate the output is well-formed JSON with a results array + # before any downstream parsing. A truncated/partial write is + # non-empty (defeats the -s guard) but unparseable -- catch it + # here so it fails closed instead of parsing to zero findings. + if ! jq -e 'has("results") and (.results | type == "array")' /tmp/osv-out.json >/dev/null 2>&1; then + echo "::error::OSV-Scanner output is not valid JSON with a .results array (partial/corrupt scan). Failing closed." + exit 1 + fi + # Parse OSV's JSON into job outputs. The terminal steps below - # (PR-fail and email) consume these outputs. + # (PR-fail and scheduled-fail) consume these outputs. # # Two thresholds: PR gating uses CVSS >= 7 (high_count) so we don't - # block merges on MEDIUM/LOW noise; the weekly email reports - # everything (total_findings) so the team has full situational - # awareness of emerging risk before it crosses the gate. + # block merges on MEDIUM/LOW noise; the weekly reports everything + # (total_findings) so the team has full situational awareness of + # emerging risk before it crosses the gate. - name: Collect findings id: findings run: | set -uo pipefail - # All findings (sorted by severity desc). Anything missing a - # CVSS score sorts to 0 -- visible in the report but not silent. - ALL_FINDINGS=$(jq -c '[ - .results[].packages[]? | - .package as $pkg | - .groups[]? | - {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: (.max_severity // "0")} - ] | sort_by(- (.severity | tonumber? // 0))' /tmp/osv-out.json) + # All findings (sorted by severity desc). + # + # Severity resolution is defense-in-depth against fail-open: + # OSV's group-level `.max_severity` is EMPTY ("") for advisory + # groups that lack a CVSS vector (common for GHSA-only and MAL-* + # malware advisories). jq's `//` only coalesces null/false -- an + # empty string is truthy and would pass through, then + # `"" | tonumber? // 0` scores it 0, silently sailing a real HIGH + # past the CVSS>=7 gate. So we do NOT trust max_severity alone: + # 1. Try group `.max_severity` (numeric only; empty/"" -> null). + # 2. Fall back to the max CVSS score across the group's own + # vulnerabilities' `.severities[].score` (parsed from the + # CVSS vector's numeric base score when present). + # 3. If still unresolved, emit the sentinel "UNKNOWN" (a + # non-numeric string), never scored 0. + # + # BLOCKING RULE: unlike the Go driver (whose scoreless findings are + # mostly Go-stdlib advisories delivered via GOTOOLCHAIN and thus + # report-only), npm advisories reliably carry a CVSS score. A + # scoreless UNKNOWN here means a GHSA-only or MAL-* malware advisory + # on a package we depend on -- always BLOCKING (fail closed). + # + # NOTE ON jq NUMBER PARSING: use `try (x|tonumber) catch null`, + # NOT `x|tonumber?`. On a non-numeric string, `tonumber?` yields + # EMPTY (not null); inside an `... as $var` binding that makes the + # entire finding row vanish -- silently dropping a scoreless HIGH. + # try/catch normalizes non-numeric to null so the row survives and + # the fallback logic runs. + ALL_FINDINGS=$(jq -c ' + def cvss_num($sev): + # OSV severity entries: {type:"CVSS_V3", score:"9.8"} (numeric) + # or a full vector string. Take numeric scores only; vectors + # without a bare numeric score contribute nothing (null). + ($sev // []) | map(try (.score | tonumber) catch null) + | map(select(. != null)) | (max // null); + [ + .results[].packages[]? | + .package as $pkg | + (.vulnerabilities // []) as $vulns | + .groups[]? | + .ids as $gids | + # max CVSS across the vulnerabilities referenced by this group + ([ $vulns[] | select(.id as $id | ($gids | index($id)) != null) | cvss_num(.severities) ] + | map(select(. != null)) | (max // null)) as $vuln_score | + (try (.max_severity | tonumber) catch null) as $grp_score | + ($grp_score // $vuln_score) as $resolved | + { + pkg: ($pkg.name + "@" + $pkg.version), + ids: .ids, + severity: (if $resolved == null then "UNKNOWN" else ($resolved | tostring) end) + } + ] | sort_by(if (try (.severity | tonumber) catch null) == null then -1 else - (.severity | tonumber) end) + ' /tmp/osv-out.json) TOTAL_FINDINGS=$(echo "$ALL_FINDINGS" | jq 'length') - # High findings (CVSS >= 7). Both counters are logged so a - # mismatch (e.g. 50 total / 0 high) is visible -- protects - # against silent fail-open if OSV ever changes its severity - # format (e.g. emits "HIGH" instead of a number, which - # `tonumber? // 0` would mask). - HIGH_FINDINGS=$(echo "$ALL_FINDINGS" | jq -c '[.[] | select((.severity | tonumber? // 0) >= 7)]') + # Scoreless (UNKNOWN) findings -- all blocking (see note above). + UNKNOWN_COUNT=$(echo "$ALL_FINDINGS" | jq '[.[] | select(.severity == "UNKNOWN")] | length') + + # Blocking findings = CVSS >= 7 (any package) OR scoreless UNKNOWN. + HIGH_FINDINGS=$(echo "$ALL_FINDINGS" | jq -c '[.[] | select(((.severity | tonumber? // 0) >= 7) or (.severity == "UNKNOWN"))]') HIGH_COUNT=$(echo "$HIGH_FINDINGS" | jq 'length') + # Guard against empty counts propagating to the numeric gates + # below. If any jq above failed, the var would be "" and + # `[ "$X" -gt 0 ]` errors / `'' != '0'` reads true. Default to 0 + # and, since a failed parse should never be silently "clean", + # fail closed if the counts didn't resolve to integers. + TOTAL_FINDINGS=${TOTAL_FINDINGS:-} + HIGH_COUNT=${HIGH_COUNT:-} + UNKNOWN_COUNT=${UNKNOWN_COUNT:-} + if ! [[ "$TOTAL_FINDINGS" =~ ^[0-9]+$ ]] || ! [[ "$HIGH_COUNT" =~ ^[0-9]+$ ]]; then + echo "::error::Could not compute finding counts from OSV output (parse failure). Failing closed." + exit 1 + fi + # Persist the full findings list to a file rather than a job # output -- GitHub Actions outputs are size-capped at 1 MB and - # the formatted email body can be larger than that for big - # finding lists. + # the formatted finding list can be larger than that. echo "$ALL_FINDINGS" > /tmp/all-findings.json echo "total_findings=$TOTAL_FINDINGS" >> "$GITHUB_OUTPUT" echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT" + echo "unknown_count=$UNKNOWN_COUNT" >> "$GITHUB_OUTPUT" # Step summary so findings are visible in the GH Actions UI # without downloading artifacts. @@ -138,7 +218,8 @@ jobs: echo "## OSV-Scanner Findings" echo "" echo "- Total findings (any severity): \`$TOTAL_FINDINGS\`" - echo "- High findings (CVSS >= 7, PR-blocking): \`$HIGH_COUNT\`" + echo "- Blocking findings (CVSS >= 7, or unscored; PR-blocking): \`$HIGH_COUNT\`" + echo "- Unscored/UNKNOWN findings: \`$UNKNOWN_COUNT\` (all blocking)" if [ "$TOTAL_FINDINGS" -gt 0 ]; then echo "" echo "All findings (sorted by severity desc):" @@ -151,7 +232,7 @@ jobs: # Also dump the findings to the job log so they're visible in # the default "Logs" view, not just the step summary panel. - echo "OSV: $TOTAL_FINDINGS total findings, $HIGH_COUNT at CVSS>=7" + echo "OSV: $TOTAL_FINDINGS total findings, $HIGH_COUNT blocking (CVSS>=7 or unscored)" if [ "$TOTAL_FINDINGS" -gt 0 ]; then echo "" echo "All findings (sorted by severity desc):" @@ -160,18 +241,18 @@ jobs: # --- Terminal: PR event --- # Fail the job so the PR's check goes red. No email. - # PR gate is CVSS >= 7 only; MEDIUM/LOW findings show up in the - # step summary but don't block merges. + # PR gate is CVSS >= 7 (or unscored) only; MEDIUM/LOW findings show + # up in the step summary but don't block merges. - name: Fail on findings (PR) if: github.event_name == 'pull_request' && steps.findings.outputs.high_count != '0' run: | set -uo pipefail - # List the actual HIGH findings inline so the author sees what + # List the actual blocking findings inline so the author sees what # needs fixing without clicking through to the step summary # panel or downloading artifacts. - HIGH_FINDINGS=$(jq -c '[.[] | select((.severity | tonumber? // 0) >= 7)]' /tmp/all-findings.json) + HIGH_FINDINGS=$(jq -c '[.[] | select(((.severity | tonumber? // 0) >= 7) or (.severity == "UNKNOWN"))]' /tmp/all-findings.json) - echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed CVSS>=7 finding(s) in this PR:" + echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed blocking finding(s) (CVSS>=7, or unscored) in this PR:" echo "" echo "$HIGH_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"' echo "" @@ -184,60 +265,26 @@ jobs: exit 1 # --- Terminal: scheduled/manual event --- - # Weekly reports ALL findings (not just CVSS >= 7) so the team sees - # emerging risk before it crosses the PR gate. PR-time is narrower - # to avoid blocking on MEDIUM/LOW noise; weekly is broader because - # it's read by humans, not enforced. - - name: Compose email body - if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' - run: | - set -uo pipefail - { - echo "SQL Node.js Driver Security Scan Results" - echo "" - echo "

Security Vulnerabilities Found

" - echo "

${{ steps.findings.outputs.total_findings }} total finding(s) on main; ${{ steps.findings.outputs.high_count }} are CVSS >= 7 (PR-blocking).

" - echo "

Full reports are attached to the GitHub Actions run as artifacts: View Artifacts

" - echo "" - jq -r '.[] | - (if (.severity | tonumber? // 0) >= 7 then "high" - elif (.severity | tonumber? // 0) >= 4 then "medium" - else "" end) as $cls | - "" - ' /tmp/all-findings.json - echo "
SeverityPackageIDs
\(.severity)\(.pkg)\(.ids | join(", "))
" - echo "" - } > security-scan-report.html - - - name: Send Email - if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' - uses: dawidd6/action-send-mail@4226df7daafa6fc901a43789c49bf7ab309066e7 # v3 - with: - server_address: smtp.gmail.com - server_port: 465 - username: ${{ secrets.SMTP_USERNAME }} - password: ${{ secrets.SMTP_PASSWORD }} - subject: OSS SQL Node.js Driver Security Scan - 🚨 Vulnerabilities Found - html_body: file://security-scan-report.html - to: ${{ secrets.EMAIL_RECIPIENTS }} - from: SQL Node.js Driver Security Scanner - content_type: text/html - + # Weekly reports ALL findings (not just CVSS >= 7) so emerging risk is + # visible before it crosses the PR gate. PR-time is narrower to avoid + # blocking on MEDIUM/LOW noise; weekly is broader for situational + # awareness. + # + # Notification is intentionally NOT done here: a separate cross-repo + # action collates findings from all driver repos and sends a single + # digest. This job's job is to (a) fail so the scheduled run is red + # when anything is found, and (b) upload the raw osv-out.json artifact + # for the collator to consume. - name: Fail on findings (scheduled/manual) if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' run: | - echo "::error::${{ steps.findings.outputs.total_findings }} OSV finding(s) on main (${{ steps.findings.outputs.high_count }} at CVSS>=7). Email sent." + echo "::error::${{ steps.findings.outputs.total_findings }} OSV finding(s) on main (${{ steps.findings.outputs.high_count }} blocking at CVSS>=7 or unscored). See the security-scan-reports artifact." exit 1 - # Always upload artifacts so triagers can pull the full reports - # without having to rerun anything. + # Always upload the raw scan output so triagers -- and the planned + # cross-repo collation/notification action -- can pull findings + # without rerunning. This is the machine-readable source of truth now + # that per-repo email has been removed. - name: Upload reports if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 @@ -245,5 +292,5 @@ jobs: name: security-scan-reports path: | /tmp/osv-out.json - security-scan-report.html + /tmp/all-findings.json if-no-files-found: ignore diff --git a/osv-scanner.toml b/osv-scanner.toml index c82f8de4..b15bf0d4 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -14,6 +14,19 @@ # # See google.github.io/osv-scanner/configuration/ for the schema. # +# CONVENTION: use suppressions sparingly, only with a strong reason +# (unreachable code path + no fix available, or dev-only + not shipped). +# EVERY [[IgnoredVulns]] entry MUST set `ignoreUntil = "YYYY-MM-DD"` +# (~6 months out). OSV-Scanner v2.3.8 honors it natively; when it lapses +# the finding re-surfaces, forcing a re-review instead of a permanent +# silent ignore. +# +# Example: +# [[IgnoredVulns]] +# id = "GHSA-xxxx-xxxx-xxxx" +# ignoreUntil = "2026-01-15" +# reason = "dev-only (eslint toolchain); not reachable from shipped dist/." +# # This file starts empty -- populate iteratively as the first scan run # surfaces real false positives or dev-only findings worth excluding. # Do not pre-populate with speculative suppressions. From ee87b692cda07d43f167c9ca01f0c6526221ec13 Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Tue, 14 Jul 2026 06:53:26 +0000 Subject: [PATCH 3/3] security-scan: prettier formatting on the cron schedule line 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 --- .github/workflows/securityScan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml index ca02dbe6..561e99ce 100644 --- a/.github/workflows/securityScan.yml +++ b/.github/workflows/securityScan.yml @@ -35,7 +35,7 @@ on: pull_request: branches: [main] schedule: - - cron: '0 0 * * 0' # Run every Sunday at midnight UTC + - cron: '0 0 * * 0' # Run every Sunday at midnight UTC workflow_dispatch: permissions: