Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a583f30
feat(java): apply config-file Maven mirrors in the pom parser
DmitriyLewen Jul 24, 2026
0283653
feat(flag): add scan.maven.mirrors config option
DmitriyLewen Jul 24, 2026
b406c3c
feat(java): wire scan.maven.mirrors to the pom analyzer
DmitriyLewen Jul 24, 2026
6e8c620
docs(java): document scan.maven.mirrors
DmitriyLewen Jul 27, 2026
c8611d5
feat(flag): reject non-absolute Maven mirror URLs
DmitriyLewen Jul 27, 2026
283355f
refactor(java): simplify pom mirror comments and drop redundant debug…
DmitriyLewen Jul 27, 2026
3ca9d19
test: refactor and dedupe Maven mirror tests
DmitriyLewen Jul 27, 2026
48fa082
test(java): merge resolveMirrors settings and config-file cases
DmitriyLewen Jul 27, 2026
017028b
test(java): build TestParser_mirrorFor inputs via resolveMirrors
DmitriyLewen Jul 27, 2026
2e8edf5
chore(java): fix golangci-lint issues (gci import order, map make)
DmitriyLewen Jul 27, 2026
405a8a4
fix(flag): restrict Maven mirror URLs to http/https
DmitriyLewen Jul 28, 2026
1b5aec6
fix(java): fall back to the next mirror on 429 instead of aborting
DmitriyLewen Jul 28, 2026
b43e591
fix(flag): reject empty Maven mirror target lists
DmitriyLewen Jul 28, 2026
77e2a0f
refactor(flag): configure Maven mirrors as a list instead of a map
DmitriyLewen Jul 30, 2026
0bb1700
fix(java): ignore credentials and host case when matching Maven mirrors
DmitriyLewen Jul 30, 2026
07b1cf7
refactor(java): use errors.AsType for the rate limit check
DmitriyLewen Jul 30, 2026
8f734a1
fix(java): clean the path when building a Maven mirror key
DmitriyLewen Aug 3, 2026
eb6d748
docs(java): correct how a 429 from a Maven mirror is described
DmitriyLewen Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/guide/coverage/language/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,40 @@ The vulnerability database will be downloaded anyway.
!!! Warning
Trivy may skip some dependencies (that were not found on your local machine) when the `--offline-scan` flag is passed.

### mirrors
Trivy supports several ways to set up mirrors for Maven repositories:

- `<mirrors>` in your Maven [`settings.xml`][maven-mirror-settings] — both the global and the user file.
- The Trivy [config file][config-file] — see [config-file mirrors](#config-file-mirrors) below.

#### resolving priority
For each package that needs to be fetched from a remote repository, Trivy applies the following order:

1. mirror from `settings.xml`;
2. mirrors[^10] from the config file.

!!! note
Trivy supports chained resolution across the two sources: if `settings.xml` maps `repo1 -> repo2` and the config file maps `repo2 -> repo3`, then `repo1` resolves to `repo3`.

#### config-file mirrors
`scan.maven.mirrors` is a list of entries, each mapping a `source` repository URL to the ordered `targets` that mirror it, tried in turn. Use it to avoid modifying `settings.xml` (for example in CI) and to configure several fallback mirrors[^10] for a single repository:

```yaml
scan:
maven:
mirrors:
- source: https://repo.maven.apache.org/maven2/
targets:
- https://my-internal-mirror/maven2/
- https://backup-mirror/maven2/
```

To mirror Maven Central, use `https://repo.maven.apache.org/maven2/` as the `source`.
As in Maven, a mirrored repository is never queried directly, so a dependency is reported as not found when every mirror of it fails.

!!! warning "Credentials"
Config-file mirrors do not read credentials from `settings.xml` `<server>` entries. To authenticate, embed them in the mirror URL (`https://user:password@host/...`), which stores the password in plaintext in the config file. For a secure setup, configure the mirror in `settings.xml` instead.

### supported scopes
Trivy only scans `import`, `compile`, `runtime` and empty [maven scopes][maven-scopes]. Other scopes and `Optional` dependencies are not currently being analyzed.

Expand Down Expand Up @@ -142,11 +176,14 @@ Make sure that you have cache[^8] directory to find licenses from `*.pom` depend
[^7]: To avoid confusion, Trivy only finds locations for direct dependencies from the base pom.xml file.
[^8]: The supported directories are `$GRADLE_USER_HOME/caches` and `$HOME/.gradle/caches` (`%HOMEPATH%\.gradle\caches` for Windows).
[^9]: License detection is limited. See [Licenses](#licenses) for details.
[^10]: The mirrors are tried in order, falling back to the next one when the requested POM is not found.

[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
[maven-invoker-plugin]: https://maven.apache.org/plugins/maven-invoker-plugin/usage.html
[maven-central]: https://repo.maven.apache.org/maven2/
[maven-pom-repos]: https://maven.apache.org/settings.html#repositories
[maven-mirror-settings]: https://maven.apache.org/guides/mini/guide-mirror-settings.html
[config-file]: ../../references/configuration/config-file.md
[maven-scopes]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Scope
[sbt-dependency-lock]: https://stringbean.github.io/sbt-dependency-lock
[detection-priority]: ../../scanner/vulnerability.md#detection-priority
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/references/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,9 @@ scan:
# Same as '--file-patterns'
file-patterns: []

maven:
mirrors: []

# Same as '--offline-scan'
offline: false

Expand Down
5 changes: 5 additions & 0 deletions docs/guide/references/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ The block applies to *all* subsequent requests from the affected IP for the dura
Recommended mitigations:

- **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.
- **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:
- `<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.
- [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.
Comment thread
knqyf263 marked this conversation as resolved.
Outdated
- **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.
- **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.

Expand Down Expand Up @@ -351,5 +354,7 @@ $ trivy clean --all
```

[air-gapped]: ../advanced/air-gap.md
[maven-mirror-settings]: https://maven.apache.org/guides/mini/guide-mirror-settings.html
[maven-mirrors]: ../coverage/language/java.md#config-file-mirrors
[network]: ../advanced/air-gap.md#connectivity-requirements
[redis-cache]: ../configuration/cache.md#redis
28 changes: 28 additions & 0 deletions magefiles/config_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/google/jsonschema-go/jsonschema"
"github.com/samber/lo"

"github.com/aquasecurity/trivy/pkg/flag"
)
Expand Down Expand Up @@ -151,6 +152,33 @@ func schemaFromFlagValue(val any) (*jsonschema.Schema, error) {
Items: &jsonschema.Schema{Type: schemaTypeString},
},
}, nil
case []flag.MavenMirror:
return &jsonschema.Schema{
Type: schemaTypeArray,
Items: &jsonschema.Schema{
Type: schemaTypeObject,
Properties: map[string]*jsonschema.Schema{
"source": {
Type: schemaTypeString,
Description: "URL of the mirrored Maven repository",
},
"targets": {
Type: schemaTypeArray,
Description: "URLs of the mirrors serving the repository, tried in order",
Items: &jsonschema.Schema{Type: schemaTypeString},
MinItems: lo.ToPtr(1),
},
},
PropertyOrder: []string{
"source",
"targets",
},
Required: []string{
"source",
"targets",
},
},
}, nil
default:
return nil, fmt.Errorf("unknown type %T, please update schemaFromFlagValue()", val)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/commands/artifact/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ func (r *runner) initScannerConfig(ctx context.Context, opts flag.Options) (Scan
AWSEndpoint: opts.Endpoint,
FileChecksum: fileChecksum,
DetectionPriority: opts.DetectionPriority,
MavenMirrors: opts.MavenMirrors,

// For image scanning
ImageOption: ftypes.ImageOptions{
Expand Down
71 changes: 62 additions & 9 deletions pkg/dependency/parser/java/pom/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"net/url"
"strings"

"github.com/samber/lo"

"github.com/aquasecurity/trivy/pkg/log"
)

Expand All @@ -17,14 +19,21 @@ type mirror struct {
url url.URL // parsed URL with userinfo from the matching <server>
}

// resolveMirrors converts <mirror> entries from settings.xml into the runtime
// mirror form: split and trim the mirrorOf patterns, parse the URL, and embed
// credentials from the <server> whose id equals the mirror id. Mirrors with
// no usable pattern or an unparsable URL are dropped.
func resolveMirrors(mirrors []Mirror, servers []Server) []mirror {
// mirrors holds the resolved mirrors from settings.xml and from the config file.
type mirrors struct {
settings []mirror // settings.xml mirrors
configFile map[string][]url.URL // config-file mirrors; key: mirrorKey(source), value: ordered parsed mirror URL
}

// resolveMirrors resolves and validates both mirror sources into their runtime form:
// it parses every URL — embedding <server> credentials into settings.xml mirrors and
// normalizing config-file keys via mirrorKey — and drops any entry with an unusable
// pattern or an unparsable URL.
func resolveMirrors(settingsMirrors []Mirror, servers []Server, configFileMirrors map[string][]string) mirrors {
logger := log.WithPrefix("pom")
var result []mirror
for _, m := range mirrors {

var resolved mirrors
for _, m := range settingsMirrors {
var patterns []string
for p := range strings.SplitSeq(m.MirrorOf, ",") {
p = strings.TrimSpace(p)
Expand Down Expand Up @@ -55,13 +64,57 @@ func resolveMirrors(mirrors []Mirror, servers []Server) []mirror {
}

logger.Debug("Adding mirror", log.String("id", m.ID), log.String("url", u.Redacted()))
result = append(result, mirror{
resolved.settings = append(resolved.settings, mirror{
id: m.ID,
patterns: patterns,
url: *u,
})
}
return result

for src, targets := range configFileMirrors {
// Config-file mirror URLs are validated when the config file is parsed (fail-fast).
srcURL, err := url.Parse(src)
if err != nil {
continue
}

var mirrorURLs []url.URL
for _, target := range targets {
mirrorURL, err := url.Parse(target)
if err != nil {
continue
}
mirrorURLs = append(mirrorURLs, *mirrorURL)
}
if len(mirrorURLs) == 0 {
continue
}
logger.Debug("Added config-file mirror", log.String("source", srcURL.Redacted()),
log.Any("mirrors", lo.Map(mirrorURLs, func(u url.URL, _ int) string {
return u.Redacted()
})))
if resolved.configFile == nil {
resolved.configFile = make(map[string][]url.URL)
}
resolved.configFile[mirrorKey(*srcURL)] = mirrorURLs
}

return resolved
}

// mirrorKey normalizes a repository URL to the key used for config-file mirror lookup:
// its string form with any trailing slash trimmed, so that "https://host/maven2/" and
// "https://host/maven2" resolve to the same key.
//
// The key has to identify the repository, so the parts that don't are dropped as well:
// the credentials that Trivy embeds from a <server> — otherwise a mirrored repository
// with credentials would never match its configured key — and the case of the host,
// which RFC 3986 defines as case-insensitive. The path is kept as it is, being
// case-sensitive.
func mirrorKey(u url.URL) string {
u.User = nil
u.Host = strings.ToLower(u.Host)
return strings.TrimRight(u.String(), "/")
Comment thread
knqyf263 marked this conversation as resolved.
Outdated
}

// matches reports whether this mirror should serve the given repository.
Expand Down
Loading