Skip to content

Commit 0bb1700

Browse files
committed
fix(java): ignore credentials and host case when matching Maven mirrors
Trivy embeds the credentials of a matching <server> into the repository URL, and `mirrorKey` derived the lookup key from the whole URL, so a mirrored repository with credentials never matched its configured entry — silently. The key is now built from the parts that identify a repository: the credentials are dropped and the host is lower-cased, as RFC 3986 defines it as case-insensitive, while the case-sensitive path is kept. `scan.maven.mirrors` rejects entries that differ only by those, since they collapse into a single key.
1 parent 77e2a0f commit 0bb1700

5 files changed

Lines changed: 125 additions & 7 deletions

File tree

docs/guide/references/troubleshooting.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ The block applies to *all* subsequent requests from the affected IP for the dura
136136
Recommended mitigations:
137137

138138
- **Populate `~/.m2` before scanning.** Run `mvn dependency:resolve` (or any build step that resolves dependencies) so that every POM is cached locally. In CI, cache the `~/.m2` directory between runs (e.g. keyed on `pom.xml` checksums) so subsequent runs reuse the artifacts.
139+
- **Configure mirrors** of the rate-limited repository, so that POM lookups go to a host that isn't blocking you. There are two ways to do it:
140+
- `<mirrors>` in Maven's [settings.xml][maven-mirror-settings] — the standard mechanism, honored by `mvn` itself as well. A repository is served by a single mirror, so a mirror that is rate-limited too leaves nothing to fall back on.
141+
- [scan.maven.mirrors][maven-mirrors] in `trivy.yaml` — Trivy-specific, and takes an ordered list of mirrors per repository. A mirror that returns `429` is skipped in favor of the next one, and the scan fails only once every mirror of an artifact is rate-limited.
139142
- **Wait for the block to expire.** The `Retry-After` value in the error tells you the minimum wait. Repeated scans during the block will extend it.
140143
- **Use `--offline-scan`** to skip remote lookups entirely and rely only on the local `~/.m2` cache. Be careful: any transitive POM missing from the cache is silently skipped, so populate `~/.m2` first (see above) — otherwise the dependency tree will be incomplete.
141144

@@ -351,5 +354,7 @@ $ trivy clean --all
351354
```
352355

353356
[air-gapped]: ../advanced/air-gap.md
357+
[maven-mirror-settings]: https://maven.apache.org/guides/mini/guide-mirror-settings.html
358+
[maven-mirrors]: ../coverage/language/java.md#config-file-mirrors
354359
[network]: ../advanced/air-gap.md#connectivity-requirements
355360
[redis-cache]: ../configuration/cache.md#redis

pkg/dependency/parser/java/pom/mirror.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,18 @@ func resolveMirrors(settingsMirrors []Mirror, servers []Server, configFileMirror
102102
return resolved
103103
}
104104

105-
// mirrorKey normalizes a repository URL to the key used for config-file mirror
106-
// lookup: its string form with any trailing slash trimmed, so that
107-
// "https://host/maven2/" and "https://host/maven2" resolve to the same key.
105+
// mirrorKey normalizes a repository URL to the key used for config-file mirror lookup:
106+
// its string form with any trailing slash trimmed, so that "https://host/maven2/" and
107+
// "https://host/maven2" resolve to the same key.
108+
//
109+
// The key has to identify the repository, so the parts that don't are dropped as well:
110+
// the credentials that Trivy embeds from a <server> — otherwise a mirrored repository
111+
// with credentials would never match its configured key — and the case of the host,
112+
// which RFC 3986 defines as case-insensitive. The path is kept as it is, being
113+
// case-sensitive.
108114
func mirrorKey(u url.URL) string {
115+
u.User = nil
116+
u.Host = strings.ToLower(u.Host)
109117
return strings.TrimRight(u.String(), "/")
110118
}
111119

pkg/dependency/parser/java/pom/mirror_test.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ func TestParser_mirrorFor(t *testing.T) {
348348
tests := []struct {
349349
name string
350350
settingsMirrors []Mirror
351+
servers []Server
351352
configMirrors map[string][]string
352353
repo repository
353354
want []repository
@@ -482,6 +483,45 @@ func TestParser_mirrorFor(t *testing.T) {
482483
},
483484
},
484485
},
486+
{
487+
// Trivy embeds <server> credentials into the repository URL, so the lookup
488+
// must ignore them — the configured key never carries a password.
489+
name: "config file: repository credentials are ignored by the lookup",
490+
configMirrors: map[string][]string{
491+
"https://repo1.example.com/maven2": {"https://repo3.example.com/maven2"},
492+
},
493+
repo: repository{
494+
id: "corp",
495+
url: mustParseURL(t, "https://repo-user:repo-pass@repo1.example.com/maven2"),
496+
releaseEnabled: true,
497+
},
498+
want: []repository{
499+
{
500+
id: "corp",
501+
url: mustParseURL(t, "https://repo3.example.com/maven2"),
502+
releaseEnabled: true,
503+
},
504+
},
505+
},
506+
{
507+
// RFC 3986 defines the host as case-insensitive, unlike the path.
508+
name: "config file: host case is ignored by the lookup",
509+
configMirrors: map[string][]string{
510+
"https://Repo1.Example.COM/maven2": {"https://repo3.example.com/maven2"},
511+
},
512+
repo: repository{
513+
id: "central",
514+
url: mustParseURL(t, "https://repo1.example.com/maven2"),
515+
releaseEnabled: true,
516+
},
517+
want: []repository{
518+
{
519+
id: "central",
520+
url: mustParseURL(t, "https://repo3.example.com/maven2"),
521+
releaseEnabled: true,
522+
},
523+
},
524+
},
485525
{
486526
// Several mirrors for one repository become ordered fallback candidates.
487527
name: "config file: fallback list — repo1 -> [repo3, repo4] in order",
@@ -555,11 +595,38 @@ func TestParser_mirrorFor(t *testing.T) {
555595
},
556596
},
557597
},
598+
{
599+
// Chaining through a mirror that has <server> credentials: pass 1 rewrites the
600+
// repository to the mirror URL with the credentials embedded, so the config-file
601+
// lookup in pass 2 sees them and must still match the plain configured key.
602+
name: "cross-source: chaining through a mirror with credentials",
603+
settingsMirrors: []Mirror{
604+
{ID: "settings-mirror", MirrorOf: "central", URL: "https://repo2.example.com/maven2"},
605+
},
606+
servers: []Server{
607+
{ID: "settings-mirror", Username: "mirror-user", Password: "mirror-pass"},
608+
},
609+
configMirrors: map[string][]string{
610+
"https://repo2.example.com/maven2": {"https://repo3.example.com/maven2"},
611+
},
612+
repo: repository{
613+
id: "central",
614+
url: mustParseURL(t, "https://repo1.example.com/maven2"),
615+
releaseEnabled: true,
616+
},
617+
want: []repository{
618+
{
619+
id: "settings-mirror",
620+
url: mustParseURL(t, "https://repo3.example.com/maven2"),
621+
releaseEnabled: true,
622+
},
623+
},
624+
},
558625
}
559626

560627
for _, tt := range tests {
561628
t.Run(tt.name, func(t *testing.T) {
562-
p := &Parser{mirrors: resolveMirrors(tt.settingsMirrors, nil, tt.configMirrors)}
629+
p := &Parser{mirrors: resolveMirrors(tt.settingsMirrors, tt.servers, tt.configMirrors)}
563630
require.Equal(t, tt.want, p.mirrorFor(tt.repo))
564631
})
565632
}

pkg/flag/scan_flags.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,9 +305,12 @@ func parseMavenMirrors(mirrors []MavenMirror) (map[string][]string, error) {
305305
return nil, xerrors.Errorf("invalid Maven mirror URL in 'scan.maven.mirrors' for %s", src.Redacted())
306306
}
307307
}
308-
// Repositories are matched ignoring the trailing slash, so entries that differ only by
309-
// it are duplicates and would otherwise silently overwrite each other.
310-
key := strings.TrimRight(mirror.Source, "/")
308+
// The parser looks a repository up by the same key: without credentials, with a
309+
// lower-cased host and without the trailing slash. Entries differing only by those
310+
// collapse into one key there, so they are duplicates and are rejected here.
311+
src.User = nil
312+
src.Host = strings.ToLower(src.Host)
313+
key := strings.TrimRight(src.String(), "/")
311314
if _, ok := parsed[key]; ok {
312315
return nil, xerrors.Errorf("duplicate Maven repository in 'scan.maven.mirrors': %s", src.Redacted())
313316
}

pkg/flag/scan_flags_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,41 @@ func TestScanFlagGroup_ToOptions(t *testing.T) {
220220
},
221221
assertion: require.Error,
222222
},
223+
{
224+
// The parser matches repositories without their credentials, so these two
225+
// would collapse into a single entry.
226+
name: "maven repositories differing only by credentials are rejected",
227+
fields: fields{
228+
mavenMirrors: []flag.MavenMirror{
229+
{
230+
Source: "https://first:pass@nexus.example.com/maven2/",
231+
Targets: []string{"https://my-internal-mirror/maven2/"},
232+
},
233+
{
234+
Source: "https://second:pass@nexus.example.com/maven2/",
235+
Targets: []string{"https://backup-mirror/maven2/"},
236+
},
237+
},
238+
},
239+
assertion: require.Error,
240+
},
241+
{
242+
// The host is case-insensitive, so these two would collapse as well.
243+
name: "maven repositories differing only by host case are rejected",
244+
fields: fields{
245+
mavenMirrors: []flag.MavenMirror{
246+
{
247+
Source: "https://nexus.example.com/maven2/",
248+
Targets: []string{"https://my-internal-mirror/maven2/"},
249+
},
250+
{
251+
Source: "https://Nexus.Example.COM/maven2/",
252+
Targets: []string{"https://backup-mirror/maven2/"},
253+
},
254+
},
255+
},
256+
assertion: require.Error,
257+
},
223258
}
224259

225260
for _, tt := range tests {

0 commit comments

Comments
 (0)