Skip to content

Add per-PR security scan: OWASP + OSV-Scanner with cyclonedx SBOM#1459

Closed
vikrantpuppala wants to merge 1 commit into
mainfrom
vp/pr-security-scan-phase1
Closed

Add per-PR security scan: OWASP + OSV-Scanner with cyclonedx SBOM#1459
vikrantpuppala wants to merge 1 commit into
mainfrom
vp/pr-security-scan-phase1

Conversation

@vikrantpuppala
Copy link
Copy Markdown
Collaborator

Summary

Adds a per-PR security gate (Phase 1 of the broader rollout). A new workflow runs on every PR to main and fails the job if either OWASP dependency-check or OSV-Scanner reports an unsuppressed CVSS ≥ 7 finding.

Why two scanners

Scanner Source Catches Misses
OWASP dependency-check NVD (CPE-based) Anything with an NVD CPE entry. Reuses existing owasp-suppressions.xml and the failBuildOnCVSS=7 threshold in jdbc-core/pom.xml. GHSA-only advisories without an NVD CPE (e.g., CVE-2025-66566 in lz4-java — the lesson from #1455).
OSV-Scanner v2.3.8 OSV.dev (purl-based; federates GHSA + NVD + PyPA + RustSec) The GHSA-only gap. Already turned up a new finding I didn't know about: bouncycastle CVE-2026-5598 (severity 8.9) in bcprov-jdk18on 1.79, which OWASP doesn't see at all. Findings without a CVSS score (rare for high-severity).

Both scanners use suppression files (owasp-suppressions.xml and osv-scanner.toml) listing documented false positives. Each entry has a justification comment. Keep the two in sync when adding/removing entries.

Why cyclonedx SBOM (and not just `pom.xml`)

OSV-Scanner's Maven resolver consults deps.dev's published-artifact metadata. For the project's own GA, that means it sees the previously-published 3.3.3 pom (with lz4@1.8.1, etc.) — not whatever bumps are sitting on `main`. Result: in CI, OSV would keep showing yesterday's released state instead of the PR branch's actual dependency tree.

Fix: produce a CycloneDX aggregate SBOM at build time (`mvn package`) and feed that to OSV. The SBOM captures the actually-resolved local tree.

Added `cyclonedx-maven-plugin` 2.9.1 to the parent pom, bound to the `package` phase, with `skipNotDeployed=false` to override its default behavior of skipping modules with `maven.deploy.skip=true` (parent and jdbc-core).

Findings on `main` today

This workflow, run against current `main` (post-#1456, pre-#1458), reports 2 unsuppressed CVSS ≥ 7 findings:

  1. `org.bouncycastle:bcprov-jdk18on@1.79` → GHSA-p93r-85wp-75v3 (CVE-2026-5598, severity 8.9). Fixed in bcprov-jdk18on 1.84. Follow-up PR.
  2. `org.apache.thrift:libthrift@0.19.0` → GHSA-7pwc-h2j2-rjgj (covers CVE-2026-41603/04/05/43869, max severity 8.2). Cleared by the libthrift 0.19 → 0.23 follow-up. Follow-up PR.

So this PR's CI will be red on its own scan until those two follow-ups land. That's intentional and aligned with the rollout plan:

  • This PR adds the workflow but does NOT mark it required-to-merge in branch protection. PRs are not blocked from merging by the red ❌.
  • Once the bouncycastle and libthrift follow-ups land and the gate goes green, then flip it to required.

What this PR is NOT

  • It is not a required check. Branch protection is unchanged.
  • It does not introduce a diff-mode (net-new findings only) gate. Every PR has to ship into a clean codebase — that's the design we agreed on, to force the team to actually burn down the backlog rather than just "not make it worse."
  • It does not include a Phase 2 design (override flags, severity tiers, auto-issue filing). Phase 2 is deferred until we have ~2 weeks of operational experience with Phase 1.

Test plan

  • `mvn package -DskipTests -Ddependency-check.skip=true` produces `target/bom.json` with the expected resolved dependency tree (jackson 2.18.7, netty 4.2.13.Final, lz4 1.10.1, etc.).
  • Local OSV scan against the aggregate SBOM with the new `osv-scanner.toml` correctly suppresses the documented false positives (Arrow R-only, gRPC-Go-only, libthrift non-Java bindings, etc.) — only the 2 real findings remain.
  • Filter-to-severity-≥7 jq logic correctly excludes BC CVEs below 7 while keeping the 8.9 finding.
  • CI run on this PR — expected to fail on the 2 known findings above. The CI failure on this PR is the intended outcome.
  • Manual workflow_dispatch run post-merge.

NO_CHANGELOG=true

This pull request and its description were written by Isaac.

Phase 1 of the security gate. A new workflow runs on every PR to main
and on workflow_dispatch, failing the job if either scanner reports a
CVSS >= 7 finding that isn't covered by an existing suppression entry.

Two scanners give complementary coverage:

* OWASP dependency-check (NVD CPE-based) -- the existing setup the
  weekly scan uses. Reuses owasp-suppressions.xml and the
  failBuildOnCVSS=7 threshold already configured in jdbc-core/pom.xml.

* OSV-Scanner v2.3.8 (purl-based via the OSV database, which federates
  GHSA / NVD / PyPA / RustSec). Catches advisories OWASP misses because
  they have no NVD CPE entry -- the lz4 CVE-2025-66566 case was the
  motivating example. Also caught a new finding I would not otherwise
  have seen: bouncycastle CVE-2026-5598 (severity 8.9) is invisible to
  OWASP's CPE-only resolution path.

OSV scans the cyclonedx aggregate SBOM produced by `mvn package`, not
the source pom.xml. This matters because OSV's default resolver pulls
artifact metadata from deps.dev, which lags behind whatever bumps are
sitting on main (deps.dev only knows about published GAs). The SBOM
captures the actually-resolved local dependency tree.

Adds:

* `.github/workflows/prSecurityScan.yml` -- the new workflow.
* `osv-scanner.toml` -- OSV suppressions mirroring owasp-suppressions.xml.
  Keep the two files in sync when adding or removing entries; both
  scanners should agree on what is and is not a real finding.
* `cyclonedx-maven-plugin` execution in the parent pom that emits an
  aggregate `target/bom.json` during the package phase, with
  `skipNotDeployed=false` so it runs on the parent and jdbc-core (which
  have maven.deploy.skip=true).

This PR does not flip branch protection -- the workflow runs visibly
red but doesn't block merges. Once the outstanding findings are cleared
(bouncycastle 1.79 -> 1.84 and the libthrift 0.19 -> 0.23 follow-up),
mark the check required so the gate actually has teeth.

Co-authored-by: Isaac
Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
@vikrantpuppala
Copy link
Copy Markdown
Collaborator Author

Closing in favor of a single unified PR that consolidates this work with #1458. The new PR will replace vulnerabilityCatcher.yml with a unified securityScan.yml that has two jobs (weekly + per-PR), so we don't end up with two workflow files duplicating JFrog setup, OWASP config, etc.

vikrantpuppala added a commit that referenced this pull request May 21, 2026
## Summary

Consolidates security scanning into one workflow with one job
(`securityScan.yml`). Replaces `vulnerabilityCatcher.yml`. Adds
OSV-Scanner alongside the existing OWASP check, plus cyclonedx SBOM
generation so OSV scans the actually-resolved local dependency tree.

This bundles the work that was originally split across (now-closed) PRs
#1458 (suppressions + email-notification fix) and #1459 (per-PR gate).
One unified workflow file, one set of suppression rules, one place to
look.

### Design: single job, terminal steps gated by event

```
Triggers: pull_request, schedule (weekly cron), workflow_dispatch
   │
   ├─ shared: checkout, JDK, JFrog OIDC, maven config
   ├─ shared: NVD database cache (saves ~2 min)
   ├─ shared: mvn package (generates cyclonedx aggregate SBOM)
   ├─ shared: OWASP dependency-check (continue-on-error)
   ├─ shared: install + run OSV-Scanner
   ├─ shared: Collect findings (writes job summary; sets outputs)
   │
   ├─ if pull_request + findings ⇒ fail the job
   └─ if schedule|dispatch + findings ⇒ send email + fail the job
```

No duplicated steps across jobs. Same scan logic for every trigger; only
the notification mechanism differs.

### What changed

| File | Change |
|---|---|
| `.github/workflows/vulnerabilityCatcher.yml` | **Removed.** Superseded
by `securityScan.yml`. |
| `.github/workflows/securityScan.yml` | **New.** Single job with
event-gated terminal steps. Includes NVD database caching for faster
runs. |
| `owasp-suppressions.xml` | **8 new entries** for documented
CPE/ecosystem false positives — Arrow R-only (CVE-2024-52338),
gRPC-Go-only (CVE-2026-33186), protobuf Python-only (CVE-2026-0994), 5
libthrift non-Java bindings. Each entry has a justification comment
block. |
| `osv-scanner.toml` | **New.** Mirrors the OWASP suppressions in TOML
format. Keep the two files in sync when adding/removing entries. |
| `pom.xml` | **cyclonedx-maven-plugin 2.9.1** added, bound to `package`
phase, `skipNotDeployed=false`. Emits an aggregate `target/bom.json` for
OSV to read. |

### Why two scanners

| Scanner | Source | Catches | Misses |
|---|---|---|---|
| OWASP dependency-check | NVD (CPE-based) | Anything with an NVD CPE
entry. Reuses existing `owasp-suppressions.xml` and `failBuildOnCVSS=7`
threshold. | GHSA-only advisories without an NVD CPE (e.g.,
CVE-2025-66566 in lz4-java — the #1455 finding). |
| OSV-Scanner v2.3.8 | OSV.dev (purl-based; federates GHSA + NVD + PyPA
+ RustSec) | The GHSA-only gap. Already turned up a new finding:
**bouncycastle CVE-2026-5598 (severity 8.9)** in bcprov-jdk18on 1.79,
invisible to OWASP. | Findings without a CVSS score (rare for
high-severity). |

### Why cyclonedx SBOM

OSV-Scanner's Maven resolver consults deps.dev's published-artifact
metadata. For the project's *own* GA, that means it sees the
previously-published 3.3.3 pom — not whatever bumps are sitting on
`main`. Result: in CI, OSV would keep showing yesterday's released state
instead of the PR branch's actual dependency tree. Producing a CycloneDX
aggregate SBOM at build time captures the actually-resolved local tree.

### Fixes from #1458 included here

The weekly scan has been failing every Sunday for 7+ consecutive weeks
(since early April 2026) without sending a notification email. Root
cause: the OWASP maven plugin step exits non-zero when CVEs are found
(because `<failBuildOnCVSS>7</failBuildOnCVSS>` is set in
`jdbc-core/pom.xml:357`), short-circuiting the rest of the job. The
subsequent steps never ran.

The new workflow has `continue-on-error: true` on the OWASP step and
routes finding-detection through an explicit `Collect findings` step
that reads the OWASP JSON report directly. Whether the job ultimately
fails (and whether email goes out) is decided after that step, not by
maven's exit code.

### Findings on `main` today

This workflow, run against current `main`, reports unsuppressed findings
that will be addressed in **separate follow-up PRs**:

1. \`org.bouncycastle:bcprov-jdk18on@1.79\` → CVE-2026-5598 (severity
8.9). Fixed in bouncycastle 1.84. **OWASP doesn't see this — OSV-only
finding.**
2. \`org.apache.thrift:libthrift@0.19.0\` → 4 CVEs
(CVE-2026-41603/04/05/43869, max severity 8.2). Cleared by the libthrift
0.19 → 0.23 follow-up (requires regenerating \`TCLIService.java\` with
the matching compiler).

So this PR's own CI is expected to fail on these 2 known findings until
the follow-ups land. Aligned with the rollout plan: add the workflow
now, mark it required-to-merge in branch protection after the burn-down.

### Expected runtime

Each invocation: **~5–7 minutes** (vs ~4 min for the old weekly).
Breakdown:

- `mvn package` (with cyclonedx SBOM): ~45–90s
- OWASP dependency-check (with NVD cache hit): ~1–2 min
- OSV-Scanner install + run: ~15s
- Other steps: ~30s

NVD caching cuts ~2 minutes off OWASP's runtime when the cache is warm.
The cache key rotates daily so we still get fresh CVE data.

### What this PR is NOT

* It is **not** a required check. Branch protection is unchanged — PRs
aren't blocked by the red ❌ yet.
* It does **not** introduce diff-mode (net-new findings only) gating.
Every PR has to ship into a clean codebase; this is the design we agreed
on, to force burn-down rather than just \"not making it worse.\"
* It does **not** include a Phase 2 design (override flags, severity
tiers, auto-issue filing). Phase 2 is deferred until we have ~2 weeks of
operational experience.

### Test plan

- [x] \`mvn package -DskipTests -Ddependency-check.skip=true\` produces
\`target/bom.json\` (69 components, all expected versions).
- [x] \`mvn -pl jdbc-core org.owasp:dependency-check-maven:check\`
reports 4 unsuppressed CVEs (all libthrift; matches expectation).
- [x] \`osv-scanner scan source --config=osv-scanner.toml --format=json
target/bom.json\` + severity≥7 filter reports 2 findings (bouncycastle +
libthrift cluster).
- [x] Findings-step jq logic correctly counts OWASP CVSS>=7 from the
JSON report.
- [x] XML validation of \`owasp-suppressions.xml\`.
- [x] YAML validation of \`securityScan.yml\`.
- [ ] CI run on this PR — expected to fail on the 2 known findings
above. **The CI failure on this PR is the intended outcome.**
- [ ] Manual \`workflow_dispatch\` run post-merge to verify the email
path fires.

NO_CHANGELOG=true

This pull request and its description were written by Isaac.

---------

Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant