DevOps: backend (Maven) dependency advisory scanning — close the Java SCA blind spot #820

Open
opened 2026-06-13 00:28:03 +02:00 by marcel · 8 comments
Owner

Context

Split out of #818 (which scopes frontend advisory early-warning: Renovate OSV alerts + a nightly npm audit job). That issue explicitly leaves the backend out of scope — and right now the entire Java dependency tree has no advisory scanning at all:

  • The PR audit gate (.gitea/workflows/ci.yml:32-33) runs npm audit only — frontend/ exclusively.
  • The nightly early-warning job added in #818 is also frontend-only.
  • Renovate, once stood up (#818 Phase 0), will open version-bump PRs for pom.xml, but osvVulnerabilityAlerts surfaces advisories Renovate already tracks — it is not a substitute for a real SCA scan of the resolved Maven tree.

So the larger transitive surface — Spring Boot 4, Hibernate, Jetty, Flyway, Jackson, and every transitive Java dependency — could carry a published CVE and nothing in CI would ever tell us. For an application holding private family documents, the backend is the higher-value attack surface, and it's the one currently unwatched.

Raised by Nora (security) during the #818 review: "This archive holds private family documents; the backend is the bigger attack surface. Even a quarterly cadence beats zero."

Goal

A published advisory against any backend (Maven) dependency — direct or transitive — is surfaced on a schedule, on a branch we own, with the same open-or-update tracking-issue mechanism the frontend nightly audit uses.

Scope

  1. Pick a scanner (see Open Decision). Candidates:
    • osv-scanner (Google) — scans the resolved tree against OSV.dev. Single static binary, fast, no DB download, same OSV source as Renovate's osvVulnerabilityAlerts → consistent signal across frontend + backend. Recommended.
    • OWASP dependency-check (org.owasp:dependency-check-maven) — richer NVD-based reporting, but downloads/maintains a large NVD cache (slow first run, needs an NVD API key to avoid rate limits) and is noisier.
  2. Add a scheduled job — extend .gitea/workflows/nightly.yml with a backend-SCA job, sibling to the frontend nightly audit from #818. Audit-only, no build beyond what the scanner needs to resolve the tree (osv-scanner reads pom.xml / a generated lockfile; dependency-check needs the dependencies resolved).
  3. Reuse the #818 tracking-issue mechanism — on a high/critical finding, open or update a single deduped tracking issue (fixed title marker, e.g. Nightly backend SCA: advisory), labelled security, devops, P1-high. Same dedupe + heartbeat + self-test pattern established in #818 — extract it into a composite action if the two jobs share enough logic (ADR-029 precedent).
  4. Severity threshold — fail/alert on HIGH and above, matching the frontend gate's --audit-level=high.
  5. Docs — a section in docs/infrastructure/ci-gitea.md documenting the scanner choice, the threshold, false-positive suppression (osv-scanner --config ignore file / dependency-check suppression XML), and the runbook for a backend-SCA tracking issue.

Acceptance criteria

  • A scheduled backend-SCA job runs against the Maven tree and, on a HIGH+ finding, opens/updates one deduped tracking issue (security, devops, P1-high).
  • Dedupe verified: two consecutive failing runs → one issue, updated not duplicated.
  • A documented, version-controlled suppression mechanism exists for accepted/false-positive advisories (so one unfixable transitive CVE doesn't keep the job permanently red).
  • docs/infrastructure/ci-gitea.md documents scanner choice, threshold, suppression, and runbook.
  • Verified end-to-end via manual workflow_dispatch (a temporarily lowered threshold or a known-vulnerable test dependency is acceptable proof).

Out of scope

  • Frontend advisory scanning — that's #818.
  • Auto-bumping vulnerable backend deps — Renovate (#818 Phase 0) owns the version-bump PRs; this issue is detection/alerting only.

Open Decision

  • Scanner choice: osv-scanner vs OWASP dependency-check. Recommendation: osv-scanner — same OSV.dev source as the frontend Renovate alerts (one consistent advisory feed), no heavy NVD cache, trivial to pin as a static binary. Choose dependency-check only if NVD-specific metadata / CVSS detail is needed over OSV coverage. Resolve before implementation.

Notes

  • Depends on #818 only for the shared tracking-issue mechanism — can proceed in parallel, but lands cleaner after #818's nightly job exists to extract the common logic from.
  • Relates to #818, #817.
## Context Split out of #818 (which scopes **frontend** advisory early-warning: Renovate OSV alerts + a nightly `npm audit` job). That issue explicitly leaves the **backend out of scope** — and right now the entire Java dependency tree has **no advisory scanning at all**: - The PR audit gate (`.gitea/workflows/ci.yml:32-33`) runs `npm audit` only — `frontend/` exclusively. - The nightly early-warning job added in #818 is also frontend-only. - Renovate, once stood up (#818 Phase 0), will open *version-bump* PRs for `pom.xml`, but `osvVulnerabilityAlerts` surfaces advisories Renovate already tracks — it is not a substitute for a real SCA scan of the resolved Maven tree. So the **larger** transitive surface — Spring Boot 4, Hibernate, Jetty, Flyway, Jackson, and every transitive Java dependency — could carry a published CVE and nothing in CI would ever tell us. For an application holding **private family documents**, the backend is the higher-value attack surface, and it's the one currently unwatched. > Raised by Nora (security) during the #818 review: *"This archive holds private family documents; the backend is the bigger attack surface. Even a quarterly cadence beats zero."* ## Goal A published advisory against any backend (Maven) dependency — direct or transitive — is surfaced on a schedule, on a branch we own, with the same open-or-update tracking-issue mechanism the frontend nightly audit uses. ## Scope 1. **Pick a scanner** (see Open Decision). Candidates: - **`osv-scanner`** (Google) — scans the resolved tree against OSV.dev. Single static binary, fast, no DB download, same OSV source as Renovate's `osvVulnerabilityAlerts` → consistent signal across frontend + backend. Recommended. - **OWASP `dependency-check`** (`org.owasp:dependency-check-maven`) — richer NVD-based reporting, but downloads/maintains a large NVD cache (slow first run, needs an NVD API key to avoid rate limits) and is noisier. 2. **Add a scheduled job** — extend `.gitea/workflows/nightly.yml` with a backend-SCA job, sibling to the frontend nightly audit from #818. Audit-only, no build beyond what the scanner needs to resolve the tree (`osv-scanner` reads `pom.xml` / a generated lockfile; dependency-check needs the dependencies resolved). 3. **Reuse the #818 tracking-issue mechanism** — on a high/critical finding, open **or update** a single deduped tracking issue (fixed title marker, e.g. `Nightly backend SCA: advisory`), labelled `security`, `devops`, `P1-high`. Same dedupe + heartbeat + self-test pattern established in #818 — extract it into a composite action if the two jobs share enough logic (ADR-029 precedent). 4. **Severity threshold** — fail/alert on `HIGH` and above, matching the frontend gate's `--audit-level=high`. 5. **Docs** — a section in `docs/infrastructure/ci-gitea.md` documenting the scanner choice, the threshold, false-positive suppression (osv-scanner `--config` ignore file / dependency-check suppression XML), and the runbook for a backend-SCA tracking issue. ## Acceptance criteria - A scheduled backend-SCA job runs against the Maven tree and, on a `HIGH`+ finding, opens/updates **one** deduped tracking issue (`security`, `devops`, `P1-high`). - Dedupe verified: two consecutive failing runs → one issue, updated not duplicated. - A documented, version-controlled suppression mechanism exists for accepted/false-positive advisories (so one unfixable transitive CVE doesn't keep the job permanently red). - `docs/infrastructure/ci-gitea.md` documents scanner choice, threshold, suppression, and runbook. - Verified end-to-end via manual `workflow_dispatch` (a temporarily lowered threshold or a known-vulnerable test dependency is acceptable proof). ## Out of scope - Frontend advisory scanning — that's #818. - Auto-bumping vulnerable backend deps — Renovate (#818 Phase 0) owns the version-bump PRs; this issue is detection/alerting only. ## Open Decision - **Scanner choice: `osv-scanner` vs OWASP `dependency-check`.** Recommendation: `osv-scanner` — same OSV.dev source as the frontend Renovate alerts (one consistent advisory feed), no heavy NVD cache, trivial to pin as a static binary. Choose `dependency-check` only if NVD-specific metadata / CVSS detail is needed over OSV coverage. Resolve before implementation. ## Notes - Depends on #818 only for the shared tracking-issue mechanism — can proceed in parallel, but lands cleaner after #818's nightly job exists to extract the common logic from. - Relates to #818, #817.
marcel added the P2-mediumdevopsneeds-discussionsecurity labels 2026-06-13 00:28:12 +02:00
Author
Owner

🛠️ Tobias Wendt — DevOps & Platform Engineer

Observations

  • The runner story is fine here: nightly.yml runs on the self-hosted DooD runner, and osv-scanner is a single static binary — no DB download, no NVD cache, no privileged container. That's exactly the "every added tool is a new failure mode" calculus working in osv-scanner's favour over OWASP dependency-check (which drags in a multi-GB NVD cache + API key + slow cold first run). I back the recommendation.
  • But the dedupe/tracking-issue mechanism this issue wants to "reuse" does not exist yet. nightly.yml today is a single deploy-staging job — #818's nightly audit job and its open-or-update issue logic are still unbuilt (Renovate has literally never run; #818 Phase 0). So "reuse the #818 mechanism" is currently "build the mechanism." Sequencing matters — see Decision Queue.
  • Token reality check. #818 says "use the auto-provided ${{ secrets.GITEA_TOKEN }}." Our own docs/infrastructure/ci-gitea.md §Token Name Difference says Gitea wants an explicit access-token secret, not the GitHub auto-token. Gitea Actions does inject a GITEA_TOKEN, but its scope on our self-hosted instance is not guaranteed to include issues:write on a scheduled (non-PR) run. Confirm it can open an issue before building on it; otherwise a dedicated bot PAT scoped to issues only.

Recommendations

  • Pin osv-scanner like everything else. We pin semgrep==1.163.0, base images by digest, actions @v4. Install a fixed osv-scanner version and verify its checksum — never curl | sh of latest. A moving scanner version is a non-reproducible nightly that changes its mind about what's vulnerable.
  • Own job, never a step inside deploy-staging — same rule #818 landed on. A backend-SCA finding must produce an independent red/green signal that a green deploy can't mask, and vice versa.
  • Heartbeat on the clean path. Emit a job-summary line ("backend SCA ran, 0 HIGH+ findings") so "no issue" provably means "ran and clean," not "cron silently stopped firing." This is the cheapest insurance against a dead scanner we never notice.
  • Resolve the tree deterministically. Maven has no lockfile (confirmed — none in backend/), so osv-scanner pointed at pom.xml does not automatically see the full transitive Spring Boot 4 / Hibernate / Jetty tree, which is the entire point of this issue. Generate a CycloneDX SBOM in the job (cyclonedx-maven-plugin, mvn cyclonedx:makeAggregateBom) and scan that — it captures the fully-resolved transitive set, offline and reproducible. See Felix's comment for the mechanics.

Open Decisions

  • Sequencing against #818 — build the shared issue-logic here and have #818 adopt it, or wait for #818 to land and extract from it? Both are defensible; it's a project-ordering call. (See Decision Queue.)
## 🛠️ Tobias Wendt — DevOps & Platform Engineer ### Observations - The runner story is fine here: `nightly.yml` runs on the self-hosted DooD runner, and `osv-scanner` is a single static binary — no DB download, no NVD cache, no privileged container. That's exactly the "every added tool is a new failure mode" calculus working in `osv-scanner`'s favour over OWASP `dependency-check` (which drags in a multi-GB NVD cache + API key + slow cold first run). I back the recommendation. - **But the dedupe/tracking-issue mechanism this issue wants to "reuse" does not exist yet.** `nightly.yml` today is a single `deploy-staging` job — #818's nightly audit job and its open-or-update issue logic are still unbuilt (Renovate has literally never run; #818 Phase 0). So "reuse the #818 mechanism" is currently "build the mechanism." Sequencing matters — see Decision Queue. - **Token reality check.** #818 says "use the auto-provided `${{ secrets.GITEA_TOKEN }}`." Our own `docs/infrastructure/ci-gitea.md §Token Name Difference` says Gitea wants an *explicit* access-token secret, not the GitHub auto-token. Gitea Actions *does* inject a `GITEA_TOKEN`, but its scope on our self-hosted instance is not guaranteed to include `issues:write` on a scheduled (non-PR) run. Confirm it can open an issue before building on it; otherwise a dedicated bot PAT scoped to `issues` only. ### Recommendations - **Pin `osv-scanner` like everything else.** We pin `semgrep==1.163.0`, base images by digest, actions `@v4`. Install a fixed `osv-scanner` version and verify its checksum — never `curl | sh` of `latest`. A moving scanner version is a non-reproducible nightly that changes its mind about what's vulnerable. - **Own job, never a step inside `deploy-staging`** — same rule #818 landed on. A backend-SCA finding must produce an independent red/green signal that a green deploy can't mask, and vice versa. - **Heartbeat on the clean path.** Emit a job-summary line ("backend SCA ran, 0 HIGH+ findings") so "no issue" provably means "ran and clean," not "cron silently stopped firing." This is the cheapest insurance against a dead scanner we never notice. - **Resolve the tree deterministically.** Maven has no lockfile (confirmed — none in `backend/`), so `osv-scanner` pointed at `pom.xml` does not automatically see the full transitive Spring Boot 4 / Hibernate / Jetty tree, which is the entire point of this issue. Generate a CycloneDX SBOM in the job (`cyclonedx-maven-plugin`, `mvn cyclonedx:makeAggregateBom`) and scan *that* — it captures the fully-resolved transitive set, offline and reproducible. See Felix's comment for the mechanics. ### Open Decisions - **Sequencing against #818** — build the shared issue-logic here and have #818 adopt it, or wait for #818 to land and extract from it? Both are defensible; it's a project-ordering call. _(See Decision Queue.)_
Author
Owner

🛡️ Nora "NullX" Steiner — Application Security Engineer

Observations

  • This is the right issue to file. For an archive of private family documents, the Java tree is the higher-value attack surface and it is currently scanned by nothing — the PR gate (ci.yml:32-33) is npm audit, frontend-only. A deserialization gadget or an SSRF in a transitive Jackson/Hibernate/Jetty dep would ship completely unseen. Glad to see my #818 review note turned into a tracked issue.
  • Consistency-of-signal is a real security benefit, not just tidiness. osv-scanner hits the same OSV.dev feed Renovate's osvVulnerabilityAlerts uses, so frontend and backend advisories speak one vocabulary. Two scanners against two different advisory databases (OSV vs NVD) means two triage mental models and inevitable "why did X fire here but not there." OSV everywhere.

Recommendations

  • Coverage must be transitive, and you must prove it. The threat is overwhelmingly in transitive deps, not the ~40 direct ones in pom.xml. A scan that only reads declared pom.xml coordinates is security theatre for this issue's stated goal. Scan a generated CycloneDX SBOM (full resolved tree) — and make the end-to-end proof use a transitive known-vulnerable dep, not a direct one, so the AC actually exercises what we care about.
  • Severity threshold is not free with osv-scanner. npm audit --audit-level=high has a native floor; osv-scanner historically exits non-zero on any advisory regardless of CVSS. Don't assume "fail on HIGH+" parity exists out of the box — either use the installed version's severity filter (verify it exists) or post-process the JSON on CVSS ≥ 7.0. Otherwise the job goes red on a LOW transitive advisory and gets muted, which is worse than no scanner.
  • Suppression must expire. The accepted-FP mechanism (osv-scanner.toml [[IgnoredVulns]]) must require a reason and an expiry date per entry. A permanent silent ignore is how a real CVE hides behind a stale suppression for "that one unfixable transitive thing." Expiry forces a re-review; document this in the runbook.
  • Least privilege on the issue-opening token. The step needs issues:write and nothing else. Do not hand the nightly a broad contents+pull_request PAT just because Renovate's bot has one. A leaked nightly token should not be able to push code.
  • Log the advisory IDs, never raw advisory text into a shell context. When building the tracking-issue body, pass OSV IDs/CVSS as data, not interpolated into an eval/bash -c. An advisory summary is attacker-influenceable upstream content.

Open Decisions (none)

## 🛡️ Nora "NullX" Steiner — Application Security Engineer ### Observations - This is the right issue to file. For an archive of **private family documents**, the Java tree is the higher-value attack surface and it is currently scanned by **nothing** — the PR gate (`ci.yml:32-33`) is `npm audit`, frontend-only. A deserialization gadget or an SSRF in a transitive Jackson/Hibernate/Jetty dep would ship completely unseen. Glad to see my #818 review note turned into a tracked issue. - **Consistency-of-signal is a real security benefit, not just tidiness.** `osv-scanner` hits the same OSV.dev feed Renovate's `osvVulnerabilityAlerts` uses, so frontend and backend advisories speak one vocabulary. Two scanners against two different advisory databases (OSV vs NVD) means two triage mental models and inevitable "why did X fire here but not there." OSV everywhere. ### Recommendations - **Coverage must be transitive, and you must prove it.** The threat is overwhelmingly in transitive deps, not the ~40 direct ones in `pom.xml`. A scan that only reads declared `pom.xml` coordinates is security theatre for this issue's stated goal. Scan a generated CycloneDX SBOM (full resolved tree) — and make the end-to-end proof use a *transitive* known-vulnerable dep, not a direct one, so the AC actually exercises what we care about. - **Severity threshold is not free with osv-scanner.** `npm audit --audit-level=high` has a native floor; `osv-scanner` historically exits non-zero on *any* advisory regardless of CVSS. Don't assume "fail on HIGH+" parity exists out of the box — either use the installed version's severity filter (verify it exists) or post-process the JSON on `CVSS ≥ 7.0`. Otherwise the job goes red on a LOW transitive advisory and gets muted, which is worse than no scanner. - **Suppression must expire.** The accepted-FP mechanism (`osv-scanner.toml` `[[IgnoredVulns]]`) must require a `reason` *and* an `expiry` date per entry. A permanent silent ignore is how a real CVE hides behind a stale suppression for "that one unfixable transitive thing." Expiry forces a re-review; document this in the runbook. - **Least privilege on the issue-opening token.** The step needs `issues:write` and nothing else. Do not hand the nightly a broad `contents`+`pull_request` PAT just because Renovate's bot has one. A leaked nightly token should not be able to push code. - **Log the advisory IDs, never raw advisory text into a shell context.** When building the tracking-issue body, pass OSV IDs/CVSS as data, not interpolated into an `eval`/`bash -c`. An advisory summary is attacker-influenceable upstream content. ### Open Decisions _(none)_
Author
Owner

🏛️ Markus Keller — Application Architect

Observations

  • Scope is clean and correctly bounded: detection/alerting only, version bumps stay with Renovate (#818). No architectural objection to the feature itself.
  • The issue proposes extracting the dedupe/open-or-update logic into a composite action (ADR-029 precedent) shared by the frontend nightly (#818) and this backend job. ADR-029 covers cross-workflow deploy logic (deploy-obs, reload-caddy, smoke-test are shared by nightly.yml + release.yml). The pattern fits — but note this is a two-caller abstraction, and one of the two callers doesn't exist yet.
  • Confirmed there is no Maven lockfile and no SBOM plugin in backend/pom.xml. So "scan the tree" is a design decision, not a given: you are choosing the source-of-truth for the resolved dependency graph. That choice belongs in the doc, ADR-style.

Recommendations

  • Don't build the composite action against a phantom. Rule of three is already relaxed to two here, which is fine for non-trivial security logic — but extracting before #818's job exists means you're designing the shared interface against one real caller and one imagined one. Either (a) #818 lands its job first and #820 extracts the now-proven logic into .gitea/actions/sca-tracking-issue, or (b) #820 builds the action with #818 as a named adopter-to-be. Inlining first and extracting on the second real caller is the lower-risk path. (Decision Queue.)
  • Make the SBOM the architectural seam. Generating a CycloneDX SBOM (cyclonedx-maven-plugin) and scanning that decouples "resolve the dependency graph" (Maven's job) from "match against advisories" (the scanner's job). It also means swapping osv-scanner for anything else later is a one-line change at the scan step, not a rework. That's the boring, low-coupling choice.
  • Capture the scanner decision in the doc as a mini-ADR. Per my doc-currency rule this isn't a new domain/migration, so a full docs/adr/NNN is optional — but the ci-gitea.md section must record why OSV-over-NVD and why SBOM-over-raw-pom, so nobody "simplifies" it back to a bare pom.xml scan in six months and silently drops transitive coverage.

Open Decisions

  • Composite-action extraction timing (inline-now-extract-later vs build-shared-now). (See Decision Queue.)
## 🏛️ Markus Keller — Application Architect ### Observations - Scope is clean and correctly bounded: detection/alerting only, version bumps stay with Renovate (#818). No architectural objection to the feature itself. - The issue proposes extracting the dedupe/open-or-update logic into a **composite action (ADR-029 precedent)** shared by the frontend nightly (#818) and this backend job. ADR-029 covers cross-*workflow* deploy logic (`deploy-obs`, `reload-caddy`, `smoke-test` are shared by `nightly.yml` + `release.yml`). The pattern fits — but note this is a *two-caller* abstraction, and one of the two callers doesn't exist yet. - Confirmed there is no Maven lockfile and no SBOM plugin in `backend/pom.xml`. So "scan the tree" is a design decision, not a given: you are choosing the source-of-truth for the resolved dependency graph. That choice belongs in the doc, ADR-style. ### Recommendations - **Don't build the composite action against a phantom.** Rule of three is already relaxed to two here, which is fine for non-trivial security logic — but extracting *before #818's job exists* means you're designing the shared interface against one real caller and one imagined one. Either (a) #818 lands its job first and #820 extracts the now-proven logic into `.gitea/actions/sca-tracking-issue`, or (b) #820 builds the action with #818 as a named adopter-to-be. Inlining first and extracting on the second real caller is the lower-risk path. _(Decision Queue.)_ - **Make the SBOM the architectural seam.** Generating a CycloneDX SBOM (`cyclonedx-maven-plugin`) and scanning that decouples "resolve the dependency graph" (Maven's job) from "match against advisories" (the scanner's job). It also means swapping `osv-scanner` for anything else later is a one-line change at the scan step, not a rework. That's the boring, low-coupling choice. - **Capture the scanner decision in the doc as a mini-ADR.** Per my doc-currency rule this isn't a new domain/migration, so a full `docs/adr/NNN` is optional — but the `ci-gitea.md` section must record *why* OSV-over-NVD and *why* SBOM-over-raw-pom, so nobody "simplifies" it back to a bare `pom.xml` scan in six months and silently drops transitive coverage. ### Open Decisions - **Composite-action extraction timing** (inline-now-extract-later vs build-shared-now). _(See Decision Queue.)_
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • Implementation detail the issue underweights: osv-scanner against a bare pom.xml does not reliably resolve the transitive tree. Maven has no lockfile (I checked — nothing in backend/), so unlike npm audit reading package-lock.json, there's no resolved manifest to hand the scanner. osv-scanner's native Maven transitive resolution exists but has been gated behind experimental flags and fetches from Maven Central at scan time — non-deterministic and version-sensitive. I don't want a nightly whose results depend on an experimental resolver's mood.
  • The "severity threshold (fail on HIGH+)" AC assumes osv-scanner has an --audit-level=high equivalent. It does not, historically — it exits non-zero on any advisory. That's a behaviour difference from the frontend gate the issue treats as symmetric.

Recommendations

  • SBOM-first, deterministic. Add cyclonedx-maven-plugin and generate an aggregate BOM, then scan it:
    cd backend && ./mvnw -q -DskipTests cyclonedx:makeAggregateBom   # → target/bom.json (full resolved tree)
    osv-scanner scan --format json --sbom backend/target/bom.json > osv.json
    
    Reproducible, offline after the Maven resolve, and the BOM is the audit artifact. (Source the SDKMAN env before ./mvnw.)
  • Do the HIGH+ gate yourself, in jq. Don't rely on a scanner exit code that fires on LOW. Parse osv.json, keep findings with max CVSS ≥ 7.0, and let that count drive open-or-update vs clean. This also gives you the exact list to put in the issue body.
  • Pin the binary + verify checksum. Match repo posture (semgrep==1.163.0, digest-pinned images). Download a fixed osv-scanner release and sha256sum -c it; fail the job if the checksum doesn't match. No latest.
  • Dedupe via a fixed title marker — same shape #818 specified: GET open issues labelled security, match the marker substring (e.g. Nightly backend SCA: advisory), PATCH the body if found else POST a new one. Capture both run URLs as the dedupe proof.
  • Add the shell self-test (mirrors the grep self-tests already in ci.yml): feed the matcher a sample existing-issue title (must match) and an unrelated title (must not), exit 1 on either failing. Cheap regression guard that the dedupe doesn't silently start duplicating.
  • No npm ci / node_modules here — this job is pure Maven + a static binary. checkout → SDKMAN/Java → mvnw cyclonedx → scan. Don't copy frontend setup steps in.

Open Decisions (none — all of the above are mechanical calls)

## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - Implementation detail the issue underweights: **`osv-scanner` against a bare `pom.xml` does not reliably resolve the transitive tree.** Maven has no lockfile (I checked — nothing in `backend/`), so unlike `npm audit` reading `package-lock.json`, there's no resolved manifest to hand the scanner. `osv-scanner`'s native Maven transitive resolution exists but has been gated behind experimental flags and fetches from Maven Central at scan time — non-deterministic and version-sensitive. I don't want a nightly whose results depend on an experimental resolver's mood. - The "severity threshold (fail on HIGH+)" AC assumes `osv-scanner` has an `--audit-level=high` equivalent. It does not, historically — it exits non-zero on any advisory. That's a behaviour difference from the frontend gate the issue treats as symmetric. ### Recommendations - **SBOM-first, deterministic.** Add `cyclonedx-maven-plugin` and generate an aggregate BOM, then scan it: ```bash cd backend && ./mvnw -q -DskipTests cyclonedx:makeAggregateBom # → target/bom.json (full resolved tree) osv-scanner scan --format json --sbom backend/target/bom.json > osv.json ``` Reproducible, offline after the Maven resolve, and the BOM *is* the audit artifact. (Source the SDKMAN env before `./mvnw`.) - **Do the HIGH+ gate yourself, in `jq`.** Don't rely on a scanner exit code that fires on LOW. Parse `osv.json`, keep findings with max CVSS ≥ 7.0, and let *that* count drive open-or-update vs clean. This also gives you the exact list to put in the issue body. - **Pin the binary + verify checksum.** Match repo posture (`semgrep==1.163.0`, digest-pinned images). Download a fixed `osv-scanner` release and `sha256sum -c` it; fail the job if the checksum doesn't match. No `latest`. - **Dedupe via a fixed title marker** — same shape #818 specified: `GET` open issues labelled `security`, match the marker substring (e.g. `Nightly backend SCA: advisory`), `PATCH` the body if found else `POST` a new one. Capture both run URLs as the dedupe proof. - **Add the shell self-test** (mirrors the `grep` self-tests already in `ci.yml`): feed the matcher a sample existing-issue title (must match) and an unrelated title (must not), `exit 1` on either failing. Cheap regression guard that the dedupe doesn't silently start duplicating. - **No `npm ci` / `node_modules` here** — this job is pure Maven + a static binary. `checkout` → SDKMAN/Java → `mvnw cyclonedx` → scan. Don't copy frontend setup steps in. ### Open Decisions _(none — all of the above are mechanical calls)_
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Observations

  • The dedupe AC is well-specified (two failing runs → one updated issue, both run URLs captured) — that's the same testable bar #818 set, good.
  • Gap: the goal says "direct or transitive" but no AC proves transitive detection. The "known-vulnerable test dependency" proof could be satisfied by a direct dep and still pass while transitive coverage is silently broken. The most dangerous failure mode (transitive advisory missed) would slip straight through this acceptance.
  • Gap: no clean-path assertion. Nothing verifies the job ran and found nothing. Without that, a cron that stopped firing looks identical to "all clear."

Recommendations

  • Make the end-to-end proof use a transitive vuln. The workflow_dispatch verification dependency should be one pulled in transitively (or temporarily lower the threshold against a real transitive advisory already in the tree). AC wording: "Verified that a HIGH+ advisory against a transitive Maven dependency opens the tracking issue." That single word closes the biggest hole.
  • Add three explicit, separately-failing test cases (one behaviour each):
    1. Create path — clean repo + injected HIGH transitive vuln → exactly one issue, correct labels (security, devops, P1-high).
    2. Update path — second run, vuln still present → same issue updated, no second issue (assert issue count == 1).
    3. Clean path — no HIGH+ finding → no issue, heartbeat line present in the job summary.
  • Test the threshold boundary. A LOW/MEDIUM-only advisory must not open an issue. Given osv-scanner's exit-on-any behaviour (see Felix), this is the case most likely to regress into noise — assert it explicitly.
  • Suppression test. A suppressed advisory ID → job green, and an expired suppression → advisory re-surfaces. Otherwise the suppression file is untested security-relevant config.
  • Keep the self-test for the title-matcher (Felix's point) — that's the unit-level guard under the integration-level dedupe AC.

Open Decisions (none)

## 🧪 Sara Holt — QA Engineer & Test Strategist ### Observations - The dedupe AC is well-specified (two failing runs → one updated issue, both run URLs captured) — that's the same testable bar #818 set, good. - **Gap: the goal says "direct *or transitive*" but no AC proves transitive detection.** The "known-vulnerable test dependency" proof could be satisfied by a *direct* dep and still pass while transitive coverage is silently broken. The most dangerous failure mode (transitive advisory missed) would slip straight through this acceptance. - **Gap: no clean-path assertion.** Nothing verifies the job ran and found nothing. Without that, a cron that stopped firing looks identical to "all clear." ### Recommendations - **Make the end-to-end proof use a transitive vuln.** The `workflow_dispatch` verification dependency should be one pulled in *transitively* (or temporarily lower the threshold against a real transitive advisory already in the tree). AC wording: "Verified that a HIGH+ advisory against a **transitive** Maven dependency opens the tracking issue." That single word closes the biggest hole. - **Add three explicit, separately-failing test cases** (one behaviour each): 1. *Create path* — clean repo + injected HIGH transitive vuln → exactly one issue, correct labels (`security`, `devops`, `P1-high`). 2. *Update path* — second run, vuln still present → same issue updated, **no second issue** (assert issue count == 1). 3. *Clean path* — no HIGH+ finding → **no issue**, heartbeat line present in the job summary. - **Test the threshold boundary.** A LOW/MEDIUM-only advisory must **not** open an issue. Given osv-scanner's exit-on-any behaviour (see Felix), this is the case most likely to regress into noise — assert it explicitly. - **Suppression test.** A suppressed advisory ID → job green, and an **expired** suppression → advisory re-surfaces. Otherwise the suppression file is untested security-relevant config. - **Keep the self-test for the title-matcher** (Felix's point) — that's the unit-level guard under the integration-level dedupe AC. ### Open Decisions _(none)_
Author
Owner

📋 "Elicit" — Requirements Engineer

Observations

  • Strong issue: explicit Goal, Scope, testable ACs, Out-of-scope, and the open decision surfaced rather than buried. The needs-discussion label is correctly applied.
  • Hidden dependency in the ACs. AC #3 ("reuse the #818 tracking-issue mechanism") and the composite-action suggestion silently assume #818 has shipped. It hasn't — Renovate has never run and #818's nightly job doesn't exist. As written, this issue is not independently deliverable: either it inherits #818's unbuilt mechanism or it builds its own. That ordering needs to be an explicit precondition, exactly like #818 made Phase 0 a gate.
  • Goal/AC mismatch on "transitive." The Goal foregrounds the transitive surface (Spring Boot 4 / Hibernate / Jetty); the ACs never require transitive detection be proven. A requirement not in the acceptance criteria is a wish, not a requirement. (Sara has the concrete fix.)
  • Orphan issue — no milestone, same as #818. Two related security-hardening issues with no milestone tend to drift indefinitely.

Recommendations

  • Add an explicit precondition block mirroring #818's Phase 0: "Depends on #818's tracking-issue mechanism. If #818 has not landed, this issue first builds that mechanism inline." Resolve the build-vs-reuse ordering before implementation starts (Decision Queue).
  • Promote transitive coverage to an AC in EARS form: "When a HIGH+ advisory exists against a transitively-resolved Maven dependency, the nightly job shall open the deduped tracking issue." Verified via workflow_dispatch against a transitive vuln.
  • Add a measurable threshold AC: "A MEDIUM-or-lower-only advisory set shall NOT open an issue." Pins the "HIGH and above" intent to something falsifiable.
  • Assign a milestone so #818 + #820 land as one coherent security-hardening slice rather than two orphans.

Open Decisions

  • Milestone assignment — which milestone owns the #818/#820 SCA hardening pair? (See Decision Queue.)
## 📋 "Elicit" — Requirements Engineer ### Observations - Strong issue: explicit Goal, Scope, testable ACs, Out-of-scope, and the open decision surfaced rather than buried. The `needs-discussion` label is correctly applied. - **Hidden dependency in the ACs.** AC #3 ("reuse the #818 tracking-issue mechanism") and the composite-action suggestion silently assume #818 has shipped. It hasn't — Renovate has never run and #818's nightly job doesn't exist. As written, this issue is *not independently deliverable*: either it inherits #818's unbuilt mechanism or it builds its own. That ordering needs to be an explicit precondition, exactly like #818 made Phase 0 a gate. - **Goal/AC mismatch on "transitive."** The Goal foregrounds the transitive surface (Spring Boot 4 / Hibernate / Jetty); the ACs never require transitive detection be proven. A requirement not in the acceptance criteria is a wish, not a requirement. (Sara has the concrete fix.) - **Orphan issue** — no milestone, same as #818. Two related security-hardening issues with no milestone tend to drift indefinitely. ### Recommendations - **Add an explicit precondition block** mirroring #818's Phase 0: "Depends on #818's tracking-issue mechanism. If #818 has not landed, this issue first builds that mechanism inline." Resolve the build-vs-reuse ordering before implementation starts (Decision Queue). - **Promote transitive coverage to an AC** in EARS form: *"When a HIGH+ advisory exists against a transitively-resolved Maven dependency, the nightly job shall open the deduped tracking issue."* Verified via `workflow_dispatch` against a transitive vuln. - **Add a measurable threshold AC:** *"A MEDIUM-or-lower-only advisory set shall NOT open an issue."* Pins the "HIGH and above" intent to something falsifiable. - **Assign a milestone** so #818 + #820 land as one coherent security-hardening slice rather than two orphans. ### Open Decisions - **Milestone assignment** — which milestone owns the #818/#820 SCA hardening pair? _(See Decision Queue.)_
Author
Owner

🎨 Leonie — UX & Interaction

No concerns from my angle. This is pure CI / dependency-scanning infrastructure with no user-facing surface — the only "UI" is a Gitea tracking issue, and the team conventions for that (fixed title marker, security/devops/P1-high labels, runbook in ci-gitea.md) are already covered by the other personas. Nothing to add on interaction or accessibility.

## 🎨 Leonie — UX & Interaction No concerns from my angle. This is pure CI / dependency-scanning infrastructure with no user-facing surface — the only "UI" is a Gitea tracking issue, and the team conventions for that (fixed title marker, `security`/`devops`/`P1-high` labels, runbook in `ci-gitea.md`) are already covered by the other personas. Nothing to add on interaction or accessibility.
Author
Owner

🗳️ Decision Queue — Action Required

3 decisions need your input before implementation starts.

Architecture / Sequencing

  • Build-vs-reuse the shared tracking-issue mechanism. AC #3 says "reuse the #818 mechanism," but #818 hasn't shipped — Renovate has never run and its nightly job doesn't exist, so there is nothing to reuse yet. Options: (a) land #818 first, then #820 extracts the proven open-or-update logic into .gitea/actions/sca-tracking-issue; (b) #820 builds the logic inline now with #818 as a named future adopter; (c) build the shared composite action up front against both. Cost: (a) serializes the two issues but extracts a real abstraction; (b) ships #820 independently but risks two near-duplicate implementations until someone reconciles them; (c) designs the interface against one real caller and one imagined one (Markus's "phantom" risk). (Raised by: Tobias, Markus, Elicit)

Scanner choice (the issue's stated Open Decision, refined)

  • osv-scanner vs OWASP dependency-check — and how to feed it the transitive tree. Recommendation stands: osv-scanner (same OSV.dev feed as Renovate, no NVD cache/key, static binary). But because Maven has no lockfile, osv-scanner only reliably covers the transitive Spring Boot/Hibernate/Jetty tree if you scan a generated CycloneDX SBOM (cyclonedx-maven-plugin) rather than a bare pom.xml. Two corollaries the issue didn't budget for: osv-scanner has no native HIGH-only gate (post-process CVSS ≥ 7.0 yourself in jq), whereas dependency-check has native transitive resolution and failBuildOnCVSS but drags a multi-GB NVD cache. Net: choose osv-scanner + SBOM unless you specifically want NVD CVSS detail. (Raised by: Tobias, Nora, Markus, Felix)

Process

  • Milestone assignment. #818 and #820 are both orphans (no milestone). Decide which milestone owns the SCA-hardening pair so they land together rather than drifting. (Raised by: Elicit)
## 🗳️ Decision Queue — Action Required _3 decisions need your input before implementation starts._ ### Architecture / Sequencing - **Build-vs-reuse the shared tracking-issue mechanism.** AC #3 says "reuse the #818 mechanism," but #818 hasn't shipped — Renovate has never run and its nightly job doesn't exist, so there is nothing to reuse yet. Options: **(a)** land #818 first, then #820 extracts the proven open-or-update logic into `.gitea/actions/sca-tracking-issue`; **(b)** #820 builds the logic inline now with #818 as a named future adopter; **(c)** build the shared composite action up front against both. Cost: (a) serializes the two issues but extracts a real abstraction; (b) ships #820 independently but risks two near-duplicate implementations until someone reconciles them; (c) designs the interface against one real caller and one imagined one (Markus's "phantom" risk). _(Raised by: Tobias, Markus, Elicit)_ ### Scanner choice _(the issue's stated Open Decision, refined)_ - **`osv-scanner` vs OWASP `dependency-check` — and how to feed it the transitive tree.** Recommendation stands: **`osv-scanner`** (same OSV.dev feed as Renovate, no NVD cache/key, static binary). **But** because Maven has no lockfile, `osv-scanner` only reliably covers the transitive Spring Boot/Hibernate/Jetty tree if you scan a generated **CycloneDX SBOM** (`cyclonedx-maven-plugin`) rather than a bare `pom.xml`. Two corollaries the issue didn't budget for: `osv-scanner` has **no native HIGH-only gate** (post-process CVSS ≥ 7.0 yourself in `jq`), whereas `dependency-check` has native transitive resolution *and* `failBuildOnCVSS` but drags a multi-GB NVD cache. Net: choose `osv-scanner + SBOM` unless you specifically want NVD CVSS detail. _(Raised by: Tobias, Nora, Markus, Felix)_ ### Process - **Milestone assignment.** #818 and #820 are both orphans (no milestone). Decide which milestone owns the SCA-hardening pair so they land together rather than drifting. _(Raised by: Elicit)_
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#820