Skip to content

Commit 1148152

Browse files
Add OSV-Scanner security gate + clear all CVEs (Python floor to 3.10) (#798)
* 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 Python: - Reads poetry.lock natively via OSV-Scanner --lockfile (no separate SBOM tool needed) - Reuses the existing ./.github/actions/setup-jfrog composite action for parity with other workflows (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 14 HIGH / 10 MED / 1 LOW (25 total) -- concentrated in cryptography, urllib3, pyjwt, pyarrow, requests, black, pytest, python-dotenv, idna. These will be addressed by a follow-up dep-bump PR. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * Harden OSV gate (fail-closed) + refresh lockfile to clear all CVEs Port the fail-closed hardening from the Go (#362) and Node (#388) OSV workflows, and refresh poetry.lock so the gate passes with zero suppressions. securityScan.yml hardening (was fail-open in three places): - Capture osv-scanner's exit code; tolerate only 0/1 and fail closed on any other code (network error, corrupt binary) instead of masking it with `|| true`. - Validate the output is well-formed JSON with a .results array before parsing, so a truncated/partial scan fails closed rather than parsing to zero findings. - Resolve empty group max_severity via a cvss_num fallback to an UNKNOWN sentinel (using `try (x|tonumber) catch null`, not `tonumber?`), so a scoreless finding can never sort to 0 and sail past the CVSS>=7 gate. UNKNOWN always blocks (PyPA advisories carry CVSS; a scoreless finding is a GHSA-only/malware advisory). - Integer-count guards fail closed on parse failure. - Drop per-repo SMTP email in favor of artifact upload for the planned cross-repo collator (parity with Go/Node). CVE clearing WITHOUT forcing dependency floors: - Bump the Python floor to ^3.10. The CVE-fixed cryptography (>=46) and pyjwt (>=2.12) require Python >=3.10 upstream, so a single CVE-clean lockfile cannot span 3.8/3.9. This is the only breaking change. - All runtime dependency pins are UNCHANGED (thrift ~=0.22.0, urllib3 >=1.26, requests ^2.18.1, pyjwt ^2.0.0, pyarrow floors). The existing constraints already ALLOW the CVE-free versions; the refreshed lock simply resolves to them (urllib3 2.7.0, cryptography 49.0.0, pyarrow 23.0.1, requests 2.34.2, pyjwt 2.13.0, idna 3.18, python-dotenv 1.2.2). Customers do not need us to relax or raise any pin to become CVE-free. - thrift stays ~=0.22.0 (no known advisory; the <0.23 cap avoids the ES-1960554 DBR-LTS install break). - Bump dev-only black ^22 -> ^26 and pytest ^7 -> ^9 to clear their advisories (never shipped in the wheel); reformat src with black 26. Result: OSV-Scanner v2.3.8 reports 0 findings on the refreshed lock; osv-scanner.toml needs no suppressions. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * ci: drop Python 3.9 from CI matrices to match the ^3.10 floor The pyproject floor is now ^3.10, so 3.9 legs can no longer `poetry install` (^3.10 is unsatisfiable on a 3.9 interpreter) and would fail. Remove "3.9" from every unit-test / lint / type-check / pyarrow / kernel matrix in code-quality-checks.yml and warm-deps-cache.yml, and drop the now-moot 3.9-kernel exclude in the warm-deps cache. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * Document thrift CPE-gap accurately + add weekly NVD-CPE thrift watch Corrects the earlier "thrift ~=0.22.0 (no known advisory)" wording, which was wrong: thrift 0.22.0 IS affected by open Apache Thrift advisories (CVE-2025-48431 + the CVE-2026-41602..41636 set, all fixed in 0.23.0). Why we still hold at ~=0.22.0 and accept them: - Apache Thrift is one monorepo shipping ~20 language libraries; the PyPI `thrift` package is built only from lib/py. Each of these CVEs is in a NON-Python binding -- verified against the upstream oss-security advisories: Node.js (41636), Go (41602), c_glib/C (48431), Java (41603), Swift (41604, 41605). None touches the Python code paths we ship. - The only fix (0.23.0) is the version that caused SEV0 ES-1960554 on DBR-LTS old setuptools, so we cannot take it until a build-safe thrift ships (THRIFT-6067). Why the OSV gate doesn't flag it (and why that is NOT proof Python is safe): - These CVEs are in OSV with `affected[].package = null` -- only a GIT/CPE coordinate (cpe:2.3:a:apache:thrift), no PyPI/npm/Go package entry. OSV and Dependabot both match by package purl, so they return nothing for PyPI thrift. This is a coordinate blind spot, independent of whether Python is affected -- a FUTURE Python-affecting thrift CVE filed the same way would also be missed. Mitigation: a supplementary NVD-CPE thrift watch in securityScan.yml, scheduled/manual only (never PR; NVD rate limits). It lists all apache:thrift CVEs affecting the locked version in the weekly summary and hard-fails if any description names Python. Scoped to thrift alone because an audit of all three drivers' full dependency sets found thrift is the only dep with this purl-vs-CPE gap (Go/Node already ship the fixed 0.23.0). The Python-detection is a heuristic (description must say python/lib/py); the full list is always surfaced for human review. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * fix(types): resolve 2 mypy errors surfaced by the refreshed mypy The lockfile refresh floats mypy (^1.10.1) up to 1.20.2, which is stricter and flags two pre-existing latent type issues that the older mypy missed: - auth/oauth.py:48 — is_expired() returned `exp_time and (...)`, whose value is `Any | None` (the exp claim) when falsy, not bool, violating the `-> bool` annotation. Use `exp_time is not None and (...)` so the return is a real bool and the None-exp case is explicit. - auth/retry.py:248 — the command_type setter was annotated `value: CommandType`, but the getter returns `Optional[CommandType]` and __private_init__ assigns an `Optional[CommandType]`. Widen the setter to `Optional[CommandType]` to match the getter and actual usage. Both are type-annotation-only changes; no runtime behavior change. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * fix(tests): declare pytz explicitly (pandas 3.0 dropped it as a hard dep) tests/unit/test_parameters.py imports pytz directly. It previously arrived transitively via pandas, but the refreshed lock resolves pandas 3.0.3 on Python >=3.11, and pandas 3.0 removed pytz from its required dependencies (it is now only a pandas extra). That broke test collection on 3.11+ with `ModuleNotFoundError: No module named 'pytz'`. Add pytz as an explicit dev dependency so the test suite no longer relies on pandas's transitive graph, which differs across the Python matrix. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * Bump thrift to ~=0.24.0 (CVE-clean + DBR-LTS-safe via wheels) thrift 0.24.0 shipped (THRIFT-6067) after this PR was opened. It is the first release that both clears the open Apache Thrift CVEs (CVE-2025-48431 + the CVE-2026-41602..41636 set, all fixed in 0.23.0) AND is safe to install on DBR LTS: unlike the yanked 0.23.0 (sdist-only, setup.py sys.exit(0) → SEV0 ES-1960554), 0.24.0 ships prebuilt manylinux2014 wheels (cp310-cp314) + macOS/musl/Windows, so pip installs a wheel and never runs setup.py -- the ES-1960554 build break cannot trigger. Gated on the DBR LTS Install CI check. Supersedes the earlier "hold at ~=0.22.0 and accept the thrift CVEs as non-Python-binding" stance now that a build-safe fixed thrift exists. The supplementary NVD-CPE thrift watch is no longer needed and is removed. Lockfile regenerated: thrift 0.24.0; all other CVE deps unchanged and clean. OSV-Scanner: 0 findings. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> * Remove the supplementary thrift NVD-CPE watch (obsolete after 0.24.0) The weekly NVD-CPE thrift watch existed only because we were accepting the thrift CVEs on the 0.22.0 pin (filed against the CPE, not the PyPI purl, so the OSV gate couldn't see them). With thrift bumped to 0.24.0 those CVEs are actually cleared, so the watch has nothing left to guard. Remove it; the normal OSV gate + version pin handle any future thrift CVE. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com> --------- Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
1 parent 9f81d35 commit 1148152

20 files changed

Lines changed: 1717 additions & 1308 deletions

.github/workflows/code-quality-checks.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
labels: linux-ubuntu-latest
3535
strategy:
3636
matrix:
37-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
37+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
3838
dependency-version: ["default", "min"]
3939
exclude:
4040
- python-version: "3.12"
@@ -96,7 +96,7 @@ jobs:
9696
labels: linux-ubuntu-latest
9797
strategy:
9898
matrix:
99-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
99+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
100100
dependency-version: ["default", "min"]
101101
exclude:
102102
- python-version: "3.12"
@@ -246,7 +246,7 @@ jobs:
246246
labels: linux-ubuntu-latest
247247
strategy:
248248
matrix:
249-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
249+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
250250
steps:
251251
- name: Check out repository
252252
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
@@ -273,7 +273,7 @@ jobs:
273273
labels: linux-ubuntu-latest
274274
strategy:
275275
matrix:
276-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
276+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
277277
steps:
278278
- name: Check out repository
279279
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

.github/workflows/securityScan.yml

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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+), or any unscored finding. MEDIUM/LOW
8+
# findings show in the step summary but don't block merges. Not yet
9+
# required-to-merge in branch protection.
10+
#
11+
# - cron (weekly): report ALL findings regardless of severity, fail the
12+
# job on any finding, and upload the raw scan output as an artifact.
13+
# The intent is full situational awareness -- emerging MEDIUM risks
14+
# should be visible before they cross the PR gate. A separate
15+
# cross-repo action collates the uploaded artifacts across all driver
16+
# repos and sends a single digest; this job does NOT email directly.
17+
#
18+
# - workflow_dispatch: behaves like the cron run (full reporting).
19+
#
20+
# Scanner: OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates GHSA,
21+
# NVD, PyPA, RustSec, Go vuln DB). Reads `poetry.lock` natively --
22+
# no separate SBOM tool needed.
23+
#
24+
# NOTE: this scans BOTH runtime and dev dependencies (OSV treats
25+
# everything in poetry.lock equally). If a finding is dev-only and
26+
# shouldn't block merges, suppress it via osv-scanner.toml with a
27+
# justification ("dev-only, not shipped in the wheel").
28+
#
29+
# Suppressions live in `osv-scanner.toml` as [[IgnoredVulns]] entries
30+
# (CVE-id global; OSV-Scanner v2.3.8 doesn't support per-package CVE
31+
# scoping). Each entry has a justification comment and an `ignoreUntil`
32+
# expiry so suppressions re-surface for re-review rather than lingering.
33+
34+
on:
35+
pull_request:
36+
branches: [main]
37+
schedule:
38+
- cron: '0 0 * * 0' # Run every Sunday at midnight UTC
39+
workflow_dispatch:
40+
41+
permissions:
42+
id-token: write
43+
contents: read
44+
45+
jobs:
46+
security-scan:
47+
name: Security Scan
48+
runs-on:
49+
group: databricks-protected-runner-group
50+
labels: linux-ubuntu-latest
51+
52+
steps:
53+
- name: Checkout repository
54+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
55+
56+
# JFrog OIDC + pip: skipped on fork PRs (no OIDC token from
57+
# GitHub's perspective). OSV-Scanner reads poetry.lock directly
58+
# without needing to download wheels, so fork PRs still work; we
59+
# keep setup-jfrog here only for parity with the other workflows
60+
# in this repo. If you remove it later, also remove the
61+
# `id-token: write` permission above.
62+
- name: Setup JFrog
63+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
64+
uses: ./.github/actions/setup-jfrog
65+
66+
- name: Install osv-scanner
67+
run: |
68+
set -euo pipefail
69+
curl -fsSL -o /tmp/osv-scanner \
70+
https://github.com/google/osv-scanner/releases/download/v2.3.8/osv-scanner_linux_amd64
71+
chmod +x /tmp/osv-scanner
72+
/tmp/osv-scanner --version
73+
74+
- name: Run OSV-Scanner
75+
# osv-scanner's exit codes are meaningful and we must NOT blanket
76+
# `|| true` them: exit 0 = no vulns, exit 1 = vulns found (expected
77+
# -- our real gate is the CVSS>=7 filter below), any OTHER non-zero
78+
# (126/127/128+, network error reaching OSV.dev, corrupt binary,
79+
# partial DB load) = a scanner error that must fail the job CLOSED.
80+
# A blanket `|| true` + zero-byte guard let an errored-but-non-empty
81+
# output pass as "clean" (fail-open); capture and classify the code.
82+
run: |
83+
set -uo pipefail
84+
85+
if [ ! -f poetry.lock ]; then
86+
echo "::error::poetry.lock not found at repo root."
87+
exit 1
88+
fi
89+
90+
scan_rc=0
91+
/tmp/osv-scanner scan source \
92+
--lockfile=poetry.lock \
93+
--config=osv-scanner.toml \
94+
--format=json \
95+
--output-file=/tmp/osv-out.json \
96+
|| scan_rc=$?
97+
98+
# Tolerate only 0 (clean) and 1 (findings present). Anything else
99+
# is a scanner failure -> fail closed rather than reporting zero.
100+
if [ "$scan_rc" -ne 0 ] && [ "$scan_rc" -ne 1 ]; then
101+
echo "::error::OSV-Scanner exited $scan_rc (scanner error, not a findings result). Failing closed."
102+
exit 1
103+
fi
104+
105+
if [ ! -s /tmp/osv-out.json ]; then
106+
echo "::error::OSV-Scanner did not produce an output file."
107+
exit 1
108+
fi
109+
110+
# Validate the output is well-formed JSON with a results array
111+
# before any downstream parsing. A truncated/partial write is
112+
# non-empty (defeats the -s guard) but unparseable -- catch it
113+
# here so it fails closed instead of parsing to zero findings.
114+
if ! jq -e 'has("results") and (.results | type == "array")' /tmp/osv-out.json >/dev/null 2>&1; then
115+
echo "::error::OSV-Scanner output is not valid JSON with a .results array (partial/corrupt scan). Failing closed."
116+
exit 1
117+
fi
118+
119+
# Parse OSV's JSON into job outputs. The terminal steps below
120+
# (PR-fail and scheduled-fail) consume these outputs.
121+
#
122+
# Two thresholds: PR gating uses CVSS >= 7 (high_count) so we don't
123+
# block merges on MEDIUM/LOW noise; the weekly reports everything
124+
# (total_findings) so the team has full situational awareness of
125+
# emerging risk before it crosses the gate.
126+
- name: Collect findings
127+
id: findings
128+
run: |
129+
set -uo pipefail
130+
131+
# All findings (sorted by severity desc).
132+
#
133+
# Severity resolution is defense-in-depth against fail-open:
134+
# OSV's group-level `.max_severity` is EMPTY ("") for advisory
135+
# groups that lack a CVSS vector (common for GHSA-only and MAL-*
136+
# malware advisories). jq's `//` only coalesces null/false -- an
137+
# empty string is truthy and would pass through, then
138+
# `"" | tonumber? // 0` scores it 0, silently sailing a real HIGH
139+
# past the CVSS>=7 gate. So we do NOT trust max_severity alone:
140+
# 1. Try group `.max_severity` (numeric only; empty/"" -> null).
141+
# 2. Fall back to the max CVSS score across the group's own
142+
# vulnerabilities' `.severities[].score` (parsed from the
143+
# CVSS vector's numeric base score when present).
144+
# 3. If still unresolved, emit the sentinel "UNKNOWN" (a
145+
# non-numeric string), never scored 0.
146+
#
147+
# BLOCKING RULE: unlike the Go driver (whose scoreless findings are
148+
# mostly Go-stdlib advisories delivered via GOTOOLCHAIN and thus
149+
# report-only), PyPA advisories reliably carry a CVSS score. A
150+
# scoreless UNKNOWN here means a GHSA-only or MAL-* malware advisory
151+
# on a package we depend on -- always BLOCKING (fail closed).
152+
#
153+
# NOTE ON jq NUMBER PARSING: use `try (x|tonumber) catch null`,
154+
# NOT `x|tonumber?`. On a non-numeric string, `tonumber?` yields
155+
# EMPTY (not null); inside an `... as $var` binding that makes the
156+
# entire finding row vanish -- silently dropping a scoreless HIGH.
157+
# try/catch normalizes non-numeric to null so the row survives and
158+
# the fallback logic runs.
159+
ALL_FINDINGS=$(jq -c '
160+
def cvss_num($sev):
161+
# OSV severity entries: {type:"CVSS_V3", score:"9.8"} (numeric)
162+
# or a full vector string. Take numeric scores only; vectors
163+
# without a bare numeric score contribute nothing (null).
164+
($sev // []) | map(try (.score | tonumber) catch null)
165+
| map(select(. != null)) | (max // null);
166+
[
167+
.results[].packages[]? |
168+
.package as $pkg |
169+
(.vulnerabilities // []) as $vulns |
170+
.groups[]? |
171+
.ids as $gids |
172+
# max CVSS across the vulnerabilities referenced by this group
173+
([ $vulns[] | select(.id as $id | ($gids | index($id)) != null) | cvss_num(.severities) ]
174+
| map(select(. != null)) | (max // null)) as $vuln_score |
175+
(try (.max_severity | tonumber) catch null) as $grp_score |
176+
($grp_score // $vuln_score) as $resolved |
177+
{
178+
pkg: ($pkg.name + "@" + $pkg.version),
179+
ids: .ids,
180+
severity: (if $resolved == null then "UNKNOWN" else ($resolved | tostring) end)
181+
}
182+
] | sort_by(if (try (.severity | tonumber) catch null) == null then -1 else - (.severity | tonumber) end)
183+
' /tmp/osv-out.json)
184+
TOTAL_FINDINGS=$(echo "$ALL_FINDINGS" | jq 'length')
185+
186+
# Scoreless (UNKNOWN) findings -- all blocking (see note above).
187+
UNKNOWN_COUNT=$(echo "$ALL_FINDINGS" | jq '[.[] | select(.severity == "UNKNOWN")] | length')
188+
189+
# Blocking findings = CVSS >= 7 (any package) OR scoreless UNKNOWN.
190+
HIGH_FINDINGS=$(echo "$ALL_FINDINGS" | jq -c '[.[] | select(((.severity | tonumber? // 0) >= 7) or (.severity == "UNKNOWN"))]')
191+
HIGH_COUNT=$(echo "$HIGH_FINDINGS" | jq 'length')
192+
193+
# Guard against empty counts propagating to the numeric gates
194+
# below. If any jq above failed, the var would be "" and
195+
# `[ "$X" -gt 0 ]` errors / `'' != '0'` reads true. Default to 0
196+
# and, since a failed parse should never be silently "clean",
197+
# fail closed if the counts didn't resolve to integers.
198+
TOTAL_FINDINGS=${TOTAL_FINDINGS:-}
199+
HIGH_COUNT=${HIGH_COUNT:-}
200+
UNKNOWN_COUNT=${UNKNOWN_COUNT:-}
201+
if ! [[ "$TOTAL_FINDINGS" =~ ^[0-9]+$ ]] || ! [[ "$HIGH_COUNT" =~ ^[0-9]+$ ]]; then
202+
echo "::error::Could not compute finding counts from OSV output (parse failure). Failing closed."
203+
exit 1
204+
fi
205+
206+
# Persist the full findings list to a file rather than a job
207+
# output -- GitHub Actions outputs are size-capped at 1 MB and
208+
# the formatted finding list can be larger than that.
209+
echo "$ALL_FINDINGS" > /tmp/all-findings.json
210+
211+
echo "total_findings=$TOTAL_FINDINGS" >> "$GITHUB_OUTPUT"
212+
echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT"
213+
echo "unknown_count=$UNKNOWN_COUNT" >> "$GITHUB_OUTPUT"
214+
215+
# Step summary so findings are visible in the GH Actions UI
216+
# without downloading artifacts.
217+
{
218+
echo "## OSV-Scanner Findings"
219+
echo ""
220+
echo "- Total findings (any severity): \`$TOTAL_FINDINGS\`"
221+
echo "- Blocking findings (CVSS >= 7, or unscored; PR-blocking): \`$HIGH_COUNT\`"
222+
echo "- Unscored/UNKNOWN findings: \`$UNKNOWN_COUNT\` (all blocking)"
223+
if [ "$TOTAL_FINDINGS" -gt 0 ]; then
224+
echo ""
225+
echo "All findings (sorted by severity desc):"
226+
echo ""
227+
echo "| Severity | Package | IDs |"
228+
echo "|---|---|---|"
229+
echo "$ALL_FINDINGS" | jq -r '.[] | "| \(.severity) | \(.pkg) | \(.ids | join(",")) |"'
230+
fi
231+
} >> "$GITHUB_STEP_SUMMARY"
232+
233+
# Also dump the findings to the job log so they're visible in
234+
# the default "Logs" view, not just the step summary panel.
235+
echo "OSV: $TOTAL_FINDINGS total findings, $HIGH_COUNT blocking (CVSS>=7 or unscored)"
236+
if [ "$TOTAL_FINDINGS" -gt 0 ]; then
237+
echo ""
238+
echo "All findings (sorted by severity desc):"
239+
echo "$ALL_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"'
240+
fi
241+
242+
# --- Terminal: PR event ---
243+
# Fail the job so the PR's check goes red. No email.
244+
# PR gate is CVSS >= 7 (or unscored) only; MEDIUM/LOW findings show
245+
# up in the step summary but don't block merges.
246+
- name: Fail on findings (PR)
247+
if: github.event_name == 'pull_request' && steps.findings.outputs.high_count != '0'
248+
run: |
249+
set -uo pipefail
250+
# List the actual blocking findings inline so the author sees what
251+
# needs fixing without clicking through to the step summary
252+
# panel or downloading artifacts.
253+
HIGH_FINDINGS=$(jq -c '[.[] | select(((.severity | tonumber? // 0) >= 7) or (.severity == "UNKNOWN"))]' /tmp/all-findings.json)
254+
255+
echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed blocking finding(s) (CVSS>=7, or unscored) in this PR:"
256+
echo ""
257+
echo "$HIGH_FINDINGS" | jq -r '.[] | " [\(.severity)] \(.pkg) \(.ids | join(", "))"'
258+
echo ""
259+
echo "Fix by either:"
260+
echo " 1. Bumping the affected dependency to a patched version, or"
261+
echo " 2. Adding a documented [[IgnoredVulns]] entry to osv-scanner.toml"
262+
echo " with a clear justification for why the CVE doesn't apply to our usage."
263+
echo ""
264+
echo "Full step summary: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
265+
exit 1
266+
# --- Terminal: scheduled/manual event ---
267+
# Weekly reports ALL findings (not just CVSS >= 7) so emerging risk is
268+
# visible before it crosses the PR gate. PR-time is narrower to avoid
269+
# blocking on MEDIUM/LOW noise; weekly is broader for situational
270+
# awareness.
271+
#
272+
# Notification is intentionally NOT done here: a separate cross-repo
273+
# action collates findings from all driver repos and sends a single
274+
# digest. This job's job is to (a) fail so the scheduled run is red
275+
# when anything is found, and (b) upload the raw osv-out.json artifact
276+
# for the collator to consume.
277+
- name: Fail on findings (scheduled/manual)
278+
if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0'
279+
run: |
280+
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."
281+
exit 1
282+
283+
# Always upload the raw scan output so triagers -- and the planned
284+
# cross-repo collation/notification action -- can pull findings
285+
# without rerunning. This is the machine-readable source of truth now
286+
# that per-repo email has been removed.
287+
- name: Upload reports
288+
if: always()
289+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
290+
with:
291+
name: security-scan-reports
292+
path: |
293+
/tmp/osv-out.json
294+
/tmp/all-findings.json
295+
if-no-files-found: ignore

.github/workflows/warm-deps-cache.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
8686
# Mirror code-quality-checks.yml exactly so every fork check has a
8787
# matching warmed entry. extras: "" is the base unit-test / lint / type
8888
# environment; pyarrow and kernel back the two extra unit-test tiers.
89-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
89+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
9090
dependency-version: ["default", "min"]
9191
extras: ["", "pyarrow", "kernel"]
9292
exclude:
@@ -98,10 +98,6 @@ jobs:
9898
dependency-version: "min"
9999
- python-version: "3.13"
100100
dependency-version: "min"
101-
# The kernel wheel is cp310-abi3 (Requires-Python >=3.10); the
102-
# [kernel] extra is a no-op on 3.9, so there's no kernel leg to warm.
103-
- python-version: "3.9"
104-
extras: "kernel"
105101

106102
name: "Warm (py ${{ matrix.python-version }}, ${{ matrix.dependency-version }} deps, extras=${{ matrix.extras || 'base' }})"
107103

osv-scanner.toml

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

0 commit comments

Comments
 (0)