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
4 changes: 3 additions & 1 deletion cli/docs/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ const (
DockerImageName = "image"
SolutionPath = "solution-path"
IncludeCachedPackages = "include-cached-packages"
AuditPackage = "package"

// Unique git flags
InputFile = "input-file"
Expand Down Expand Up @@ -217,7 +218,7 @@ var commandFlags = map[string][]string{
StaticSca, XrayLibPluginBinaryCustomPath, AnalyzerManagerCustomPath, AddSastRules,
},
CurationAudit: {
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, UseIncludedBuilds, SolutionPath, DockerImageName, IncludeCachedPackages,
CurationOutput, WorkingDirs, Threads, RequirementsFile, InsecureTls, useWrapperAudit, UseIncludedBuilds, SolutionPath, DockerImageName, IncludeCachedPackages, AuditPackage,
},
GitCountContributors: {
InputFile, ScmType, ScmApiUrl, Token, Owner, RepoName, Months, DetailedSummary, InsecureTls,
Expand Down Expand Up @@ -338,6 +339,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."),
AuditPackage: components.NewStringFlag(AuditPackage, "Run curation audit on a specific package and its transitive dependencies. Format: <packageName>@<packageVersion> (e.g. express@4.18.2 for npm, github.com/pkg/errors@v0.9.1 for Go)."),
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)),
binarySecrets: components.NewBoolFlag(Secrets, fmt.Sprintf("Selective scanners mode: Execute Secrets sub-scan. Can be combined with --%s.", Sca)),
binaryWithoutCA: components.NewBoolFlag(WithoutCA, fmt.Sprintf("Selective scanners mode: Disable Contextual Analysis scanner after SCA. Relevant only with --%s flag.", Sca)),
Expand Down
1 change: 1 addition & 0 deletions cli/scancommands.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ func getCurationCommand(c *components.Context) (*curation.CurationAuditCommand,
SetSolutionFilePath(c.GetStringFlagValue(flags.SolutionPath))
curationAuditCommand.SetDockerImageName(c.GetStringFlagValue(flags.DockerImageName))
curationAuditCommand.SetIncludeCachedPackages(c.GetBoolFlagValue(flags.IncludeCachedPackages))
curationAuditCommand.SetAuditPackage(c.GetStringFlagValue(flags.AuditPackage))
return curationAuditCommand, nil
}

Expand Down
50 changes: 50 additions & 0 deletions commands/curation/curationaudit.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ type CurationAuditCommand struct {
parallelRequests int
dockerImageName string
includeCachedPackages bool
auditPackage string
audit.AuditParamsInterface
}

Expand Down Expand Up @@ -274,7 +275,15 @@ func (ca *CurationAuditCommand) SetIncludeCachedPackages(includeCachedPackages b
return ca
}

func (ca *CurationAuditCommand) SetAuditPackage(spec string) *CurationAuditCommand {
ca.auditPackage = spec
return ca
}

func (ca *CurationAuditCommand) Run() (err error) {
if ca.auditPackage != "" {
return ca.runSpecificPackageMode()
}
rootDir, err := os.Getwd()
if err != nil {
return errorutils.CheckError(err)
Expand Down Expand Up @@ -443,6 +452,7 @@ func (ca *CurationAuditCommand) getBuildInfoParamsByTech() (technologies.BuildIn
InstallCommandArgs: ca.InstallCommandArgs(),
// Curation params
IsCurationCmd: true,
IsPackageMode: ca.auditPackage != "",
// Java params
IsMavenDepTreeInstalled: true,
UseWrapper: ca.UseWrapper(),
Expand Down Expand Up @@ -475,6 +485,9 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map
if len(depTreeResult.FullDepTrees) == 0 {
return errorutils.CheckErrorf("found no dependencies for the audited project using '%v' as the package manager", tech.String())
}
if params.IsPackageMode && ca.auditPackage != "" {
filterAuditPackageTree(&depTreeResult, tech, ca.auditPackage)
}
rtManager, serverDetails, err := ca.getRtManagerAndAuth(tech)
if err != nil {
return err
Expand Down Expand Up @@ -542,6 +555,43 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map
return err
}

// filterAuditPackageTree modifies depTreeResult in place to keep only the
// requested package and its transitive dependencies. This ensures that in --package mode
// we only audit the new package, not the customer's existing dependencies.
func filterAuditPackageTree(depTreeResult *buildinfo.DependencyTreeResult, tech techutils.Technology, installPkgName string) {
prefix := tech.GetXrayPackageTypeId() + installPkgName + ":"
for _, tree := range depTreeResult.FullDepTrees {
for _, child := range tree.Nodes {
if strings.HasPrefix(child.Id, prefix) {
tree.Nodes = []*xrayUtils.GraphNode{child}
break
}
}
}
reachable := map[string]struct{}{}
for _, tree := range depTreeResult.FullDepTrees {
reachable[tree.Id] = struct{}{}
collectReachableNodes(tree, reachable)
}
var filteredFlat []*xrayUtils.GraphNode
for _, node := range depTreeResult.FlatTree.Nodes {
if _, ok := reachable[node.Id]; ok {
filteredFlat = append(filteredFlat, node)
}
}
depTreeResult.FlatTree.Nodes = filteredFlat
}

func collectReachableNodes(node *xrayUtils.GraphNode, reachable map[string]struct{}) {
for _, child := range node.Nodes {
if _, seen := reachable[child.Id]; seen {
continue
}
reachable[child.Id] = struct{}{}
collectReachableNodes(child, reachable)
}
}

func getSelectedPackages(requestedRows string, blockedPackages []*PackageStatus) (selectedPackages []*PackageStatus, ok bool) {
// Accepts the following formats: "all", or a comma-separated list of row numbers, or ranges of row numbers."
validFormat := regexp.MustCompile(`^(all|(\d+(-\d+)?)(,\d+(-\d+)?)*$)`)
Expand Down
121 changes: 121 additions & 0 deletions commands/curation/curationaudit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"sync"
"testing"

"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo"
"github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/java"
"github.com/jfrog/jfrog-cli-security/utils/formats"

Expand Down Expand Up @@ -545,6 +546,9 @@ func createCurationCmdAndRun(tt testCase) (cmdResults map[string]*CurationReport
curationCmd.SetInsecureTls(true)
curationCmd.SetIgnoreConfigFile(tt.shouldIgnoreConfigFile)
curationCmd.SetInsecureTls(tt.allowInsecureTls)
if tt.specificPackage != "" {
curationCmd.SetAuditPackage(tt.specificPackage)
}
cmdResults = map[string]*CurationReport{}
err = curationCmd.doCurateAudit(cmdResults)
return
Expand Down Expand Up @@ -600,6 +604,7 @@ type testCase struct {
tech techutils.Technology
createServerWithoutCreds bool
allowInsecureTls bool
specificPackage string
}

func (tc testCase) getPathToTests() string {
Expand Down Expand Up @@ -1636,3 +1641,119 @@ func TestSendWaiverRequests(t *testing.T) {
})
}
}

func TestFilterAuditPackageTree(t *testing.T) {
tests := []struct {
name string
installPkgName string
tech techutils.Technology
tree *xrayUtils.GraphNode
flatNodes []*xrayUtils.GraphNode
expectedChildren []string
expectedFlatCount int
}{
{
name: "npm - keep install package and transitives, drop others",
installPkgName: "express",
tech: techutils.Npm,
tree: &xrayUtils.GraphNode{
Id: "npm://my-project:1.0.0",
Nodes: []*xrayUtils.GraphNode{
{Id: "npm://react:18.0.0"},
{
Id: "npm://express:4.18.2",
Nodes: []*xrayUtils.GraphNode{
{
Id: "npm://body-parser:1.20.1",
Nodes: []*xrayUtils.GraphNode{
{Id: "npm://raw-body:2.5.1"},
},
},
{Id: "npm://cookie:0.5.0"},
},
},
{Id: "npm://lodash:4.17.21"},
},
},
flatNodes: []*xrayUtils.GraphNode{
{Id: "npm://react:18.0.0"},
{Id: "npm://express:4.18.2"},
{Id: "npm://body-parser:1.20.1"},
{Id: "npm://raw-body:2.5.1"},
{Id: "npm://cookie:0.5.0"},
{Id: "npm://lodash:4.17.21"},
},
expectedChildren: []string{"npm://express:4.18.2"},
expectedFlatCount: 4,
},
{
name: "go - keep install package and deep transitives",
installPkgName: "rsc.io/quote",
tech: techutils.Go,
tree: &xrayUtils.GraphNode{
Id: "go://my-project",
Nodes: []*xrayUtils.GraphNode{
{Id: "go://github.com/existing/dep:v1.0.0"},
{
Id: "go://rsc.io/quote:v1.5.2",
Nodes: []*xrayUtils.GraphNode{
{
Id: "go://rsc.io/sampler:v1.3.0",
Nodes: []*xrayUtils.GraphNode{
{Id: "go://golang.org/x/text:v0.0.0-20170915032832-14c0d48ead0c"},
},
},
},
},
},
},
flatNodes: []*xrayUtils.GraphNode{
{Id: "go://github.com/existing/dep:v1.0.0"},
{Id: "go://rsc.io/quote:v1.5.2"},
{Id: "go://rsc.io/sampler:v1.3.0"},
{Id: "go://golang.org/x/text:v0.0.0-20170915032832-14c0d48ead0c"},
},
expectedChildren: []string{"go://rsc.io/quote:v1.5.2"},
expectedFlatCount: 3,
},
{
name: "npm - no match leaves tree unchanged",
installPkgName: "nonexistent",
tech: techutils.Npm,
tree: &xrayUtils.GraphNode{
Id: "npm://my-project:1.0.0",
Nodes: []*xrayUtils.GraphNode{
{Id: "npm://react:18.0.0"},
{Id: "npm://lodash:4.17.21"},
},
},
flatNodes: []*xrayUtils.GraphNode{
{Id: "npm://react:18.0.0"},
{Id: "npm://lodash:4.17.21"},
},
expectedChildren: []string{"npm://react:18.0.0", "npm://lodash:4.17.21"},
expectedFlatCount: 2,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
depTreeResult := &buildinfo.DependencyTreeResult{
FullDepTrees: []*xrayUtils.GraphNode{tt.tree},
FlatTree: &xrayUtils.GraphNode{Nodes: tt.flatNodes},
}

filterAuditPackageTree(depTreeResult, tt.tech, tt.installPkgName)

// Verify the filtered direct children
var childIds []string
for _, child := range depTreeResult.FullDepTrees[0].Nodes {
childIds = append(childIds, child.Id)
}
assert.Equal(t, tt.expectedChildren, childIds)

// Verify the flat tree only contains reachable nodes
assert.Equal(t, tt.expectedFlatCount, len(depTreeResult.FlatTree.Nodes))
})
}
}
Loading
Loading