-
Notifications
You must be signed in to change notification settings - Fork 40
312 lines (281 loc) · 14.1 KB
/
securityScan.yml
File metadata and controls
312 lines (281 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
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, PyPA, RustSec, Go vuln DB). Reads the cyclonedx aggregate SBOM
# produced by `mvn package` so it sees the actually-resolved local
# dependency tree, not deps.dev's stale published-artifact metadata.
#
# OSV replaced OWASP dependency-check (NVD CPE-based) as the sole gate
# in PR #1460. OSV's database is a strict superset of NVD's, and several
# real CVEs (CVE-2025-66566 in lz4, CVE-2026-5598 in bouncycastle) are
# GHSA-only with no NVD CPE -- invisible to OWASP, caught by OSV. The
# `owasp-suppressions.xml` and dependency-check plugin in jdbc-core/pom.xml
# remain in the repo because the in-repo release.yml/release-thin.yml
# workflows still reference them, but those workflows are themselves
# `if: false` and superseded by databricks/secure-public-registry-releases-eng.
#
# 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
- name: Set up JDK 11
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
java-version: '11'
distribution: 'temurin'
cache: maven
# JFrog OIDC + maven proxy: skipped on fork PRs (no OIDC token from
# GitHub's perspective). Fork PRs still work because all of the
# driver's direct dependencies are published to public Maven Central
# (verified against jdbc-core/pom.xml); without ~/.m2/settings.xml,
# Maven falls through to Central directly. JFrog is just a faster
# mirror, not a source of any artifact the build genuinely needs.
- name: Get JFrog OIDC token
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
run: |
set -euo pipefail
ID_TOKEN=$(curl -sLS \
-H "User-Agent: actions/oidc-client" \
-H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=jfrog-github" | jq .value | tr -d '"')
echo "::add-mask::${ID_TOKEN}"
ACCESS_TOKEN=$(curl -sLS -XPOST -H "Content-Type: application/json" \
"https://databricks.jfrog.io/access/api/v1/oidc/token" \
-d "{\"grant_type\": \"urn:ietf:params:oauth:grant-type:token-exchange\", \"subject_token_type\":\"urn:ietf:params:oauth:token-type:id_token\", \"subject_token\": \"${ID_TOKEN}\", \"provider_name\": \"github-actions\"}" | jq .access_token | tr -d '"')
echo "::add-mask::${ACCESS_TOKEN}"
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
echo "FAIL: Could not extract JFrog access token"
exit 1
fi
echo "JFROG_ACCESS_TOKEN=${ACCESS_TOKEN}" >> "$GITHUB_ENV"
- name: Configure maven
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
run: |
set -euo pipefail
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<mirrors>
<mirror>
<id>jfrog-central</id>
<mirrorOf>*</mirrorOf>
<url>https://databricks.jfrog.io/artifactory/db-maven/</url>
</mirror>
</mirrors>
<servers>
<server>
<id>jfrog-central</id>
<username>gha-service-account</username>
<password>${JFROG_ACCESS_TOKEN}</password>
</server>
</servers>
</settings>
EOF
# Build the project to produce the cyclonedx aggregate SBOM that OSV
# will scan. -Ddependency-check.skip=true because the OWASP plugin
# is bound to the verify phase in jdbc-core/pom.xml and we don't
# use it anymore -- skipping saves ~2 minutes.
- name: Build (generates cyclonedx SBOM)
run: mvn package -DskipTests -Ddependency-check.skip=true -B
- 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 target/bom.json ]; then
echo "::error::SBOM not found at target/bom.json (build likely failed)."
exit 1
fi
/tmp/osv-scanner scan source \
--recursive=false \
--config=osv-scanner.toml \
--format=json \
--output-file=/tmp/osv-out.json \
target/bom.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 "<!DOCTYPE html><html><head><title>JDBC Driver Security Scan Results</title>"
echo "<style>"
echo " body { font-family: -apple-system, sans-serif; }"
echo " table { border-collapse: collapse; margin-top: 1em; }"
echo " th, td { border: 1px solid #ddd; padding: 6px 12px; text-align: left; }"
echo " th { background: #f5f5f5; }"
echo " tr.high { background: #ffe5e5; }"
echo " tr.medium { background: #fff5e5; }"
echo "</style></head><body>"
echo "<h1>Security Vulnerabilities Found</h1>"
echo "<p><b>${{ steps.findings.outputs.total_findings }}</b> total finding(s) on main; <b>${{ steps.findings.outputs.high_count }}</b> are CVSS >= 7 (PR-blocking).</p>"
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>"
echo "<table><tr><th>Severity</th><th>Package</th><th>IDs</th></tr>"
jq -r '.[] |
(if (.severity | tonumber? // 0) >= 7 then "high"
elif (.severity | tonumber? // 0) >= 4 then "medium"
else "" end) as $cls |
"<tr class=\"\($cls)\"><td>\(.severity)</td><td>\(.pkg)</td><td>\(.ids | join(", "))</td></tr>"
' /tmp/all-findings.json
echo "</table>"
echo "</body></html>"
} > 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 JDBC Driver Security Scan - 🚨 Vulnerabilities Found
html_body: file://security-scan-report.html
to: ${{ secrets.EMAIL_RECIPIENTS }}
from: JDBC 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
target/bom.json
security-scan-report.html
if-no-files-found: ignore