diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml new file mode 100644 index 000000000..94de356e7 --- /dev/null +++ b/.github/workflows/securityScan.yml @@ -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 + + + + jfrog-central + * + https://databricks.jfrog.io/artifactory/db-maven/ + + + + + jfrog-central + gha-service-account + ${JFROG_ACCESS_TOKEN} + + + + 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 "JDBC 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 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 diff --git a/.github/workflows/vulnerabilityCatcher.yml b/.github/workflows/vulnerabilityCatcher.yml deleted file mode 100644 index 42be61e88..000000000 --- a/.github/workflows/vulnerabilityCatcher.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: Weekly Security Scan - -on: - schedule: - - cron: '0 0 * * 0' # Run every Sunday at midnight UTC - workflow_dispatch: # Allow manual triggering - -permissions: - id-token: write - contents: read - -jobs: - security-scan: - runs-on: - group: databricks-protected-runner-group - labels: linux-ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - ref: main # Explicitly check out main branch - - - name: Set up JDK 11 - uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4 - with: - java-version: '11' - distribution: 'temurin' - cache: maven - - - name: Get JFrog OIDC token - run: | - set -euo pipefail - - # Get GitHub OIDC ID token - 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}" - - # Exchange for JFrog access 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" - - echo "JFrog OIDC token obtained successfully" - - - name: Configure maven - run: | - set -euo pipefail - - mkdir -p ~/.m2 - cat > ~/.m2/settings.xml << EOF - - - - jfrog-central - * - https://databricks.jfrog.io/artifactory/db-maven/ - - - - - jfrog-central - gha-service-account - ${JFROG_ACCESS_TOKEN} - - - - EOF - - echo "Maven configured to use JFrog registry" - - - name: Run OWASP Dependency Check - run: mvn -pl jdbc-core org.owasp:dependency-check-maven:check -Dnvd.api.key=${{ secrets.NVD_API_KEY }} - - - name: Check for vulnerabilities - id: check_vulnerabilities - run: | - if grep -q "CVSS score >= 7" jdbc-core/target/dependency-check-report.html; then - echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT - echo "Critical or high vulnerabilities found (CVSS score >= 7)" - # Generate a simple HTML report for email - echo "JDBC Driver Security Scan Results" > security-scan-report.html - echo "

Security Vulnerabilities Found

" >> security-scan-report.html - echo "

Critical or high vulnerabilities (CVSS score >= 7) were found in the weekly scan of the JDBC driver.

" >> security-scan-report.html - echo "

Please check the full report in the GitHub Actions artifacts: View Artifacts

" >> security-scan-report.html - echo "" >> security-scan-report.html - exit 1 - else - echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT - echo "No critical or high vulnerabilities found" - fi - - - name: Send Email - if: steps.check_vulnerabilities.outputs.has_vulnerabilities == 'true' - 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: Upload Report as Artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: security-scan-reports - path: | - jdbc-core/target/dependency-check-report.html - jdbc-core/target/dependency-check-report.json - security-scan-report.html \ No newline at end of file diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 000000000..e3818b417 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,94 @@ +# OSV-Scanner suppressions for the databricks-jdbc security gate. +# +# Each entry suppresses a CVE that is a documented CPE / ecosystem false +# positive against an artifact we ship. Every entry has a justification. +# +# Trade-off worth noting (reviewer pointed this out): [[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"); the config struct at +# internal/config/config.go has no per-CVE field on PackageOverrides. +# +# Net effect: if a future Maven dep ever legitimately picks up one of +# the CVE IDs below, it will be silently suppressed here. The +# mitigation is that these are all ecosystem-mismatched CVEs (the Go / +# Python / R / C_glib binding of a library, not the Java binding), so a +# legitimate Java affectation would itself be a notable advisory event +# we'd want to revisit -- which would make us re-read these entries +# anyway. Acceptable risk for now. +# +# See google.github.io/osv-scanner/configuration/ for the schema. + +# --- Apache Arrow --- +# Both Arrow CVEs come from the same pattern: an advisory scoped to +# Arrow C++ or Arrow R, matched against the Java arrow-* artifacts via +# the shared "apache:arrow" identifier. Java Arrow is unaffected. + +[[IgnoredVulns]] +id = "CVE-2026-25087" +reason = """ +Use-After-Free in Apache Arrow C++ IPC file reader (variadic buffers). +Does not affect the Java Arrow libraries (arrow-memory-core, +arrow-vector, etc.) which are pure Java with no native C++ code. +""" + +[[IgnoredVulns]] +id = "CVE-2024-52338" +reason = """ +Deserialization vulnerability in the Apache Arrow R package on CRAN +(R 4.0.0 - 16.1.0, fixed in R 17.0.0). Apache advisory explicitly +states it does not affect other Arrow implementations or bindings. +Driver ships Java Arrow 18.3.0 -- wrong ecosystem and outside version +range. See https://www.openwall.com/lists/oss-security/2024/11/28/3 +""" + +# --- gRPC --- + +[[IgnoredVulns]] +id = "CVE-2026-33186" +reason = """ +gRPC-Go server authorization bypass (google.golang.org/grpc, fixed in +1.79.3). The Java gRPC libraries are a separate codebase with +independent release lines. The JDBC driver is also a gRPC client only +-- it does not run a gRPC server, so even the underlying flaw would be +unreachable. +""" + +# --- protobuf-java --- + +[[IgnoredVulns]] +id = "CVE-2026-0994" +reason = """ +Vulnerability in pip protobuf (Python) json_format.ParseDict(). +Advisory record lists only the pip module as affected; no Java fix +exists because Java isn't affected. +""" + +# --- libthrift (non-Java bindings) --- +# Several CVEs in the May 2026 Apache Thrift batch are scoped to +# non-Java bindings. Remaining libthrift CVEs whose binding is +# unspecified or known to affect Java are NOT suppressed and will be +# cleared by a follow-up bump to libthrift 0.23.0. + +[[IgnoredVulns]] +id = "CVE-2025-48431" +reason = "libthrift C_glib (invalid free). Not Java. CPE namespace collision." + +[[IgnoredVulns]] +id = "CVE-2026-41602" +reason = "libthrift Go TFramedTransport. Not Java. CPE namespace collision." + +[[IgnoredVulns]] +id = "CVE-2026-41636" +reason = "libthrift Node.js skip() recursion. Not Java. CPE namespace collision." + +[[IgnoredVulns]] +id = "CVE-2026-43868" +reason = "libthrift Rust memory allocation excess size. Not Java. CPE namespace collision." + +[[IgnoredVulns]] +id = "CVE-2026-43870" +reason = "libthrift Node.js web_server.js multi-vuln. Not Java. CPE namespace collision." diff --git a/owasp-suppressions.xml b/owasp-suppressions.xml index e1c0da23d..ea134e8dd 100644 --- a/owasp-suppressions.xml +++ b/owasp-suppressions.xml @@ -16,4 +16,119 @@ ^pkg:maven/org\.apache\.arrow/.*$ CVE-2026-25087 + + + + + ^pkg:maven/org\.apache\.arrow/.*$ + CVE-2024-52338 + + + + + + ^pkg:maven/io\.grpc/.*$ + CVE-2026-33186 + + + + + + ^pkg:maven/com\.google\.protobuf/.*$ + CVE-2026-0994 + + + + + + ^pkg:maven/org\.apache\.thrift/libthrift@.*$ + CVE-2025-48431 + + + + ^pkg:maven/org\.apache\.thrift/libthrift@.*$ + CVE-2026-41602 + + + + ^pkg:maven/org\.apache\.thrift/libthrift@.*$ + CVE-2026-41636 + + + + ^pkg:maven/org\.apache\.thrift/libthrift@.*$ + CVE-2026-43868 + + + + ^pkg:maven/org\.apache\.thrift/libthrift@.*$ + CVE-2026-43870 + diff --git a/pom.xml b/pom.xml index 1f47342e9..93f56bd4e 100644 --- a/pom.xml +++ b/pom.xml @@ -61,6 +61,7 @@ 0.8.11 2.39.0 3.6.1 + 2.9.1 3.3.3 @@ -171,6 +172,11 @@ build-helper-maven-plugin ${build-helper-maven-plugin.version} + + org.cyclonedx + cyclonedx-maven-plugin + ${cyclonedx-maven-plugin.version} + @@ -203,6 +209,38 @@ + + + org.cyclonedx + cyclonedx-maven-plugin + + + aggregate-bom + package + + makeAggregateBom + + + json + 1.5 + + false + + + +