Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ var flagsMap = map[string]components.Flag{
CurationOutput: components.NewStringFlag(OutputFormat, "Defines the output format of the command. Acceptable values are: table, json.", components.WithStrDefaultValue("table")),
SolutionPath: components.NewStringFlag(SolutionPath, "Path to the .NET solution file (.sln) to use when multiple solution files are present in the directory."),
IncludeCachedPackages: components.NewBoolFlag(IncludeCachedPackages, "When set to true, the system will audit cached packages. This configuration is mandatory for Curation on-demand workflows, which rely on package caching."),
MvnIncludePluginDeps: components.NewBoolFlag(MvnIncludePluginDeps, "[Maven] When set to true, Maven build-plugin transitive dependencies are included in the curation evaluation. Requires two additional Maven invocations (help:effective-pom, dependency:resolve-plugins) which may slow down the scan. By default only project dependencies are scanned."),
MvnIncludePluginDeps: components.NewBoolFlag(MvnIncludePluginDeps, "[Maven] When set to true, Maven build-plugin transitive dependencies are resolved and included in the curation evaluation. By default only project dependencies are scanned."),
LegacyPeerDeps: components.NewBoolFlag(LegacyPeerDeps, "[npm] Pass --legacy-peer-deps to npm install to bypass peer-dependency version conflicts."),
RunNative: components.NewBoolFlag(RunNative, "[npm] Use the native npm client for dependency resolution. Reads Artifactory URL and repository from the project's .npmrc registry — no 'jf npm-config' required. Respects .npmrc and Volta configuration."),
binarySca: components.NewBoolFlag(Sca, fmt.Sprintf("Selective scanners mode: Execute SCA (Software Composition Analysis) sub-scan. Use --%s to run both SCA and Contextual Analysis. Use --%s --%s to to run SCA. Can be combined with --%s.", Sca, Sca, WithoutCA, Secrets)),
Expand Down
11 changes: 5 additions & 6 deletions commands/curation/curationaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,16 +915,15 @@ func getTestCasesForDoCurationAudit() []testCase {
curationCache, err := utils.GetCurationCacheFolderByTech(techutils.Maven.String())
require.NoError(t, err)
cleanUpTestDirChange()
// One mvn invocation, multiple goals: maven-dep-tree:tree primes the project
// dep cache; dependency:resolve-plugins and help:effective-pom pre-download
// the plugins that resolvePluginDeps()/resolveInstallLifecyclePlugins() will
// re-run during the test phase against the mock server.
// Pre-populate the curation cache with all plugin artifact downloads so that
// during the actual test run against the mock server only the blocked artifact
// triggers an HTTP request. The -DincludePluginDeps=true flag causes
// maven-dep-tree to resolve plugin transitive deps in the same invocation.
return []string{
"com.jfrog:maven-dep-tree:" + java.GetMavenDepTreeVersion() + ":tree",
"-DdepsTreeOutputFile=output",
"-Dmaven.repo.local=" + curationCache,
"dependency:resolve-plugins",
"help:effective-pom",
"-DincludePluginDeps=true",
}
},
mvnIncludePluginDeps: true,
Expand Down
63 changes: 61 additions & 2 deletions sca/bom/buildinfo/technologies/java/deptreemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package java
import (
"encoding/json"
"os"
"sort"
"strings"

"github.com/jfrog/jfrog-cli-security/utils/techutils"
Expand Down Expand Up @@ -47,14 +48,27 @@ func NewDepTreeManager(params *DepTreeParams) DepTreeManager {
}

// The structure of a dependency tree of a module in a Gradle/Maven project, as created by the gradle-dep-tree and maven-dep-tree plugins.
// PluginNodes is populated by maven-dep-tree when -DincludePluginDeps=true is passed; it contains
// transitive dependencies of Maven build plugins that participate in the install lifecycle.
type moduleDepTree struct {
Root string `json:"root"`
Nodes map[string]xray.DepTreeNode `json:"nodes"`
Root string `json:"root"`
Nodes map[string]xray.DepTreeNode `json:"nodes"`
PluginNodes map[string]xray.DepTreeNode `json:"pluginNodes,omitempty"`
}

// Reads the output files of the gradle-dep-tree and maven-dep-tree plugins and returns them as a slice of GraphNodes.
// It takes the output of the plugin's run (which is a byte representation of a list of paths of the output files, separated by newlines) as input.
// Thin wrapper over getGraphAndPluginDepsFromDepTree for callers that don't need plugin deps.
func getGraphFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNode, uniqueDepsMap map[string]*xray.DepTreeNode, err error) {
depsGraph, uniqueDepsMap, _, _, err = getGraphAndPluginDepsFromDepTree(outputFilePaths)
return
}

// getGraphAndPluginDepsFromDepTree returns the dependency graph and flat unique-deps map, plus the
// plugin transitive deps emitted under "pluginNodes" when -DincludePluginDeps=true. pluginDepsMap is
// nil when none were collected; pluginNodesPresent is true when the field was emitted (even empty),
// distinguishing "ran, nothing to inject" from "plugin ignored the flag".
func getGraphAndPluginDepsFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNode, uniqueDepsMap map[string]*xray.DepTreeNode, pluginDepsMap map[string]*xray.DepTreeNode, pluginNodesPresent bool, err error) {
modules, err := parseDepTreeFiles(outputFilePaths)
if err != nil {
return
Expand All @@ -66,10 +80,55 @@ func getGraphFromDepTree(outputFilePaths string) (depsGraph []*xrayUtils.GraphNo
for depToAdd, depTypes := range moduleUniqueDeps {
uniqueDepsMap[depToAdd] = depTypes
}
// A non-nil map (including an empty {}) means the plugin emitted the field.
if module.PluginNodes != nil {
pluginNodesPresent = true
}
for gav, node := range module.PluginNodes {
if pluginDepsMap == nil {
pluginDepsMap = map[string]*xray.DepTreeNode{}
}
existing, exists := pluginDepsMap[gav]
if !exists {
pluginDepsMap[gav] = &xray.DepTreeNode{
Types: node.Types,
Classifier: node.Classifier,
Configurations: node.Configurations,
}
continue
}
// Same GAV in another module: keep the first module's classifier/configurations but
// union the types so a single-module variant (e.g. test-jar) is still curated.
existing.Types = mergePluginNodeTypes(existing.Types, node.Types)
}
}
return
}

// mergePluginNodeTypes returns the deduplicated, sorted union of two artifact-type lists, so a
// plugin dep appearing in multiple modules keeps every type variant. Returns nil when both are empty.
func mergePluginNodeTypes(existing, incoming *[]string) *[]string {
seen := map[string]struct{}{}
var merged []string
for _, src := range []*[]string{existing, incoming} {
if src == nil {
continue
}
for _, t := range *src {
if _, ok := seen[t]; ok {
continue
}
seen[t] = struct{}{}
merged = append(merged, t)
}
}
if merged == nil {
return nil
}
sort.Strings(merged)
return &merged
}

// Returns a dependency tree and a flat list of the module's dependencies for the given module
func GetModuleTreeAndDependencies(module *moduleDepTree) (*xrayUtils.GraphNode, map[string]*xray.DepTreeNode) {
moduleTreeMap := make(map[string]xray.DepTreeNode)
Expand Down
118 changes: 118 additions & 0 deletions sca/bom/buildinfo/technologies/java/deptreemanager_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package java

import (
"fmt"
"os"
"path/filepath"
"reflect"
Expand Down Expand Up @@ -84,3 +85,120 @@ func TestGetGradleGraphFromDepTreeWithCuration(t *testing.T) {
assert.NotEmpty(t, depTree)
assert.NotEmpty(t, uniqueDeps)
}

// writeDepTreeModuleFiles writes each maven-dep-tree module JSON to its own temp file and
// returns the newline-separated list of paths that getGraph*FromDepTree expects as input.
func writeDepTreeModuleFiles(t *testing.T, modulesJSON ...string) string {
dir := t.TempDir()
paths := make([]string, 0, len(modulesJSON))
for i, content := range modulesJSON {
p := filepath.Join(dir, fmt.Sprintf("module-%d.json", i))
assert.NoError(t, os.WriteFile(p, []byte(content), 0600))
paths = append(paths, p)
}
return strings.Join(paths, "\n")
}

func TestGetGraphAndPluginDepsFromDepTreeSingleModule(t *testing.T) {
moduleJSON := `{
"root": "org.example:app:1.0",
"nodes": {
"org.example:app:1.0": {"classifier": null, "types": ["jar"], "children": ["commons-io:commons-io:2.11.0"]},
"commons-io:commons-io:2.11.0": {"classifier": null, "types": ["jar"], "children": []}
},
"pluginNodes": {
"org.ow2.asm:asm:9.8": {"classifier": null, "types": ["jar"], "children": []},
"commons-io:commons-io:2.21.0": {"classifier": "sources", "types": ["jar"], "children": [], "configurations": ["compile"]}
}
}`
depsGraph, uniqueDeps, pluginDeps, pluginNodesPresent, err := getGraphAndPluginDepsFromDepTree(writeDepTreeModuleFiles(t, moduleJSON))
assert.NoError(t, err)
assert.Len(t, depsGraph, 1)
assert.NotEmpty(t, uniqueDeps)

assert.True(t, pluginNodesPresent, "pluginNodes field is present")
assert.NotNil(t, pluginDeps)
assert.Len(t, pluginDeps, 2)

asm := pluginDeps["org.ow2.asm:asm:9.8"]
assert.NotNil(t, asm)
assert.Equal(t, []string{"jar"}, *asm.Types)
assert.Nil(t, asm.Classifier)

// Classifier and Configurations must be carried over from the plugin node (A4).
commonsIo := pluginDeps["commons-io:commons-io:2.21.0"]
assert.NotNil(t, commonsIo)
assert.Equal(t, "sources", *commonsIo.Classifier)
assert.Equal(t, []string{"compile"}, *commonsIo.Configurations)
}

func TestGetGraphAndPluginDepsFromDepTreeNoPluginNodes(t *testing.T) {
moduleJSON := `{
"root": "org.example:app:1.0",
"nodes": {"org.example:app:1.0": {"classifier": null, "types": ["jar"], "children": []}}
}`
depsGraph, uniqueDeps, pluginDeps, pluginNodesPresent, err := getGraphAndPluginDepsFromDepTree(writeDepTreeModuleFiles(t, moduleJSON))
assert.NoError(t, err)
assert.Len(t, depsGraph, 1)
assert.NotEmpty(t, uniqueDeps)
// No "pluginNodes" field -> nil map and pluginNodesPresent=false, so callers can tell
// "plugin ignored the flag" from "ran but found nothing".
assert.False(t, pluginNodesPresent, "pluginNodes field is absent")
assert.Nil(t, pluginDeps)
}

func TestGetGraphAndPluginDepsFromDepTreeMultiModuleDedup(t *testing.T) {
moduleA := `{
"root": "org.example:app-a:1.0",
"nodes": {"org.example:app-a:1.0": {"classifier": null, "types": ["jar"], "children": []}},
"pluginNodes": {
"org.ow2.asm:asm:9.8": {"classifier": null, "types": ["jar"], "children": []},
"commons-io:commons-io:2.21.0": {"classifier": null, "types": ["jar"], "children": []}
}
}`
moduleB := `{
"root": "org.example:app-b:1.0",
"nodes": {"org.example:app-b:1.0": {"classifier": null, "types": ["jar"], "children": []}},
"pluginNodes": {
"org.ow2.asm:asm:9.8": {"classifier": null, "types": ["test-jar"], "children": []},
"org.codehaus.plexus:plexus-utils:4.0.2": {"classifier": null, "types": ["jar"], "children": []}
}
}`
depsGraph, _, pluginDeps, pluginNodesPresent, err := getGraphAndPluginDepsFromDepTree(writeDepTreeModuleFiles(t, moduleA, moduleB))
assert.NoError(t, err)
assert.Len(t, depsGraph, 2)

assert.True(t, pluginNodesPresent, "pluginNodes field is present")
assert.NotNil(t, pluginDeps)
assert.Len(t, pluginDeps, 3)
// Types are unioned across modules (sorted), so module B's "test-jar" isn't dropped.
asm := pluginDeps["org.ow2.asm:asm:9.8"]
assert.NotNil(t, asm)
assert.Equal(t, []string{"jar", "test-jar"}, *asm.Types)
assert.Contains(t, pluginDeps, "commons-io:commons-io:2.21.0")
assert.Contains(t, pluginDeps, "org.codehaus.plexus:plexus-utils:4.0.2")
}

func TestMergePluginNodeTypes(t *testing.T) {
sp := func(s ...string) *[]string { return &s }
cases := []struct {
name string
a, b *[]string
want *[]string
}{
{"both nil", nil, nil, nil},
{"left nil", nil, sp("jar"), sp("jar")},
{"right nil", sp("jar"), nil, sp("jar")},
{"dedup and sort", sp("test-jar", "jar"), sp("jar"), sp("jar", "test-jar")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := mergePluginNodeTypes(tc.a, tc.b)
if tc.want == nil {
assert.Nil(t, got)
return
}
assert.Equal(t, *tc.want, *got)
})
}
}
Loading
Loading