Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 312 additions & 0 deletions .github/workflows/securityScan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,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 &gt;= 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
Loading
Loading