diff --git a/docs/docs/coverage/language/java.md b/docs/docs/coverage/language/java.md index 67cd8c135b9d..26bad288e552 100644 --- a/docs/docs/coverage/language/java.md +++ b/docs/docs/coverage/language/java.md @@ -12,12 +12,12 @@ Each artifact supports the following scanners: The following table provides an outline of the features Trivy offers. -| Artifact | Internet access | Dev dependencies | [Dependency graph][dependency-graph] | Position | [Detection Priority][detection-priority] | -|------------------|:---------------------:|:----------------:|:------------------------------------:|:--------:|:----------------------------------------:| -| JAR/WAR/PAR/EAR | Trivy Java DB | Include | - | - | Not needed | -| pom.xml | Maven repository [^1] | Exclude | ✓ | ✓[^7] | - | -| *gradle.lockfile | - | Exclude | ✓ | ✓ | Not needed | -| *.sbt.lock | - | Exclude | - | ✓ | Not needed | +| Artifact | Internet access | Dev dependencies | [Dependency graph][dependency-graph] | Position | [Detection Priority][detection-priority] | +|------------------|:---------------------:|:------------------:|:------------------------------------:|:--------:|:----------------------------------------:| +| JAR/WAR/PAR/EAR | Trivy Java DB | Include | - | - | Not needed | +| pom.xml | Maven repository [^1] | [Exclude](#scopes) | ✓ | ✓[^7] | - | +| *gradle.lockfile | - | Exclude | ✓ | ✓ | Not needed | +| *.sbt.lock | - | Exclude | - | ✓ | Not needed | These may be enabled or disabled depending on the target. See [here](./index.md) for the detail. @@ -69,6 +69,11 @@ 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. +### scopes +Trivy supports `runtime`, `compile`, `test` and `import` (for `dependencyManagement`) [dependency scopes][dependency-scopes]. +Dependencies without scope are also detected. + +By default, Trivy doesn't report dependencies with `test` scope. Use the `--include-dev-deps` flag to include them. ### maven-invoker-plugin Typically, the integration tests directory (`**/[src|target]/it/*/pom.xml`) of [maven-invoker-plugin][maven-invoker-plugin] doesn't contain actual `pom.xml` files and should be skipped to avoid noise. @@ -120,3 +125,4 @@ Make sure that you have cache[^8] directory to find licenses from `*.pom` depend [maven-pom-repos]: https://maven.apache.org/settings.html#repositories [sbt-dependency-lock]: https://stringbean.github.io/sbt-dependency-lock [detection-priority]: ../../scanner/vulnerability.md#detection-priority +[dependency-scopes]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Scope diff --git a/pkg/dependency/parser/java/pom/artifact.go b/pkg/dependency/parser/java/pom/artifact.go index b2e97efb229b..f691afac5ebd 100644 --- a/pkg/dependency/parser/java/pom/artifact.go +++ b/pkg/dependency/parser/java/pom/artifact.go @@ -27,6 +27,7 @@ type artifact struct { Module bool Relationship ftypes.Relationship + Test bool Locations ftypes.Locations } diff --git a/pkg/dependency/parser/java/pom/parse.go b/pkg/dependency/parser/java/pom/parse.go index 1add19a4b53b..8b5ee0ca28f3 100644 --- a/pkg/dependency/parser/java/pom/parse.go +++ b/pkg/dependency/parser/java/pom/parse.go @@ -118,6 +118,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc return p.parseRoot(root.artifact(), make(map[string]struct{})) } +// nolint: gocyclo func (p *Parser) parseRoot(root artifact, uniqModules map[string]struct{}) ([]ftypes.Package, []ftypes.Dependency, error) { // Prepare a queue for dependencies queue := newArtifactQueue() @@ -161,7 +162,16 @@ func (p *Parser) parseRoot(root artifact, uniqModules map[string]struct{}) ([]ft // For soft requirements, skip dependency resolution that has already been resolved. if uniqueArt, ok := uniqArtifacts[art.Name()]; ok { + // Check that both artifact and saved artifact has `test` scope. + // Otherwise, it is non-test dependency + art.Test = uniqueArt.Test && art.Test if !uniqueArt.Version.shouldOverride(art.Version) { + // If saved artifact is `test`, but new artifact is `non-test`, + // then mark saved artifact as `non-test`. + if !art.Test && uniqueArt.Test { + uniqueArt.Test = art.Test + uniqArtifacts[art.Name()] = uniqueArt + } continue } // mark artifact as Direct, if saved artifact is Direct @@ -214,6 +224,7 @@ func (p *Parser) parseRoot(root artifact, uniqModules map[string]struct{}) ([]ft Licenses: result.artifact.Licenses, Relationship: art.Relationship, Locations: art.Locations, + Test: art.Test, } // save only dependency names @@ -234,6 +245,7 @@ func (p *Parser) parseRoot(root artifact, uniqModules map[string]struct{}) ([]ft Licenses: art.Licenses, Relationship: art.Relationship, Locations: art.Locations, + Dev: art.Test, } pkgs = append(pkgs, pkg) @@ -301,6 +313,7 @@ func (p *Parser) resolve(art artifact, rootDepManagement []pomDependency) (analy result, err := p.analyze(pomContent, analysisOptions{ exclusions: art.Exclusions, depManagement: rootDepManagement, + testScope: art.Test, }) if err != nil { return analysisResult{}, xerrors.Errorf("analyze error: %w", err) @@ -323,6 +336,7 @@ type analysisOptions struct { exclusions map[string]struct{} depManagement []pomDependency // from the root POM lineNumber bool // Save line numbers + testScope bool } func (p *Parser) analyze(pom *pom, opts analysisOptions) (analysisResult, error) { @@ -400,7 +414,7 @@ func (p *Parser) parseDependencies(deps []pomDependency, props map[string]string // Resolve dependencies d = d.Resolve(props, depManagement, rootDepManagement) - if (d.Scope != "" && d.Scope != "compile" && d.Scope != "runtime") || d.Optional { + if (d.Scope != "" && d.Scope != "compile" && d.Scope != "runtime" && d.Scope != "test") || d.Optional { continue } diff --git a/pkg/dependency/parser/java/pom/parse_test.go b/pkg/dependency/parser/java/pom/parse_test.go index 934085d5d536..94868359c08d 100644 --- a/pkg/dependency/parser/java/pom/parse_test.go +++ b/pkg/dependency/parser/java/pom/parse_test.go @@ -1499,6 +1499,90 @@ func TestPom_Parse(t *testing.T) { }, }, }, + // [INFO] --- dependency:3.7.0:tree (default-cli) @ test-example --- + // [INFO] com.example:test-example:jar:1.0.0 + // [INFO] +- org.example:example-nested:jar:3.3.3:compile + // [INFO] | \- org.example:example-dependency:jar:1.2.3:compile + // [INFO] \- org.example:example-dependency2:jar:2.3.4:test + // [INFO] \- org.example:example-api:jar:1.7.30:compile + { + name: "include dependencies with test scope", + inputFile: filepath.Join("testdata", "test-scope", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:test-example:1.0.0", + Name: "com.example:test-example", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency2:2.3.4", + Name: "org.example:example-dependency2", + Version: "2.3.4", + Relationship: ftypes.RelationshipDirect, + Dev: true, + Locations: ftypes.Locations{ + { + StartLine: 18, + EndLine: 23, + }, + }, + }, + { + ID: "org.example:example-nested:3.3.3", + Name: "org.example:example-nested", + Version: "3.3.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 13, + EndLine: 17, + }, + }, + }, + { + ID: "org.example:example-api:1.7.30", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "org.example:example-dependency:1.2.3", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:test-example:1.0.0", + DependsOn: []string{ + "org.example:example-dependency2:2.3.4", + "org.example:example-nested:3.3.3", + }, + }, + { + ID: "org.example:example-dependency2:2.3.4", + DependsOn: []string{ + "org.example:example-api:1.7.30", + }, + }, + { + ID: "org.example:example-dependency:1.2.3", + DependsOn: []string{ + "org.example:example-api:1.7.30", + }, + }, + { + ID: "org.example:example-nested:3.3.3", + DependsOn: []string{ + "org.example:example-dependency:1.2.3", + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/dependency/parser/java/pom/pom.go b/pkg/dependency/parser/java/pom/pom.go index 889d107c3c6c..75c4ad0320f2 100644 --- a/pkg/dependency/parser/java/pom/pom.go +++ b/pkg/dependency/parser/java/pom/pom.go @@ -303,6 +303,7 @@ func (d pomDependency) ToArtifact(opts analysisOptions) artifact { Exclusions: exclusions, Locations: locations, Relationship: ftypes.RelationshipIndirect, // default + Test: d.Scope == "test" || opts.testScope, } } diff --git a/pkg/dependency/parser/java/pom/testdata/test-scope/pom.xml b/pkg/dependency/parser/java/pom/testdata/test-scope/pom.xml new file mode 100644 index 000000000000..1afdc18698fb --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/test-scope/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + com.example + test-example + 1.0.0 + + test-example + Example + + + + org.example + example-nested + 3.3.3 + + + org.example + example-dependency2 + 2.3.4 + test + + +