Skip to content

Commit aaab4d3

Browse files
authored
Diff scan (jfrog#389)
1 parent c61615f commit aaab4d3

34 files changed

+1168
-375
lines changed

commands/audit/audit.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -296,19 +296,23 @@ func RunJasScans(auditParallelRunner *utils.SecurityParallelRunner, auditParams
296296
return
297297
}
298298
auditParallelRunner.ResultsMu.Lock()
299-
jasScanner, err = jas.CreateJasScanner(
300-
serverDetails,
301-
scanResults.SecretValidation,
302-
auditParams.minSeverityFilter,
303-
jas.GetAnalyzerManagerXscEnvVars(
304-
auditParams.GetMultiScanId(),
305-
utils.GetGitRepoUrlKey(auditParams.resultsContext.GitRepoHttpsCloneUrl),
306-
auditParams.resultsContext.ProjectKey,
307-
auditParams.resultsContext.Watches,
308-
scanResults.GetTechnologies()...,
299+
scannerOptions := []jas.JasScannerOption{
300+
jas.WithEnvVars(
301+
scanResults.SecretValidation,
302+
jas.GetDiffScanTypeValue(auditParams.diffMode, auditParams.resultsToCompare),
303+
jas.GetAnalyzerManagerXscEnvVars(
304+
auditParams.GetMultiScanId(),
305+
utils.GetGitRepoUrlKey(auditParams.resultsContext.GitRepoHttpsCloneUrl),
306+
auditParams.resultsContext.ProjectKey,
307+
auditParams.resultsContext.Watches,
308+
scanResults.GetTechnologies()...,
309+
),
309310
),
310-
auditParams.Exclusions()...,
311-
)
311+
jas.WithMinSeverity(auditParams.minSeverityFilter),
312+
jas.WithExclusions(auditParams.Exclusions()...),
313+
jas.WithResultsToCompare(auditParams.resultsToCompare),
314+
}
315+
jasScanner, err = jas.NewJasScanner(serverDetails, scannerOptions...)
312316
jas.UpdateJasScannerWithExcludePatternsFromProfile(jasScanner, auditParams.AuditBasicParams.GetConfigProfile())
313317

314318
auditParallelRunner.ResultsMu.Unlock()
@@ -353,6 +357,7 @@ func createJasScansTasks(auditParallelRunner *utils.SecurityParallelRunner, scan
353357
Module: *module,
354358
ConfigProfile: auditParams.AuditBasicParams.GetConfigProfile(),
355359
ScansToPerform: auditParams.ScansToPerform(),
360+
SourceResultsToCompare: scanner.GetResultsToCompare(utils.GetRelativePath(targetResult.Target, scanResults.GetCommonParentPath())),
356361
SecretsScanType: secrets.SecretsScannerType,
357362
DirectDependencies: auditParams.DirectDependencies(),
358363
ThirdPartyApplicabilityScan: auditParams.thirdPartyApplicabilityScan,

commands/audit/audit_test.go

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ package audit
22

33
import (
44
"fmt"
5-
"net/http"
6-
"path/filepath"
7-
"sort"
8-
"strings"
9-
"testing"
10-
115
commonCommands "github.com/jfrog/jfrog-cli-core/v2/common/commands"
126
"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
137
configTests "github.com/jfrog/jfrog-cli-security/tests"
148
securityTestUtils "github.com/jfrog/jfrog-cli-security/tests/utils"
159
clientTests "github.com/jfrog/jfrog-client-go/utils/tests"
10+
"net/http"
11+
"path/filepath"
12+
"sort"
13+
"strings"
14+
"testing"
1615

1716
"github.com/stretchr/testify/assert"
1817

@@ -598,7 +597,7 @@ func TestAuditWithConfigProfile(t *testing.T) {
598597

599598
for _, testcase := range testcases {
600599
t.Run(testcase.name, func(t *testing.T) {
601-
mockServer, serverDetails := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion, XscVersion: services.ConfigProfileMinXscVersion})
600+
mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion, XscVersion: services.ConfigProfileMinXscVersion})
602601
defer mockServer.Close()
603602

604603
tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t)
@@ -649,7 +648,7 @@ func TestAuditWithConfigProfile(t *testing.T) {
649648

650649
// This test tests audit flow when providing --output-dir flag
651650
func TestAuditWithScansOutputDir(t *testing.T) {
652-
mockServer, serverDetails := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion})
651+
mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: utils.EntitlementsMinVersion})
653652
defer mockServer.Close()
654653

655654
outputDirPath, removeOutputDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t)
@@ -919,7 +918,7 @@ func TestCreateResultsContext(t *testing.T) {
919918
}
920919
for _, testCase := range testCases {
921920
t.Run(fmt.Sprintf("%s - %s", test.name, testCase.name), func(t *testing.T) {
922-
mockServer, serverDetails := validations.XrayServer(t, validations.MockServerParams{XrayVersion: test.xrayVersion, ReturnMockPlatformWatches: test.expectedPlatformWatches})
921+
mockServer, serverDetails, _ := validations.XrayServer(t, validations.MockServerParams{XrayVersion: test.xrayVersion, ReturnMockPlatformWatches: test.expectedPlatformWatches})
923922
defer mockServer.Close()
924923
context := CreateAuditResultsContext(serverDetails, test.xrayVersion, testCase.watches, testCase.artifactoryRepoPath, testCase.jfrogProjectKey, testCase.httpCloneUrl, testCase.includeVulnerabilities, testCase.includeLicenses, testCase.includeSbom)
925924
assert.Equal(t, testCase.expectedArtifactoryRepoPath, context.RepoPath)
@@ -933,3 +932,89 @@ func TestCreateResultsContext(t *testing.T) {
933932
}
934933
}
935934
}
935+
936+
func TestAudit_DiffScanFlow(t *testing.T) {
937+
testDirPath := filepath.Join("..", "..", "tests", "testdata", "projects", "jas", "jas")
938+
tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t)
939+
defer createTempDirCallback()
940+
assert.NoError(t, biutils.CopyDir(testDirPath, tempDirPath, true, nil))
941+
942+
testCases := []struct {
943+
name string
944+
resultsToCompare *results.SecurityCommandResults
945+
expectedApiCallsCount map[string]int
946+
}{
947+
{
948+
name: "No results to compare, no api scan calls",
949+
expectedApiCallsCount: map[string]int{},
950+
},
951+
{
952+
name: "with results to compare, api scan calls",
953+
resultsToCompare: &results.SecurityCommandResults{
954+
Targets: []*results.TargetResults{
955+
{
956+
ScanTarget: results.ScanTarget{
957+
Target: tempDirPath,
958+
Technology: techutils.Pip,
959+
},
960+
Sbom: results.Sbom{
961+
Components: []results.SbomEntry{
962+
{
963+
Component: "werkzeug",
964+
Version: "1.0.2",
965+
Type: "Python",
966+
XrayType: "pypi",
967+
},
968+
{
969+
Component: "pyyaml",
970+
Version: "5.2",
971+
Type: "Python",
972+
XrayType: "pypi",
973+
},
974+
{
975+
Component: "wasabi",
976+
Version: "1.1.3",
977+
Type: "Python",
978+
XrayType: "pypi",
979+
},
980+
},
981+
},
982+
},
983+
},
984+
},
985+
expectedApiCallsCount: map[string]int{
986+
validations.GraphScanPostAPI: 1,
987+
validations.GraphScanGetAPI: 1,
988+
},
989+
},
990+
}
991+
for _, tc := range testCases {
992+
t.Run(tc.name, func(t *testing.T) {
993+
testParams := validations.MockServerParams{
994+
XrayVersion: utils.EntitlementsMinVersion,
995+
XscVersion: services.ConfigProfileMinXscVersion,
996+
}
997+
mockServer, serverDetails, apiCallsCount := validations.XrayServer(t, testParams)
998+
defer mockServer.Close()
999+
1000+
auditBasicParams := (&utils.AuditBasicParams{}).
1001+
SetServerDetails(serverDetails).
1002+
SetXrayVersion(utils.EntitlementsMinVersion).
1003+
SetXscVersion(services.ConfigProfileMinXscVersion).
1004+
SetOutputFormat(format.SimpleJson).
1005+
SetUseJas(false)
1006+
1007+
auditParams := NewAuditParams().
1008+
SetWorkingDirs([]string{tempDirPath}).
1009+
SetMultiScanId(validations.TestMsi).
1010+
SetGraphBasicParams(auditBasicParams).
1011+
SetResultsContext(results.ResultContext{IncludeVulnerabilities: true}).
1012+
SetDiffMode(true).
1013+
SetResultsToCompare(tc.resultsToCompare)
1014+
1015+
auditResults := RunAudit(auditParams)
1016+
assert.NoError(t, auditResults.GetErrors())
1017+
assert.Equal(t, tc.expectedApiCallsCount, *apiCallsCount)
1018+
})
1019+
}
1020+
}

commands/audit/auditparams.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ type AuditParams struct {
2323
threads int
2424
scanResultsOutputDir string
2525
startTime time.Time
26+
// Diff mode, scan only the files affected by the diff.
27+
diffMode bool
28+
filesToScan []string
29+
resultsToCompare *results.SecurityCommandResults
2630
}
2731

2832
func NewAuditParams() *AuditParams {
@@ -126,3 +130,30 @@ func (params *AuditParams) createXrayGraphScanParams() *services.XrayGraphScanPa
126130
ScanType: services.Dependency,
127131
}
128132
}
133+
134+
func (params *AuditParams) SetFilesToScan(filesToScan []string) *AuditParams {
135+
params.filesToScan = filesToScan
136+
return params
137+
}
138+
139+
func (params *AuditParams) FilesToScan() []string {
140+
return params.filesToScan
141+
}
142+
143+
func (params *AuditParams) SetResultsToCompare(resultsToCompare *results.SecurityCommandResults) *AuditParams {
144+
params.resultsToCompare = resultsToCompare
145+
return params
146+
}
147+
148+
func (params *AuditParams) ResultsToCompare() *results.SecurityCommandResults {
149+
return params.resultsToCompare
150+
}
151+
152+
func (params *AuditParams) SetDiffMode(diffMode bool) *AuditParams {
153+
params.diffMode = diffMode
154+
return params
155+
}
156+
157+
func (params *AuditParams) DiffMode() bool {
158+
return params.diffMode
159+
}

commands/audit/scarunner.go

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7-
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/swift"
8-
97
biutils "github.com/jfrog/build-info-go/utils"
108
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/conan"
9+
"github.com/jfrog/jfrog-cli-security/commands/audit/sca/swift"
1110
"github.com/jfrog/jfrog-client-go/utils/io/fileutils"
1211
"golang.org/x/exp/slices"
1312

@@ -99,9 +98,26 @@ func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner
9998
_ = targetResult.AddTargetError(fmt.Errorf("failed to build dependency tree: %s", bdtErr.Error()), auditParams.AllowPartialResults())
10099
continue
101100
}
101+
if auditParams.diffMode {
102+
if auditParams.resultsToCompare == nil {
103+
// First scan, no diff to compare
104+
log.Debug(fmt.Sprintf("Diff scan - calculated dependencies tree for target %s, skipping scan part", targetResult.Target))
105+
continue
106+
} else if treeResult, bdtErr = getDiffDependencyTree(targetResult, results.SearchTargetResultsByPath(utils.GetRelativePath(targetResult.Target, cmdResults.GetCommonParentPath()), auditParams.resultsToCompare), treeResult.FullDepTrees...); bdtErr != nil {
107+
_ = targetResult.AddTargetError(fmt.Errorf("failed to build diff dependency tree in source branch: %s", bdtErr.Error()), auditParams.AllowPartialResults())
108+
continue
109+
}
110+
}
111+
if treeResult.FlatTree == nil || len(treeResult.FlatTree.Nodes) == 0 {
112+
// No dependencies were found. We don't want to run the scan in this case.
113+
log.Debug(fmt.Sprintf("No dependencies were found in target %s. Skipping SCA", targetResult.Target))
114+
continue
115+
}
116+
if err := logDeps(treeResult.FlatTree); err != nil {
117+
log.Warn("Failed to log dependencies tree: " + err.Error())
118+
}
102119
// Create sca scan task
103120
auditParallelRunner.ScaScansWg.Add(1)
104-
// defer auditParallelRunner.ScaScansWg.Done()
105121
_, taskErr := auditParallelRunner.Runner.AddTaskWithError(executeScaScanTask(auditParallelRunner, serverDetails, auditParams, targetResult, treeResult), func(err error) {
106122
_ = targetResult.AddTargetError(fmt.Errorf("failed to execute SCA scan: %s", err.Error()), auditParams.AllowPartialResults())
107123
})
@@ -133,7 +149,7 @@ func executeScaScanTask(auditParallelRunner *utils.SecurityParallelRunner, serve
133149
auditParallelRunner.ResultsMu.Lock()
134150
defer auditParallelRunner.ResultsMu.Unlock()
135151
// We add the results before checking for errors, so we can display the results even if an error occurred.
136-
scan.NewScaScanResults(sca.GetScaScansStatusCode(xrayErr, scanResults...), results.DepTreeToSbom(treeResult.FullDepTrees), scanResults...).IsMultipleRootProject = clientutils.Pointer(len(treeResult.FullDepTrees) > 1)
152+
scan.NewScaScanResults(sca.GetScaScansStatusCode(xrayErr, scanResults...), scanResults...).IsMultipleRootProject = clientutils.Pointer(len(treeResult.FullDepTrees) > 1)
137153
addThirdPartyDependenciesToParams(auditParams, scan.Technology, treeResult.FlatTree, treeResult.FullDepTrees)
138154

139155
if xrayErr != nil {
@@ -277,10 +293,10 @@ func GetTechDependencyTree(params xrayutils.AuditParams, artifactoryServerDetail
277293
}
278294
log.Debug(fmt.Sprintf("Created '%s' dependency tree with %d nodes. Elapsed time: %.1f seconds.", tech.ToFormal(), len(uniqueDeps), time.Since(startTime).Seconds()))
279295
if len(uniqDepsWithTypes) > 0 {
280-
depTreeResult.FlatTree, err = createFlatTreeWithTypes(uniqDepsWithTypes)
296+
depTreeResult.FlatTree = createFlatTreeWithTypes(uniqDepsWithTypes)
281297
return
282298
}
283-
depTreeResult.FlatTree, err = createFlatTree(uniqueDeps)
299+
depTreeResult.FlatTree = createFlatTree(uniqueDeps)
284300
return
285301
}
286302

@@ -331,10 +347,7 @@ func SetResolutionRepoInAuditParamsIfExists(params utils.AuditParams, tech techu
331347
return
332348
}
333349

334-
func createFlatTreeWithTypes(uniqueDeps map[string]*xray.DepTreeNode) (*xrayCmdUtils.GraphNode, error) {
335-
if err := logDeps(uniqueDeps); err != nil {
336-
return nil, err
337-
}
350+
func createFlatTreeWithTypes(uniqueDeps map[string]*xray.DepTreeNode) *xrayCmdUtils.GraphNode {
338351
var uniqueNodes []*xrayCmdUtils.GraphNode
339352
for uniqueDep, nodeAttr := range uniqueDeps {
340353
node := &xrayCmdUtils.GraphNode{Id: uniqueDep}
@@ -344,26 +357,31 @@ func createFlatTreeWithTypes(uniqueDeps map[string]*xray.DepTreeNode) (*xrayCmdU
344357
}
345358
uniqueNodes = append(uniqueNodes, node)
346359
}
347-
return &xrayCmdUtils.GraphNode{Id: "root", Nodes: uniqueNodes}, nil
360+
return &xrayCmdUtils.GraphNode{Id: "root", Nodes: uniqueNodes}
348361
}
349362

350-
func createFlatTree(uniqueDeps []string) (*xrayCmdUtils.GraphNode, error) {
351-
if err := logDeps(uniqueDeps); err != nil {
352-
return nil, err
353-
}
363+
func createFlatTree(uniqueDeps []string) *xrayCmdUtils.GraphNode {
354364
uniqueNodes := []*xrayCmdUtils.GraphNode{}
355365
for _, uniqueDep := range uniqueDeps {
356366
uniqueNodes = append(uniqueNodes, &xrayCmdUtils.GraphNode{Id: uniqueDep})
357367
}
358-
return &xrayCmdUtils.GraphNode{Id: "root", Nodes: uniqueNodes}, nil
368+
return &xrayCmdUtils.GraphNode{Id: "root", Nodes: uniqueNodes}
369+
}
370+
371+
func flatTreeToStringList(flatTree *xrayCmdUtils.GraphNode) []string {
372+
var uniqueNodes []string
373+
for _, node := range flatTree.Nodes {
374+
uniqueNodes = append(uniqueNodes, node.Id)
375+
}
376+
return uniqueNodes
359377
}
360378

361-
func logDeps(uniqueDeps any) (err error) {
379+
func logDeps(flatTree *xrayCmdUtils.GraphNode) (err error) {
362380
if log.GetLogger().GetLogLevel() != log.DEBUG {
363381
// Avoid printing and marshaling if not on DEBUG mode.
364382
return
365383
}
366-
jsonList, err := json.Marshal(uniqueDeps)
384+
jsonList, err := json.Marshal(flatTreeToStringList(flatTree))
367385
if errorutils.CheckError(err) != nil {
368386
return err
369387
}
@@ -388,6 +406,7 @@ func buildDependencyTree(scan *results.TargetResults, params *AuditParams) (*Dep
388406
if treeResult.FlatTree == nil || len(treeResult.FlatTree.Nodes) == 0 {
389407
return nil, errorutils.CheckErrorf("no dependencies were found. Please try to build your project and re-run the audit command")
390408
}
409+
scan.SetSbom(results.DepTreeToSbom(treeResult.FullDepTrees))
391410
return &treeResult, nil
392411
}
393412

@@ -402,3 +421,29 @@ func dumpScanResponseToFileIfNeeded(results []services.ScanResponse, scanResults
402421
}
403422
return utils.DumpContentToFile(fileContent, scanResultsOutputDir, scanType.String())
404423
}
424+
425+
// Collect dependencies exists in target and not in resultsToCompare
426+
func getDiffDependencyTree(scanResults *results.TargetResults, resultsToCompare *results.TargetResults, fullDepTrees ...*xrayCmdUtils.GraphNode) (*DependencyTreeResult, error) {
427+
if resultsToCompare == nil {
428+
return nil, fmt.Errorf("failed to get diff dependency tree: no results to compare")
429+
}
430+
log.Debug(fmt.Sprintf("Comparing %s SBOM with %s to get diff", scanResults.Target, resultsToCompare.Target))
431+
// Compare the dependency trees
432+
filterDepsMap := datastructures.MakeSet[string]()
433+
for _, component := range resultsToCompare.Sbom.Components {
434+
filterDepsMap.Add(techutils.ToXrayComponentId(component.XrayType, component.Component, component.Version))
435+
}
436+
addedDepsMap := datastructures.MakeSet[string]()
437+
for _, component := range scanResults.Sbom.Components {
438+
componentId := techutils.ToXrayComponentId(component.XrayType, component.Component, component.Version)
439+
if exists := filterDepsMap.Exists(componentId); !exists {
440+
// Dependency in scan results but not in results to compare
441+
addedDepsMap.Add(componentId)
442+
}
443+
}
444+
diffDepTree := DependencyTreeResult{
445+
FlatTree: createFlatTree(addedDepsMap.ToSlice()),
446+
FullDepTrees: fullDepTrees,
447+
}
448+
return &diffDepTree, nil
449+
}

0 commit comments

Comments
 (0)