From 0f9e8f3d2ba3d9380c523b4e8b45023dd1e33220 Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 20 May 2026 09:38:32 +0000 Subject: [PATCH 1/5] Unify weekly + per-PR security scanning into a single workflow Replaces vulnerabilityCatcher.yml (weekly-only) with securityScan.yml, which runs two scanners with complementary coverage across two trigger modes: * On every pull_request to main (pr-scan job): fails the job on any unsuppressed CVSS >= 7 finding. Not yet required-to-merge; once the outstanding libthrift and bouncycastle findings are cleared, flip the check to required. * On the existing weekly schedule + workflow_dispatch (weekly-scan job): emails on findings. Backstops the PR gate by catching CVEs newly filed against unchanged code. Fixes the broken notification chain -- previously the OWASP maven step exited non-zero on findings, which short-circuited the Send Email step so notifications only fired when the scan was clean (i.e. never when they mattered). Scanner stack: * OWASP dependency-check (NVD CPE-based). Existing setup; reuses owasp-suppressions.xml and the failBuildOnCVSS=7 threshold from jdbc-core/pom.xml. * OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates GHSA/NVD/PyPA/RustSec). Catches advisories with no NVD CPE entry that OWASP misses. Verified to catch CVE-2025-66566 in at.yawk.lz4:lz4-java and CVE-2026-5598 (severity 8.9) in bouncycastle, both invisible to OWASP. 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 for the project's own GA). Adds: * `.github/workflows/securityScan.yml` -- the new unified workflow. * `osv-scanner.toml` -- OSV suppressions mirroring the OWASP ones. * Eight new entries in `owasp-suppressions.xml` for documented CPE/ecosystem false positives: Arrow R-only (CVE-2024-52338), gRPC-Go-only (CVE-2026-33186), protobuf Python-only (CVE-2026-0994), and five libthrift non-Java-binding CVEs (C_glib/Go/Node/Rust). Each has a justification comment block. * `cyclonedx-maven-plugin` 2.9.1 in the parent pom, bound to the package phase with skipNotDeployed=false so it runs on the non-deployed parent and jdbc-core modules. Removes: * `.github/workflows/vulnerabilityCatcher.yml` -- superseded by securityScan.yml's weekly-scan job. Local verification: * `mvn package -DskipTests -Ddependency-check.skip=true` produces `target/bom.json` with the expected resolved tree. * OWASP scan shows the 4 unsuppressed libthrift CVEs (the only real Java-applicable findings; will be cleared by the libthrift 0.23 bump follow-up). * OSV scan shows 2 unsuppressed CVSS>=7 findings: bouncycastle CVE-2026-5598 (separate small follow-up) and the same libthrift cluster. Both scanners' findings are listed as follow-up work in NEXT_CHANGELOG-adjacent tracking. This PR's own CI is expected to fail until those follow-ups land, which is fine because the gate isn't required-to-merge yet. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala --- .github/workflows/securityScan.yml | 323 +++++++++++++++++++++ .github/workflows/vulnerabilityCatcher.yml | 125 -------- osv-scanner.toml | 81 ++++++ owasp-suppressions.xml | 115 ++++++++ pom.xml | 38 +++ 5 files changed, 557 insertions(+), 125 deletions(-) create mode 100644 .github/workflows/securityScan.yml delete mode 100644 .github/workflows/vulnerabilityCatcher.yml create mode 100644 osv-scanner.toml diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml new file mode 100644 index 000000000..6b3cc9354 --- /dev/null +++ b/.github/workflows/securityScan.yml @@ -0,0 +1,323 @@ +name: Security Scan + +# Single source of truth for dependency vulnerability scanning. Replaces the +# previous vulnerabilityCatcher.yml (weekly-only) workflow. Runs two scanners +# with complementary coverage in two trigger modes: +# +# - On every pull_request to main (the `pr-scan` job): fails the job if +# either scanner reports an unsuppressed CVSS >= 7 finding. Not yet +# marked required-to-merge; once the outstanding libthrift and +# bouncycastle findings are cleared, flip to required. +# +# - On a weekly schedule + workflow_dispatch (the `weekly-scan` job): +# sends an email notification on any unsuppressed CVSS >= 7 finding. +# Backstops the PR gate by catching CVEs newly filed against unchanged +# code. +# +# Scanners: +# +# - OWASP dependency-check (NVD CPE-based). Existing setup; reuses +# owasp-suppressions.xml and the 7 +# threshold from jdbc-core/pom.xml. +# +# - OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates +# GHSA/NVD/PyPA/RustSec). Catches advisories with no NVD CPE entry -- +# e.g. CVE-2025-66566 in at.yawk.lz4:lz4-java and CVE-2026-5598 in +# bouncycastle, both invisible to OWASP. 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. +# +# Suppression files (keep in sync): +# +# - owasp-suppressions.xml -- consumed by OWASP. +# - osv-scanner.toml -- consumed by OSV. +# +# Both files have justification comments per entry. + +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: + + # ---------------------------------------------------------------------------- + # PR scan: runs on pull_request + workflow_dispatch (so authors and reviewers + # can manually rerun against a branch). Fails the job on findings; reviewers + # see a red X in the PR's checks list. + # ---------------------------------------------------------------------------- + pr-scan: + name: PR Security Scan + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + 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). + - 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 OWASP runs as its own + # explicit step below. + - name: Build (generates cyclonedx SBOM) + run: mvn package -DskipTests -Ddependency-check.skip=true -B + + - name: Run OWASP Dependency Check + id: owasp + run: | + mvn -pl jdbc-core org.owasp:dependency-check-maven:check \ + -Dnvd.api.key=${{ secrets.NVD_API_KEY }} + + - name: Install osv-scanner + if: always() + 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 + id: osv + if: always() + run: | + set -uo pipefail + # Scan only the aggregate SBOM at the repo root. Per-module SBOMs + # would just repeat findings for shared transitives; the aggregate + # is the de-duped view. + /tmp/osv-scanner scan source \ + --recursive=false \ + --config=osv-scanner.toml \ + --format=json \ + --output-file=/tmp/osv-out.json \ + target/bom.json || true + + # OSV-Scanner's exit code isn't severity-aware; filter to >=7 here. + HIGH_FINDINGS=$(jq '[ + .results[].packages[]? | + .package as $pkg | + .groups[]? | + select((.max_severity | tonumber? // 0) >= 7) | + {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: .max_severity} + ]' /tmp/osv-out.json) + + COUNT=$(echo "$HIGH_FINDINGS" | jq 'length') + if [ "$COUNT" -gt 0 ]; then + echo "::error::OSV-Scanner found $COUNT unsuppressed CVSS>=7 finding(s):" + echo "$HIGH_FINDINGS" | jq -r '.[] | " - \(.pkg) | \(.ids | join(",")) | severity \(.severity)"' + echo "" + echo "Fix by:" + echo " 1. Bumping the affected dependency to a patched version, or" + echo " 2. Adding a documented suppression entry to osv-scanner.toml" + echo " AND owasp-suppressions.xml (keep both files in sync)." + exit 1 + fi + + echo "OSV-Scanner: no unsuppressed CVSS>=7 findings." + + - name: Upload OWASP report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pr-security-owasp-report + path: | + jdbc-core/target/dependency-check-report.html + jdbc-core/target/dependency-check-report.json + if-no-files-found: ignore + + - name: Upload OSV report + SBOM + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: pr-security-osv-report + path: | + /tmp/osv-out.json + target/bom.json + if-no-files-found: ignore + + # ---------------------------------------------------------------------------- + # Weekly scan: runs on cron + workflow_dispatch. Backstops the PR gate by + # catching CVEs newly filed against unchanged code. Notifies via email. + # ---------------------------------------------------------------------------- + weekly-scan: + name: Weekly Security Scan + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + 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 + + 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 + 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 + + - name: Run OWASP Dependency Check + # The maven plugin exits non-zero when CVSS >= 7 findings exist + # (`7` in jdbc-core/pom.xml). Without + # continue-on-error, that non-zero exit short-circuits the rest of the + # job and the Send Email step never runs -- which means the weekly + # notification only fires when the scan is clean. Let this step "pass" + # so the explicit `Check for vulnerabilities` step below decides whether + # to alert and fail the job. + continue-on-error: true + 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 + # Always upload, even when findings cause the job to fail. The HTML/JSON + # reports are the primary artifact recipients use to triage findings. + if: always() + 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 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..809eca3b6 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,81 @@ +# OSV-Scanner suppressions for the databricks-jdbc security gate. +# +# Mirror of owasp-suppressions.xml. Each entry suppresses a CVE that is a +# documented CPE / ecosystem false positive against an artifact we ship. +# When you add or remove an entry here, mirror the same change in +# owasp-suppressions.xml so the two scanners report the same set of +# findings on every PR. +# +# See google.github.io/osv-scanner/configuration/ for schema. + +# --- Apache Arrow --- +# Both Arrow CVEs below come from the same CPE collision 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. False positive via the shared +apache:arrow CPE/identifier namespace. +""" + +[[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 (io.grpc:grpc-api / grpc-context / +grpc-stub) are a separate codebase with independent release lines and are +not affected. The JDBC driver is a gRPC client only -- it does not run a +gRPC server and has no server-side attack surface. +""" + +# --- 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. False positive via the +google:protobuf CPE namespace. +""" + +# --- libthrift (non-Java bindings) --- +# Several CVEs in the May 2026 Apache Thrift advisory batch are scoped to +# non-Java bindings (Go, Node.js, C_glib, Rust). They get matched against +# the Java libthrift jar via the shared apache:thrift identifier. +# 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 + + + + From 7d015d1fb679cc0de757240c48611e7f2f78d872 Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 20 May 2026 09:47:27 +0000 Subject: [PATCH 2/5] Collapse security scan into a single job with event-gated terminal steps The previous workflow had two parallel jobs (`pr-scan` and `weekly-scan`) that duplicated the JFrog OIDC + maven config + OWASP setup. Restructure into a single `security-scan` job that runs the same scan for all triggers, then branches at the terminal steps: - pull_request: exits non-zero on findings; reviewers see the red X. - schedule (and workflow_dispatch): composes + sends the email notification, then exits non-zero. Adds: - A `Collect findings` step that parses both scanners' JSON output into a single set of job outputs (owasp_count, osv_count, has_findings). The terminal steps consume these outputs instead of re-parsing reports. Also writes a markdown summary to GITHUB_STEP_SUMMARY so findings are visible in the run UI without downloading artifacts. - NVD database caching keyed on date. Cuts ~2 minutes off OWASP's runtime by avoiding the full ~150 MB NVD feed download on every run. The restore-key prefix means we always restore the most-recent cache; OWASP's own auto-update logic keeps the data fresh. Fixes a bug in my previous version: the OWASP findings check was grepping the HTML report for "CVSS score >= 7", which is not actually present in the HTML output. Read the JSON report directly instead and count vulnerabilities with cvssv3.baseScore >= 7. (The OWASP plugin suppresses suppressed CVEs from the report entirely, so the count naturally reflects unsuppressed findings.) Co-authored-by: Isaac Signed-off-by: Vikrant Puppala --- .github/workflows/securityScan.yml | 321 +++++++++++++---------------- 1 file changed, 149 insertions(+), 172 deletions(-) diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml index 6b3cc9354..53d251944 100644 --- a/.github/workflows/securityScan.yml +++ b/.github/workflows/securityScan.yml @@ -1,32 +1,27 @@ name: Security Scan -# Single source of truth for dependency vulnerability scanning. Replaces the -# previous vulnerabilityCatcher.yml (weekly-only) workflow. Runs two scanners -# with complementary coverage in two trigger modes: +# Single workflow, single job. Triggered three ways: # -# - On every pull_request to main (the `pr-scan` job): fails the job if -# either scanner reports an unsuppressed CVSS >= 7 finding. Not yet -# marked required-to-merge; once the outstanding libthrift and -# bouncycastle findings are cleared, flip to required. +# - pull_request to main: fail the job on any unsuppressed CVSS >= 7 +# finding. Not yet required-to-merge in branch protection. +# - cron (weekly): same scan; on findings, send the existing email +# notification and fail the job. +# - workflow_dispatch: behaves like the cron run (sends email). # -# - On a weekly schedule + workflow_dispatch (the `weekly-scan` job): -# sends an email notification on any unsuppressed CVSS >= 7 finding. -# Backstops the PR gate by catching CVEs newly filed against unchanged -# code. +# Two scanners run on every invocation: # -# Scanners: -# -# - OWASP dependency-check (NVD CPE-based). Existing setup; reuses -# owasp-suppressions.xml and the 7 -# threshold from jdbc-core/pom.xml. +# - OWASP dependency-check (NVD CPE-based). Reuses owasp-suppressions.xml +# and the 7 threshold from +# jdbc-core/pom.xml. The maven plugin exits non-zero on findings, so +# continue-on-error is set on that step; the explicit findings step +# below is what decides whether to alert. # # - OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates # GHSA/NVD/PyPA/RustSec). Catches advisories with no NVD CPE entry -- # e.g. CVE-2025-66566 in at.yawk.lz4:lz4-java and CVE-2026-5598 in # bouncycastle, both invisible to OWASP. 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. +# local dependency tree. # # Suppression files (keep in sync): # @@ -47,15 +42,8 @@ permissions: contents: read jobs: - - # ---------------------------------------------------------------------------- - # PR scan: runs on pull_request + workflow_dispatch (so authors and reviewers - # can manually rerun against a branch). Fails the job on findings; reviewers - # see a red X in the PR's checks list. - # ---------------------------------------------------------------------------- - pr-scan: - name: PR Security Scan - if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + security-scan: + name: Security Scan runs-on: group: databricks-protected-runner-group labels: linux-ubuntu-latest @@ -71,7 +59,27 @@ jobs: distribution: 'temurin' cache: maven - # JFrog OIDC + maven proxy: skipped on fork PRs (no OIDC token). + # Cache OWASP's NVD vulnerability database (~150 MB of CVE feeds). + # Without this, dependency-check downloads it fresh on every run, + # which adds ~2 minutes. The restore-key prefix means we always + # restore the most recent cache; OWASP itself decides whether to + # incrementally update the data on top (default: every 4 hours), so + # we get fresh CVE data without paying the full download each time. + - name: Compute NVD cache key + id: nvd-key + run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + + - name: Cache NVD database + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 + with: + path: ~/.m2/repository/org/owasp/dependency-check-data + key: nvd-${{ runner.os }}-${{ steps.nvd-key.outputs.date }} + restore-keys: | + nvd-${{ runner.os }}- + + # JFrog OIDC + maven proxy. Skipped on fork PRs (no OIDC token); the + # build will fall through to Maven Central directly, which is slower + # but still works for read-only resolution. - name: Get JFrog OIDC token if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository run: | @@ -119,20 +127,23 @@ jobs: EOF - # Build the project to produce the cyclonedx aggregate SBOM that OSV will - # scan. -Ddependency-check.skip=true because OWASP runs as its own - # explicit step below. + # Build the project to produce the cyclonedx aggregate SBOM that OSV + # will scan. -Ddependency-check.skip=true because OWASP runs as its + # own explicit step below. - name: Build (generates cyclonedx SBOM) run: mvn package -DskipTests -Ddependency-check.skip=true -B - name: Run OWASP Dependency Check - id: owasp + # failBuildOnCVSS=7 in jdbc-core/pom.xml makes this step exit + # non-zero on findings. Let it pass; the findings step below + # decides whether to alert (and is the single source of truth for + # both PR-fail and weekly-email branches). + continue-on-error: true run: | mvn -pl jdbc-core org.owasp:dependency-check-maven:check \ -Dnvd.api.key=${{ secrets.NVD_API_KEY }} - name: Install osv-scanner - if: always() run: | set -euo pipefail curl -fsSL -o /tmp/osv-scanner \ @@ -141,13 +152,8 @@ jobs: /tmp/osv-scanner --version - name: Run OSV-Scanner - id: osv - if: always() run: | set -uo pipefail - # Scan only the aggregate SBOM at the repo root. Per-module SBOMs - # would just repeat findings for shared transitives; the aggregate - # is the de-duped view. /tmp/osv-scanner scan source \ --recursive=false \ --config=osv-scanner.toml \ @@ -155,149 +161,111 @@ jobs: --output-file=/tmp/osv-out.json \ target/bom.json || true - # OSV-Scanner's exit code isn't severity-aware; filter to >=7 here. - HIGH_FINDINGS=$(jq '[ - .results[].packages[]? | - .package as $pkg | - .groups[]? | - select((.max_severity | tonumber? // 0) >= 7) | - {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: .max_severity} - ]' /tmp/osv-out.json) + # Single source of truth for "are there findings?". Combines OWASP's + # HTML report (grep) and OSV's JSON (jq, filtered to CVSS >= 7). + # Sets job outputs that the terminal steps below consume. + - name: Collect findings + id: findings + run: | + set -uo pipefail - COUNT=$(echo "$HIGH_FINDINGS" | jq 'length') - if [ "$COUNT" -gt 0 ]; then - echo "::error::OSV-Scanner found $COUNT unsuppressed CVSS>=7 finding(s):" - echo "$HIGH_FINDINGS" | jq -r '.[] | " - \(.pkg) | \(.ids | join(",")) | severity \(.severity)"' - echo "" - echo "Fix by:" - echo " 1. Bumping the affected dependency to a patched version, or" - echo " 2. Adding a documented suppression entry to osv-scanner.toml" - echo " AND owasp-suppressions.xml (keep both files in sync)." - exit 1 + # --- OWASP findings --- + # Read the JSON report directly. Suppressed CVEs are omitted from + # the report by OWASP itself, so this counts only real findings. + OWASP_COUNT=0 + if [ -f jdbc-core/target/dependency-check-report.json ]; then + OWASP_COUNT=$(jq '[ + .dependencies[]? | + .vulnerabilities[]? | + select((.cvssv3.baseScore // .cvssv2.score // 0) >= 7) + ] | length' jdbc-core/target/dependency-check-report.json) + fi + OWASP_HIGH=false + if [ "$OWASP_COUNT" -gt 0 ]; then + OWASP_HIGH=true fi - echo "OSV-Scanner: no unsuppressed CVSS>=7 findings." - - - name: Upload OWASP report - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: pr-security-owasp-report - path: | - jdbc-core/target/dependency-check-report.html - jdbc-core/target/dependency-check-report.json - if-no-files-found: ignore - - - name: Upload OSV report + SBOM - if: always() - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: pr-security-osv-report - path: | - /tmp/osv-out.json - target/bom.json - if-no-files-found: ignore - - # ---------------------------------------------------------------------------- - # Weekly scan: runs on cron + workflow_dispatch. Backstops the PR gate by - # catching CVEs newly filed against unchanged code. Notifies via email. - # ---------------------------------------------------------------------------- - weekly-scan: - name: Weekly Security Scan - if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' - 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 + # --- OSV findings (filter to CVSS >= 7) --- + OSV_HIGH_JSON='[]' + if [ -f /tmp/osv-out.json ]; then + OSV_HIGH_JSON=$(jq -c '[ + .results[].packages[]? | + .package as $pkg | + .groups[]? | + select((.max_severity | tonumber? // 0) >= 7) | + {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: .max_severity} + ]' /tmp/osv-out.json) + fi + OSV_COUNT=$(echo "$OSV_HIGH_JSON" | jq 'length') + OSV_HIGH=false + if [ "$OSV_COUNT" -gt 0 ]; then + OSV_HIGH=true + fi - 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}" + # --- Combined verdict --- + HAS_FINDINGS=false + if [ "$OWASP_HIGH" = "true" ] || [ "$OSV_HIGH" = "true" ]; then + HAS_FINDINGS=true + fi - 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}" + echo "owasp_count=$OWASP_COUNT" >> "$GITHUB_OUTPUT" + echo "osv_count=$OSV_COUNT" >> "$GITHUB_OUTPUT" + echo "has_findings=$HAS_FINDINGS" >> "$GITHUB_OUTPUT" - if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then - echo "FAIL: Could not extract JFrog access token" - exit 1 + # Log to job summary so it's visible without downloading artifacts. + { + echo "## Security Scan Findings" + echo "" + echo "- OWASP CVSS>=7 findings: \`$OWASP_COUNT\` (see uploaded HTML report for details)" + echo "- OSV CVSS>=7 findings: \`$OSV_COUNT\`" + if [ "$OSV_COUNT" -gt 0 ]; then + echo "" + echo "OSV findings:" + echo "$OSV_HIGH_JSON" | jq -r '.[] | "- \(.pkg) | \(.ids | join(",")) | severity \(.severity)"' + fi + } >> "$GITHUB_STEP_SUMMARY" + + if [ "$HAS_FINDINGS" = "true" ]; then + echo "::warning::Unsuppressed CVSS>=7 findings detected. See job summary." + else + echo "No unsuppressed CVSS>=7 findings." fi - echo "JFROG_ACCESS_TOKEN=${ACCESS_TOKEN}" >> "$GITHUB_ENV" - - - name: Configure maven + # --- Terminal: PR event --- + # Fail the job so the PR's check goes red. No email; reviewers see + # the ❌ in the PR conversation and dig into the artifacts/summary. + - name: Fail on findings (PR) + if: github.event_name == 'pull_request' && steps.findings.outputs.has_findings == 'true' 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 - - - name: Run OWASP Dependency Check - # The maven plugin exits non-zero when CVSS >= 7 findings exist - # (`7` in jdbc-core/pom.xml). Without - # continue-on-error, that non-zero exit short-circuits the rest of the - # job and the Send Email step never runs -- which means the weekly - # notification only fires when the scan is clean. Let this step "pass" - # so the explicit `Check for vulnerabilities` step below decides whether - # to alert and fail the job. - continue-on-error: true - 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 + echo "::error::Unsuppressed CVSS>=7 finding(s) in this PR." + echo "" + echo "Fix by either:" + echo " 1. Bumping the affected dependency to a patched version, or" + echo " 2. Adding a documented suppression entry to BOTH" + echo " owasp-suppressions.xml AND osv-scanner.toml (keep them in sync)." + exit 1 + + # --- Terminal: scheduled/manual event --- + # On the weekly cron (and manual workflow_dispatch reruns), send the + # existing email notification and fail the job. Uses the same SMTP + # secrets as the old vulnerabilityCatcher.yml. + - name: Compose email body + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.has_findings == 'true' 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 + { + echo "JDBC Driver Security Scan Results" + echo "

Security Vulnerabilities Found

" + echo "

Unsuppressed CVSS >= 7 finding(s) detected in the weekly scan of the JDBC driver.

" + echo "
    " + echo "
  • OWASP findings: ${{ steps.findings.outputs.owasp_count }}
  • " + echo "
  • OSV findings: ${{ steps.findings.outputs.osv_count }}
  • " + echo "
" + echo "

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

" + echo "" + } > security-scan-report.html - name: Send Email - if: steps.check_vulnerabilities.outputs.has_vulnerabilities == 'true' + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.has_findings == 'true' uses: dawidd6/action-send-mail@4226df7daafa6fc901a43789c49bf7ab309066e7 # v3 with: server_address: smtp.gmail.com @@ -310,9 +278,15 @@ jobs: from: JDBC Security Scanner content_type: text/html - - name: Upload Report as Artifact - # Always upload, even when findings cause the job to fail. The HTML/JSON - # reports are the primary artifact recipients use to triage findings. + - name: Fail on findings (scheduled/manual) + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.has_findings == 'true' + run: | + echo "::error::Unsuppressed CVSS>=7 finding(s) on main. 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: @@ -320,4 +294,7 @@ jobs: path: | jdbc-core/target/dependency-check-report.html jdbc-core/target/dependency-check-report.json + /tmp/osv-out.json + target/bom.json security-scan-report.html + if-no-files-found: ignore From c1da4ab44b420a8c291351686e6016bb6a10bffd Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 20 May 2026 10:30:07 +0000 Subject: [PATCH 3/5] Drop OWASP from security scan; OSV is the sole gate Removes the OWASP step, NVD database cache, and OWASP findings parsing from securityScan.yml. OSV-Scanner now drives the gate alone. Motivation: - OSV's database is a strict superset of NVD's (federates GHSA, NVD, PyPA, RustSec, Go vuln DB). We've already seen two real CVEs OSV catches that OWASP misses entirely -- CVE-2025-66566 in lz4 and CVE-2026-5598 in bouncycastle, both GHSA-only with no NVD CPE. - The release pipeline at databricks/secure-public-registry-releases-eng already skips OWASP (every Maven step has -Ddependency-check.skip=true) and runs its own artifact-level scan via databricks/gh-action-scan. So removing OWASP from this repo's CI does not disturb release gating. - OWASP's NVD database (~150-700 MB), the NVD_API_KEY secret, and the cache machinery added minutes per run for coverage we don't actually use any more. The OWASP plugin block stays in jdbc-core/pom.xml and owasp-suppressions.xml stays in the repo, because the disabled-anyway in-repo release.yml / release-thin.yml workflows still reference them. Cleaning those up is a separate change. Also addresses these from the PR review: #1 (SBOM existence guard): the OSV step now explicitly checks target/bom.json exists before scanning, and that osv-scanner produced a non-empty output. Build failures now surface with a clear ::error:: instead of a downstream jq error. #3 (set -uo pipefail without -e): added a comment explaining that -e is dropped intentionally because osv-scanner exits 1 on any finding regardless of severity; the severity >= 7 filter below is the real gate. #4 (severity filter silently treats malformed scores as 0): the Collect findings step now logs total_findings alongside high_count, so a mismatch (e.g. "10 total / 0 high") would be visible in the step summary and warn ops about OSV format drift. #5 (suppressions are CVE-id global, not package-scoped): OSV-Scanner v2.3.8 does not actually support per-package CVE-id ignores (verified against internal/config/config.go -- PackageOverrides has no per-CVE field). Documented the trade-off explicitly in osv-scanner.toml's header so future readers understand the scoping model and the acceptable-risk reasoning. #7 (fork PR maven config): added a comment in the workflow explaining that fork PRs work even with JFrog OIDC skipped, because all of the driver's direct dependencies are published to public Maven Central. JFrog is just a mirror, not a source. Verified locally: OSV reports the same 2 unsuppressed CVSS>=7 findings (bouncycastle CVE-2026-5598 + libthrift cluster) as before, with no schema errors against the corrected osv-scanner.toml. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala --- .github/workflows/securityScan.yml | 190 +++++++++++------------------ osv-scanner.toml | 73 ++++++----- 2 files changed, 115 insertions(+), 148 deletions(-) diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml index 53d251944..3bbaf6bb8 100644 --- a/.github/workflows/securityScan.yml +++ b/.github/workflows/securityScan.yml @@ -8,27 +8,23 @@ name: Security Scan # notification and fail the job. # - workflow_dispatch: behaves like the cron run (sends email). # -# Two scanners run on every invocation: +# 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. # -# - OWASP dependency-check (NVD CPE-based). Reuses owasp-suppressions.xml -# and the 7 threshold from -# jdbc-core/pom.xml. The maven plugin exits non-zero on findings, so -# continue-on-error is set on that step; the explicit findings step -# below is what decides whether to alert. +# 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. # -# - OSV-Scanner v2.3.8 (purl-based via OSV.dev; federates -# GHSA/NVD/PyPA/RustSec). Catches advisories with no NVD CPE entry -- -# e.g. CVE-2025-66566 in at.yawk.lz4:lz4-java and CVE-2026-5598 in -# bouncycastle, both invisible to OWASP. Reads the cyclonedx aggregate -# SBOM produced by `mvn package` so it sees the actually-resolved -# local dependency tree. -# -# Suppression files (keep in sync): -# -# - owasp-suppressions.xml -- consumed by OWASP. -# - osv-scanner.toml -- consumed by OSV. -# -# Both files have justification comments per entry. +# 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: @@ -59,27 +55,12 @@ jobs: distribution: 'temurin' cache: maven - # Cache OWASP's NVD vulnerability database (~150 MB of CVE feeds). - # Without this, dependency-check downloads it fresh on every run, - # which adds ~2 minutes. The restore-key prefix means we always - # restore the most recent cache; OWASP itself decides whether to - # incrementally update the data on top (default: every 4 hours), so - # we get fresh CVE data without paying the full download each time. - - name: Compute NVD cache key - id: nvd-key - run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" - - - name: Cache NVD database - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4 - with: - path: ~/.m2/repository/org/owasp/dependency-check-data - key: nvd-${{ runner.os }}-${{ steps.nvd-key.outputs.date }} - restore-keys: | - nvd-${{ runner.os }}- - - # JFrog OIDC + maven proxy. Skipped on fork PRs (no OIDC token); the - # build will fall through to Maven Central directly, which is slower - # but still works for read-only resolution. + # 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: | @@ -128,21 +109,12 @@ jobs: EOF # Build the project to produce the cyclonedx aggregate SBOM that OSV - # will scan. -Ddependency-check.skip=true because OWASP runs as its - # own explicit step below. + # 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: Run OWASP Dependency Check - # failBuildOnCVSS=7 in jdbc-core/pom.xml makes this step exit - # non-zero on findings. Let it pass; the findings step below - # decides whether to alert (and is the single source of truth for - # both PR-fail and weekly-email branches). - continue-on-error: true - run: | - mvn -pl jdbc-core org.owasp:dependency-check-maven:check \ - -Dnvd.api.key=${{ secrets.NVD_API_KEY }} - - name: Install osv-scanner run: | set -euo pipefail @@ -152,8 +124,17 @@ 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`. 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 \ @@ -161,105 +142,80 @@ jobs: --output-file=/tmp/osv-out.json \ target/bom.json || true - # Single source of truth for "are there findings?". Combines OWASP's - # HTML report (grep) and OSV's JSON (jq, filtered to CVSS >= 7). - # Sets job outputs that the terminal steps below consume. + 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. - name: Collect findings id: findings run: | set -uo pipefail - # --- OWASP findings --- - # Read the JSON report directly. Suppressed CVEs are omitted from - # the report by OWASP itself, so this counts only real findings. - OWASP_COUNT=0 - if [ -f jdbc-core/target/dependency-check-report.json ]; then - OWASP_COUNT=$(jq '[ - .dependencies[]? | - .vulnerabilities[]? | - select((.cvssv3.baseScore // .cvssv2.score // 0) >= 7) - ] | length' jdbc-core/target/dependency-check-report.json) - fi - OWASP_HIGH=false - if [ "$OWASP_COUNT" -gt 0 ]; then - OWASP_HIGH=true - fi - - # --- OSV findings (filter to CVSS >= 7) --- - OSV_HIGH_JSON='[]' - if [ -f /tmp/osv-out.json ]; then - OSV_HIGH_JSON=$(jq -c '[ - .results[].packages[]? | - .package as $pkg | - .groups[]? | - select((.max_severity | tonumber? // 0) >= 7) | - {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: .max_severity} - ]' /tmp/osv-out.json) - fi - OSV_COUNT=$(echo "$OSV_HIGH_JSON" | jq 'length') - OSV_HIGH=false - if [ "$OSV_COUNT" -gt 0 ]; then - OSV_HIGH=true - fi + # Total findings ignores severity; high findings filter to >=7. + # Log both 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). + TOTAL_FINDINGS=$(jq '[.results[].packages[]? | .groups[]?] | length' /tmp/osv-out.json) + HIGH_FINDINGS=$(jq -c '[ + .results[].packages[]? | + .package as $pkg | + .groups[]? | + select((.max_severity | tonumber? // 0) >= 7) | + {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: .max_severity} + ]' /tmp/osv-out.json) + HIGH_COUNT=$(echo "$HIGH_FINDINGS" | jq 'length') - # --- Combined verdict --- HAS_FINDINGS=false - if [ "$OWASP_HIGH" = "true" ] || [ "$OSV_HIGH" = "true" ]; then + if [ "$HIGH_COUNT" -gt 0 ]; then HAS_FINDINGS=true fi - echo "owasp_count=$OWASP_COUNT" >> "$GITHUB_OUTPUT" - echo "osv_count=$OSV_COUNT" >> "$GITHUB_OUTPUT" + echo "total_findings=$TOTAL_FINDINGS" >> "$GITHUB_OUTPUT" + echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT" echo "has_findings=$HAS_FINDINGS" >> "$GITHUB_OUTPUT" - # Log to job summary so it's visible without downloading artifacts. + # Step summary so findings are visible in the GH Actions UI + # without downloading artifacts. { - echo "## Security Scan Findings" + echo "## OSV-Scanner Findings" echo "" - echo "- OWASP CVSS>=7 findings: \`$OWASP_COUNT\` (see uploaded HTML report for details)" - echo "- OSV CVSS>=7 findings: \`$OSV_COUNT\`" - if [ "$OSV_COUNT" -gt 0 ]; then + echo "- Total findings (any severity): \`$TOTAL_FINDINGS\`" + echo "- High findings (CVSS >= 7): \`$HIGH_COUNT\`" + if [ "$HIGH_COUNT" -gt 0 ]; then echo "" - echo "OSV findings:" - echo "$OSV_HIGH_JSON" | jq -r '.[] | "- \(.pkg) | \(.ids | join(",")) | severity \(.severity)"' + echo "Unsuppressed CVSS>=7 findings:" + echo "$HIGH_FINDINGS" | jq -r '.[] | "- \(.pkg) | \(.ids | join(",")) | severity \(.severity)"' fi } >> "$GITHUB_STEP_SUMMARY" - if [ "$HAS_FINDINGS" = "true" ]; then - echo "::warning::Unsuppressed CVSS>=7 findings detected. See job summary." - else - echo "No unsuppressed CVSS>=7 findings." - fi + echo "OSV: $TOTAL_FINDINGS total findings, $HIGH_COUNT at CVSS>=7" # --- Terminal: PR event --- - # Fail the job so the PR's check goes red. No email; reviewers see - # the ❌ in the PR conversation and dig into the artifacts/summary. + # Fail the job so the PR's check goes red. No email. - name: Fail on findings (PR) if: github.event_name == 'pull_request' && steps.findings.outputs.has_findings == 'true' run: | - echo "::error::Unsuppressed CVSS>=7 finding(s) in this PR." + echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed CVSS>=7 finding(s) in this PR. See the step summary or downloaded artifacts." echo "" echo "Fix by either:" echo " 1. Bumping the affected dependency to a patched version, or" - echo " 2. Adding a documented suppression entry to BOTH" - echo " owasp-suppressions.xml AND osv-scanner.toml (keep them in sync)." + 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." exit 1 # --- Terminal: scheduled/manual event --- - # On the weekly cron (and manual workflow_dispatch reruns), send the - # existing email notification and fail the job. Uses the same SMTP - # secrets as the old vulnerabilityCatcher.yml. - name: Compose email body if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.has_findings == 'true' run: | { echo "JDBC Driver Security Scan Results" echo "

Security Vulnerabilities Found

" - echo "

Unsuppressed CVSS >= 7 finding(s) detected in the weekly scan of the JDBC driver.

" - echo "
    " - echo "
  • OWASP findings: ${{ steps.findings.outputs.owasp_count }}
  • " - echo "
  • OSV findings: ${{ steps.findings.outputs.osv_count }}
  • " - echo "
" + echo "

${{ steps.findings.outputs.high_count }} unsuppressed CVSS >= 7 finding(s) detected in the weekly scan of the JDBC driver." + echo "(Total OSV findings at any severity: ${{ steps.findings.outputs.total_findings }}.)

" echo "

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

" echo "" } > security-scan-report.html @@ -292,8 +248,6 @@ jobs: with: name: security-scan-reports path: | - jdbc-core/target/dependency-check-report.html - jdbc-core/target/dependency-check-report.json /tmp/osv-out.json target/bom.json security-scan-report.html diff --git a/osv-scanner.toml b/osv-scanner.toml index 809eca3b6..e3818b417 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -1,64 +1,77 @@ # OSV-Scanner suppressions for the databricks-jdbc security gate. # -# Mirror of owasp-suppressions.xml. Each entry suppresses a CVE that is a -# documented CPE / ecosystem false positive against an artifact we ship. -# When you add or remove an entry here, mirror the same change in -# owasp-suppressions.xml so the two scanners report the same set of -# findings on every PR. +# Each entry suppresses a CVE that is a documented CPE / ecosystem false +# positive against an artifact we ship. Every entry has a justification. # -# See google.github.io/osv-scanner/configuration/ for schema. +# 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 below come from the same CPE collision 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. +# 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. False positive via the shared -apache:arrow CPE/identifier namespace. +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 +(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 (io.grpc:grpc-api / grpc-context / -grpc-stub) are a separate codebase with independent release lines and are -not affected. The JDBC driver is a gRPC client only -- it does not run a -gRPC server and has no server-side attack surface. +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. False positive via the -google:protobuf CPE namespace. +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 advisory batch are scoped to -# non-Java bindings (Go, Node.js, C_glib, Rust). They get matched against -# the Java libthrift jar via the shared apache:thrift identifier. -# 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. +# 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" From a989bc00a6a0b8ab884620890000415f23a5923e Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 20 May 2026 10:36:18 +0000 Subject: [PATCH 4/5] Split PR gate from weekly report thresholds PR-time and weekly serve different audiences: - PRs are evaluated by code: false positives are expensive because every author hits them. Keep CVSS >= 7 as the gate threshold so we don't block on MEDIUM/LOW noise. - The weekly is read by humans, not enforced. False positives are cheap (one person reads the email, ignores most of it). Report EVERYTHING so emerging MEDIUM risk is visible before it crosses the gate. Changes: - `Collect findings` step now produces an `all_findings` JSON sorted by severity desc (written to /tmp/all-findings.json -- larger than the 1 MB GH Actions output cap allows), plus a `high_count` for the PR gate. `total_findings` covers every finding regardless of severity. - PR terminal step gates on `high_count != '0'` (unchanged semantics; just no longer uses the now-deleted has_findings output). - Weekly email body now lists ALL findings in a styled HTML table sorted by severity desc, with HIGH rows highlighted red and MEDIUM rows highlighted amber. The email and the Fail-on-findings step trigger on `total_findings != '0'`. - Step summary in the Actions UI also lists every finding in a markdown table. Verified locally against the existing /tmp/osv-out.json: previously the gate showed 2 findings; with the new logic it surfaces 4 -- the same 2 HIGH (bouncycastle 8.9, libthrift 7.3) plus 2 MEDIUM bouncycastle CVEs (6.3 and 5.5) that the team would otherwise discover only when one of them later got re-scored above 7. Co-authored-by: Isaac Signed-off-by: Vikrant Puppala --- .github/workflows/securityScan.yml | 107 ++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 33 deletions(-) diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml index 3bbaf6bb8..11784b13c 100644 --- a/.github/workflows/securityScan.yml +++ b/.github/workflows/securityScan.yml @@ -1,12 +1,20 @@ name: Security Scan -# Single workflow, single job. Triggered three ways: +# Single workflow, single job. Triggered three ways with DIFFERENT +# thresholds: # -# - pull_request to main: fail the job on any unsuppressed CVSS >= 7 -# finding. Not yet required-to-merge in branch protection. -# - cron (weekly): same scan; on findings, send the existing email -# notification and fail the job. -# - workflow_dispatch: behaves like the cron run (sends email). +# - 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 @@ -149,34 +157,42 @@ jobs: # 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 - # Total findings ignores severity; high findings filter to >=7. - # Log both 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). - TOTAL_FINDINGS=$(jq '[.results[].packages[]? | .groups[]?] | length' /tmp/osv-out.json) - HIGH_FINDINGS=$(jq -c '[ + # 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[]? | - select((.max_severity | tonumber? // 0) >= 7) | - {pkg: ($pkg.name + "@" + $pkg.version), ids: .ids, severity: .max_severity} - ]' /tmp/osv-out.json) + {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') - HAS_FINDINGS=false - if [ "$HIGH_COUNT" -gt 0 ]; then - HAS_FINDINGS=true - 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. + echo "$ALL_FINDINGS" > /tmp/all-findings.json echo "total_findings=$TOTAL_FINDINGS" >> "$GITHUB_OUTPUT" echo "high_count=$HIGH_COUNT" >> "$GITHUB_OUTPUT" - echo "has_findings=$HAS_FINDINGS" >> "$GITHUB_OUTPUT" # Step summary so findings are visible in the GH Actions UI # without downloading artifacts. @@ -184,11 +200,14 @@ jobs: echo "## OSV-Scanner Findings" echo "" echo "- Total findings (any severity): \`$TOTAL_FINDINGS\`" - echo "- High findings (CVSS >= 7): \`$HIGH_COUNT\`" - if [ "$HIGH_COUNT" -gt 0 ]; then + echo "- High findings (CVSS >= 7, PR-blocking): \`$HIGH_COUNT\`" + if [ "$TOTAL_FINDINGS" -gt 0 ]; then echo "" - echo "Unsuppressed CVSS>=7 findings:" - echo "$HIGH_FINDINGS" | jq -r '.[] | "- \(.pkg) | \(.ids | join(",")) | severity \(.severity)"' + 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" @@ -196,8 +215,10 @@ 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. - name: Fail on findings (PR) - if: github.event_name == 'pull_request' && steps.findings.outputs.has_findings == 'true' + if: github.event_name == 'pull_request' && steps.findings.outputs.high_count != '0' run: | echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed CVSS>=7 finding(s) in this PR. See the step summary or downloaded artifacts." echo "" @@ -208,20 +229,40 @@ 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.has_findings == 'true' + 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 "JDBC Driver Security Scan Results" + echo "" echo "

Security Vulnerabilities Found

" - echo "

${{ steps.findings.outputs.high_count }} unsuppressed CVSS >= 7 finding(s) detected in the weekly scan of the JDBC driver." - echo "(Total OSV findings at any severity: ${{ steps.findings.outputs.total_findings }}.)

" + 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.has_findings == 'true' + 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 @@ -235,9 +276,9 @@ jobs: content_type: text/html - name: Fail on findings (scheduled/manual) - if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.has_findings == 'true' + if: (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.findings.outputs.total_findings != '0' run: | - echo "::error::Unsuppressed CVSS>=7 finding(s) on main. Email sent." + 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 From d6f1192ab6c151bc91b482dbe4ce6ecb30356e65 Mon Sep 17 00:00:00 2001 From: Vikrant Puppala Date: Wed, 20 May 2026 10:42:44 +0000 Subject: [PATCH 5/5] Dump CVE/GHSA list inline in PR-fail and Collect-findings steps Before: the PR-fail step printed only a count ("2 unsuppressed CVSS>=7 finding(s) in this PR") and pointed the author at the step summary panel or downloaded artifacts. The Collect-findings step wrote findings to GITHUB_STEP_SUMMARY but not stdout. Authors clicking into the failing run's default "Logs" view had to take a second navigation step to see which CVEs actually triggered the fail. After: both steps echo the findings list to stdout (sorted by severity desc, with [severity] pkg ids format). The PR-fail step shows only HIGH findings (the ones it blocks on); the Collect-findings step shows all findings. The step summary table is unchanged -- it's still the rich rendering -- but the job log now has enough information to act without extra clicks. Example new output on a HIGH-finding fail: ::error:: 2 unsuppressed CVSS>=7 finding(s) in this PR: [8.9] org.bouncycastle:bcprov-jdk18on@1.79 GHSA-p93r-85wp-75v3 [7.3] org.apache.thrift:libthrift@0.19.0 GHSA-7pwc-h2j2-rjgj Fix by either: ... Full step summary: Co-authored-by: Isaac Signed-off-by: Vikrant Puppala --- .github/workflows/securityScan.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/securityScan.yml b/.github/workflows/securityScan.yml index 11784b13c..94de356e7 100644 --- a/.github/workflows/securityScan.yml +++ b/.github/workflows/securityScan.yml @@ -211,7 +211,14 @@ jobs: 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. @@ -220,12 +227,22 @@ jobs: - name: Fail on findings (PR) if: github.event_name == 'pull_request' && steps.findings.outputs.high_count != '0' run: | - echo "::error::${{ steps.findings.outputs.high_count }} unsuppressed CVSS>=7 finding(s) in this PR. See the step summary or downloaded artifacts." + 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 ---