ADR-018: The Source Scan Is Aware of the Pipeline's Dependency Cache¶
| Field | Value |
|---|---|
| Date | 2026-07-29 |
| Status | Accepted |
| Deciders | Holger Zahnleiter |
| Supersedes | — |
| Superseded by | — |
Context and Problem Statement¶
The scan job of the reference mono-repo's payment artifact failed with
FATAL Error remote Maven repository returned 429 Too Many Requests for
https://repo.maven.apache.org/maven2/org/springframework/data/spring-data-bom/2026.0.0/spring-data-bom-2026.0.0.pom.
Retry-After: 1800.
Every generated pipeline caches its dependencies inside $CI_PROJECT_DIR — .m2/repository,
.go-cache, .conan, .npm-cache — and the cache: block sits under default:, so the cache
is restored into every job of the pipeline, the scan job included. None of the nine CI
templates told Trivy that this directory exists. That single omission has two independent
consequences, pulling in opposite directions.
The resolver never finds the cache. Trivy's pom.xml analyzer follows the parent and
imported-BOM chain to determine the versions of managed dependencies. It looks for those POMs in
~/.m2/repository; the pipeline's Maven jobs resolve into $CI_PROJECT_DIR/.m2/repository via
MAVEN_OPTS. Trivy finds nothing locally and fetches each POM from Maven Central instead — for a
Spring Boot stack, dozens per scan. Maven Central rate-limits shared CI egress IPs, and the
Retry-After: 1800 blocks every subsequent request for half an hour.
Measured on a freshly generated Java service with a populated local repository, scanning with the network cut off entirely:
| Resolved packages | |
|---|---|
| Without the pointer | 2 |
| With the pointer | 41 |
The 429 was therefore never the whole story. Even when Maven Central answered, the scan was doing work it did not need to do; when it did not answer, the job failed outright.
The walk includes the cache. The reverse problem, on the same directory. Where Trivy's analyzers recognise the cache's contents, they become part of the scan. Go demonstrates it:
| Scan targets | Packages | |
|---|---|---|
Without --skip-dirs .go-cache |
35 | 228 |
With --skip-dirs .go-cache |
1 | 13 |
The 34 surplus targets are go.mod files below .go-cache/pkg/mod — module versions the project
does not depend on, reported as if it did.
An earlier draft of this decision assumed the same held for Maven and was the second source of
the 429, on the theory that Trivy queries Maven Central by SHA-1 for each unidentified jar in the
cache. Measurement refuted it: trivy fs over a 214-jar local repository reports
Number of language-specific files num=0, with and without network access. The filesystem
analyzers match pom.xml, package-lock.json and conan.lock — file names a package cache does
not contain. Go is the exception because a module cache stores real go.mod files.
Decision Drivers¶
- The observed failure must stop, without trading it for a scan that reports less than it should.
- The pre-check tier should not depend on a third-party service being reachable and unthrottled.
- A finding that describes something outside the project's dependency graph is noise, and noise in a gating job is how gating jobs get ignored.
- Whatever is decided has to hold for four languages and nine templates, not just for Maven.
Considered Options¶
- Point Trivy at the pipeline's Maven repository, and exclude the cache from the walk.
- As 1., plus
--offline-scanto remove the network dependency entirely. - Drop
vulnfrom the source scan and rely onscan-imagefor dependency vulnerabilities. - Populate
~/.m2in the scan job before scanning.
Decision Outcome¶
Option 1.
Every one of the nine CI templates passes --skip-dirs for its own cache directory. The two Java
templates additionally write a settings.xml in before_script:
before_script:
- mkdir -p ~/.m2
- printf '<settings><localRepository>%s/.m2/repository</localRepository></settings>\n' "$CI_PROJECT_DIR" > ~/.m2/settings.xml
--skip-dirs is emitted for all four languages even though only Go needs it today. It controls
the walk over the scan target; <localRepository> controls the resolver's lookups; the two do
not interfere. In a mono-repo child the scan target is the artifact directory and the flag is
inert — it is emitted there anyway, because an artifact is free to keep a cache of its own and a
conditional would only add a way to get this wrong later.
--offline-scan is deliberately not used. It would remove the residual network dependency,
but on a cold cache Trivy then drops every dependency it cannot resolve, logs one WARN, and
exits 0. That is the same failure mode ADR-017 rejected for image scans: a gate that passes
because it had nothing to look at. A rate-limit failure is loud and rare; a silent pass is quiet
and permanent. The warm cache is the normal case — fallback_keys: [$CI_DEFAULT_BRANCH] gives
even a fresh branch the default branch's cache — so the network fallback is an exception path,
not the operating mode.
Positive Consequences¶
- The reported failure is resolved, and on a warm cache the scan touches no external service.
- The Java scan resolves 41 packages where it previously resolved 2. This is a security improvement in its own right, independent of the 429: the job had been reporting almost nothing.
- Go scans stop reporting module versions the project does not depend on.
- The pre-check tier gets measurably faster: 1 scan target instead of 35 for Go.
Negative Consequences / Risks¶
- On a genuinely cold cache — a brand-new project's first pipeline — the Maven scan still reaches
out to Maven Central and can still hit a 429. This is accepted as the loud failure mode; see
above for why the quiet one is worse. A retry after the
Retry-Afterwindow resolves it. --skip-dirsis inert in six of the nine templates. It is documented as a guard, not a fix, so that nobody later reads it as load-bearing where it is not — or removes it as dead where it is.- The
settings.xmlis written into the Trivy container's$HOME, which couples the scan job to the assumption that Trivy runs as a user with a writable home. Every image used here does.
Rejected alternatives¶
- Option 2 is the trap described above: it converts a visible failure into an invisible one.
- Option 3 would move dependency scanning to
scan-image, which sees the real resolved jars and is in that sense more accurate — but thejava/libraryprofile has no image stage at all, so its dependencies would go unscanned entirely. It also gives up fail-fast in the pre-check tier for the profiles that do have one. - Option 4 solves it by copying the cache to where Trivy expects it, doubling the disk cost of every Java job to work around a path mismatch that one line of configuration expresses directly.
Links¶
specs/guidelines/ci_cd.md— "A source scan must know about the dependency cache".- ADR-017 — the sibling decision for
trivy image; this ADR coverstrivy fs. Both refuse to make a gate green by making it blind. - Trivy respects
<localRepository>fromsettings.xml - Trivy Java coverage and
--offline-scan