Skip to content

Commit 8224664

Browse files
authored
Enable allow-partial-results for Audit SCA scan and dep tree construction (#200)
1 parent 68eb00c commit 8224664

File tree

8 files changed

+213
-11
lines changed

8 files changed

+213
-11
lines changed

cli/docs/flags.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ const (
119119
WorkingDirs = "working-dirs"
120120
OutputDir = "output-dir"
121121
SkipAutoInstall = "skip-auto-install"
122+
AllowPartialResults = "allow-partial-results"
122123

123124
// Unique curation flags
124125
CurationOutput = "curation-format"
@@ -155,7 +156,7 @@ var commandFlags = map[string][]string{
155156
url, user, password, accessToken, ServerId, InsecureTls, Project, Watches, RepoPath, Licenses, OutputFormat, ExcludeTestDeps,
156157
useWrapperAudit, DepType, RequirementsFile, Fail, ExtendedTable, WorkingDirs, ExclusionsAudit, Mvn, Gradle, Npm,
157158
Pnpm, Yarn, Go, Nuget, Pip, Pipenv, Poetry, MinSeverity, FixableOnly, ThirdPartyContextualAnalysis, Threads,
158-
Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall,
159+
Sca, Iac, Sast, Secrets, WithoutCA, ScanVuln, SecretValidation, OutputDir, SkipAutoInstall, AllowPartialResults,
159160
},
160161
CurationAudit: {
161162
CurationOutput, WorkingDirs, Threads, RequirementsFile,
@@ -230,9 +231,10 @@ var flagsMap = map[string]components.Flag{
230231
"Set to false if you wish to not use the gradle or maven wrapper.",
231232
components.WithBoolDefaultValue(true),
232233
),
233-
WorkingDirs: components.NewStringFlag(WorkingDirs, "A comma-separated list of relative working directories, to determine audit targets locations."),
234-
OutputDir: components.NewStringFlag(OutputDir, "Target directory to save partial results to.", components.SetHiddenStrFlag()),
235-
SkipAutoInstall: components.NewBoolFlag(SkipAutoInstall, "Set to true to skip auto-install of dependencies in un-built modules. Currently supported for Yarn and NPM only.", components.SetHiddenBoolFlag()),
234+
WorkingDirs: components.NewStringFlag(WorkingDirs, "A comma-separated list of relative working directories, to determine audit targets locations."),
235+
OutputDir: components.NewStringFlag(OutputDir, "Target directory to save partial results to.", components.SetHiddenStrFlag()),
236+
SkipAutoInstall: components.NewBoolFlag(SkipAutoInstall, "Set to true to skip auto-install of dependencies in un-built modules. Currently supported for Yarn and NPM only.", components.SetHiddenBoolFlag()),
237+
AllowPartialResults: components.NewBoolFlag(AllowPartialResults, "Set to true to allow partial results and continuance of the scan in case of certain errors.", components.SetHiddenBoolFlag()),
236238
ExclusionsAudit: components.NewStringFlag(
237239
Exclusions,
238240
"List of exclusions separated by semicolons, utilized to skip sub-projects from undergoing an audit. These exclusions may incorporate the * and ? wildcards.",

cli/scancommands.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ func CreateAuditCmd(c *components.Context) (*audit.AuditCommand, error) {
478478
SetFixableOnly(c.GetBoolFlagValue(flags.FixableOnly)).
479479
SetThirdPartyApplicabilityScan(c.GetBoolFlagValue(flags.ThirdPartyContextualAnalysis)).
480480
SetScansResultsOutputDir(scansOutputDir).
481-
SetSkipAutoInstall(c.GetBoolFlagValue(flags.SkipAutoInstall))
481+
SetSkipAutoInstall(c.GetBoolFlagValue(flags.SkipAutoInstall)).
482+
SetAllowPartialResults(c.GetBoolFlagValue(flags.AllowPartialResults))
482483

483484
if c.GetStringFlagValue(flags.Watches) != "" {
484485
auditCmd.SetWatches(splitByCommaAndTrim(c.GetStringFlagValue(flags.Watches)))

commands/audit/audit.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ func RunAudit(auditParams *AuditParams) (results *utils.Results, err error) {
202202
}
203203
// The sca scan doesn't require the analyzer manager, so it can run separately from the analyzer manager download routine.
204204
if scaScanErr := buildDepTreeAndRunScaScan(auditParallelRunner, auditParams, results); scaScanErr != nil {
205-
auditParallelRunner.AddErrorToChan(scaScanErr)
205+
// If error to be caught, we add it to the auditParallelRunner error queue and continue. The error need not be returned
206+
_ = createErrorIfPartialResultsDisabled(auditParams, auditParallelRunner, fmt.Sprintf("An error has occurred during SCA scan process. SCA scan is skipped for the following directories: %s.", auditParams.workingDirs), scaScanErr)
206207
}
207208
go func() {
208209
auditParallelRunner.ScaScansWg.Wait()
@@ -277,3 +278,27 @@ func downloadAnalyzerManagerAndRunScanners(auditParallelRunner *utils.SecurityPa
277278
}
278279
return
279280
}
281+
282+
// This function checks if partial results are allowed. If so we log the error and continue.
283+
// If partial results are not allowed and a SecurityParallelRunner is provided we add the error to its error queue and return without an error, since the errors will be later collected from the queue.
284+
// If partial results are not allowed and a SecurityParallelRunner is not provided we return the error.
285+
func createErrorIfPartialResultsDisabled(auditParams *AuditParams, auditParallelRunner *utils.SecurityParallelRunner, extraMassageForLog string, err error) error {
286+
if err == nil {
287+
return nil
288+
}
289+
290+
if auditParams.AllowPartialResults() {
291+
if extraMassageForLog == "" {
292+
extraMassageForLog = "An error has occurred during the audit scans"
293+
}
294+
log.Warn(fmt.Sprintf("%s\nSince partial results are allowed, the error is skipped: %s", extraMassageForLog, err.Error()))
295+
return nil
296+
}
297+
298+
// When SecurityParallelRunner is provided we add the error to the queue, otherwise we return the error
299+
if auditParallelRunner != nil {
300+
auditParallelRunner.AddErrorToChan(err)
301+
return nil
302+
}
303+
return err
304+
}

commands/audit/audit_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package audit
22

33
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
47
"path/filepath"
58
"strings"
69
"testing"
@@ -197,3 +200,144 @@ func searchForStrWithSubString(t *testing.T, filesList []string, subString strin
197200
}
198201
assert.Fail(t, "File %s not found in the list", subString)
199202
}
203+
204+
func TestAuditWithPartialResults(t *testing.T) {
205+
testcases := []struct {
206+
name string
207+
allowPartialResults bool
208+
useJas bool
209+
testDirPath string
210+
}{
211+
{
212+
name: "Failure in SCA during dependency tree construction",
213+
allowPartialResults: false,
214+
useJas: false,
215+
testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-un-installable"),
216+
},
217+
{
218+
name: "Failure in SCA during scan itself",
219+
allowPartialResults: false,
220+
useJas: false,
221+
testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-project"),
222+
},
223+
{
224+
name: "Skip failure in SCA during dependency tree construction",
225+
allowPartialResults: true,
226+
useJas: false,
227+
testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-un-installable"),
228+
},
229+
{
230+
name: "Skip failure in SCA during scan itself",
231+
allowPartialResults: true,
232+
useJas: false,
233+
testDirPath: filepath.Join("..", "..", "tests", "testdata", "projects", "package-managers", "npm", "npm-project"),
234+
},
235+
// TODO when applying allow-partial-results to JAS make sure to add a test case that checks failures in JAS scans + add some JAS api call to the mock server
236+
}
237+
238+
serverMock, serverDetails := utils.CreateXrayRestsMockServer(func(w http.ResponseWriter, r *http.Request) {
239+
if r.RequestURI == "/xray/api/v1/system/version" {
240+
_, err := w.Write([]byte(fmt.Sprintf(`{"xray_version": "%s", "xray_revision": "xxx"}`, scangraph.GraphScanMinXrayVersion)))
241+
if !assert.NoError(t, err) {
242+
return
243+
}
244+
}
245+
if strings.HasPrefix(r.RequestURI, "/xray/api/v1/scan/graph") && r.Method == http.MethodPost {
246+
// We set SCA scan graph API to fail
247+
w.WriteHeader(http.StatusBadRequest)
248+
}
249+
})
250+
defer serverMock.Close()
251+
252+
for _, testcase := range testcases {
253+
t.Run(testcase.name, func(t *testing.T) {
254+
tempDirPath, createTempDirCallback := coreTests.CreateTempDirWithCallbackAndAssert(t)
255+
defer createTempDirCallback()
256+
257+
assert.NoError(t, biutils.CopyDir(testcase.testDirPath, tempDirPath, false, nil))
258+
259+
auditBasicParams := (&utils.AuditBasicParams{}).
260+
SetServerDetails(serverDetails).
261+
SetOutputFormat(format.Table).
262+
SetUseJas(testcase.useJas).
263+
SetAllowPartialResults(testcase.allowPartialResults)
264+
265+
auditParams := NewAuditParams().
266+
SetWorkingDirs([]string{tempDirPath}).
267+
SetGraphBasicParams(auditBasicParams).
268+
SetCommonGraphScanParams(&scangraph.CommonGraphScanParams{
269+
ScanType: scanservices.Dependency,
270+
IncludeVulnerabilities: true,
271+
MultiScanId: utils.TestScaScanId,
272+
})
273+
auditParams.SetIsRecursiveScan(true)
274+
275+
scanResults, err := RunAudit(auditParams)
276+
if testcase.allowPartialResults {
277+
assert.NoError(t, scanResults.ScansErr)
278+
assert.NoError(t, err)
279+
} else {
280+
assert.Error(t, scanResults.ScansErr)
281+
assert.NoError(t, err)
282+
}
283+
})
284+
}
285+
}
286+
287+
func TestCreateErrorIfPartialResultsDisabled(t *testing.T) {
288+
testcases := []struct {
289+
name string
290+
allowPartialResults bool
291+
auditParallelRunner bool
292+
}{
293+
{
294+
name: "Allow partial results - no error expected",
295+
allowPartialResults: true,
296+
auditParallelRunner: true,
297+
},
298+
{
299+
name: "Partial results disabled with SecurityParallelRunner",
300+
allowPartialResults: false,
301+
auditParallelRunner: true,
302+
},
303+
{
304+
name: "Partial results disabled without SecurityParallelRunner",
305+
allowPartialResults: false,
306+
auditParallelRunner: false,
307+
},
308+
}
309+
310+
for _, testcase := range testcases {
311+
t.Run(testcase.name, func(t *testing.T) {
312+
auditBasicParams := (&utils.AuditBasicParams{}).SetAllowPartialResults(testcase.allowPartialResults)
313+
auditParams := NewAuditParams().SetGraphBasicParams(auditBasicParams)
314+
315+
var auditParallelRunner *utils.SecurityParallelRunner
316+
if testcase.auditParallelRunner {
317+
auditParallelRunner = utils.CreateSecurityParallelRunner(1)
318+
}
319+
320+
err := createErrorIfPartialResultsDisabled(auditParams, auditParallelRunner, "", errors.New("error"))
321+
if testcase.allowPartialResults {
322+
assert.NoError(t, err)
323+
} else {
324+
if testcase.auditParallelRunner {
325+
assert.False(t, isErrorsQueueEmpty(auditParallelRunner))
326+
} else {
327+
assert.Error(t, err)
328+
}
329+
}
330+
})
331+
}
332+
}
333+
334+
func isErrorsQueueEmpty(spr *utils.SecurityParallelRunner) bool {
335+
select {
336+
case <-spr.ErrorsQueue:
337+
// Channel is not empty
338+
return false
339+
default:
340+
// Channel is empty
341+
return true
342+
}
343+
}

commands/audit/scarunner.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,15 @@ func buildDepTreeAndRunScaScan(auditParallelRunner *utils.SecurityParallelRunner
8282
log.Warn(bdtErr.Error())
8383
continue
8484
}
85-
err = errors.Join(err, fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, bdtErr.Error()))
85+
err = errors.Join(err, createErrorIfPartialResultsDisabled(auditParams, nil, fmt.Sprintf("Dependencies tree construction ha failed for the following target: %s", scan.Target), fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, bdtErr.Error())))
8686
continue
8787
}
8888
// Create sca scan task
8989
auditParallelRunner.ScaScansWg.Add(1)
9090
_, taskErr := auditParallelRunner.Runner.AddTaskWithError(executeScaScanTask(auditParallelRunner, serverDetails, auditParams, scan, treeResult), func(err error) {
91-
auditParallelRunner.AddErrorToChan(fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, err.Error()))
91+
// If error to be caught, we add it to the auditParallelRunner error queue and continue. The error need not be returned
92+
_ = createErrorIfPartialResultsDisabled(auditParams, auditParallelRunner, fmt.Sprintf("Failed to execute SCA scan for the following target: %s", scan.Target), fmt.Errorf("audit command in '%s' failed:\n%s", scan.Target, err.Error()))
93+
auditParallelRunner.ScaScansWg.Done()
9294
})
9395
if taskErr != nil {
9496
return fmt.Errorf("failed to create sca scan task for '%s': %s", scan.Target, taskErr.Error())
@@ -147,8 +149,12 @@ func executeScaScanTask(auditParallelRunner *utils.SecurityParallelRunner, serve
147149
scan *xrayutils.ScaScanResult, treeResult *DependencyTreeResult) parallel.TaskFunc {
148150
return func(threadId int) (err error) {
149151
log.Info(clientutils.GetLogMsgPrefix(threadId, false)+"Running SCA scan for", scan.Target, "vulnerable dependencies in", scan.Target, "directory...")
152+
var xrayErr error
150153
defer func() {
151-
auditParallelRunner.ScaScansWg.Done()
154+
if xrayErr == nil {
155+
// We Sca waitGroup as done only when we have no errors. If we have errors we mark it done in the error's handler function
156+
auditParallelRunner.ScaScansWg.Done()
157+
}
152158
}()
153159
// Scan the dependency tree.
154160
scanResults, xrayErr := runScaWithTech(scan.Technology, auditParams, serverDetails, *treeResult.FlatTree, treeResult.FullDepTrees)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "jfrog-cli-tests",
3+
"version": "v1.0.0",
4+
"description": "test package",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1"
8+
},
9+
"author": "",
10+
"license": "ISC",
11+
"dependencies": {
12+
"non-existing-dependency": "1.0.1"
13+
}
14+
}

utils/auditbasicparams.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type AuditBasicParams struct {
6565
exclusions []string
6666
isRecursiveScan bool
6767
skipAutoInstall bool
68+
allowPartialResults bool
6869
}
6970

7071
func (abp *AuditBasicParams) DirectDependencies() *[]string {
@@ -105,6 +106,11 @@ func (abp *AuditBasicParams) SetSkipAutoInstall(skipAutoInstall bool) *AuditBasi
105106
return abp
106107
}
107108

109+
func (abp *AuditBasicParams) SetAllowPartialResults(allowPartialResults bool) *AuditBasicParams {
110+
abp.allowPartialResults = allowPartialResults
111+
return abp
112+
}
113+
108114
func (abp *AuditBasicParams) UseJas() bool {
109115
return abp.useJas
110116
}
@@ -264,3 +270,7 @@ func (abp *AuditBasicParams) IsRecursiveScan() bool {
264270
func (abp *AuditBasicParams) SkipAutoInstall() bool {
265271
return abp.skipAutoInstall
266272
}
273+
274+
func (abp *AuditBasicParams) AllowPartialResults() bool {
275+
return abp.allowPartialResults
276+
}

utils/results.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ func (r *Results) GetScaScansXrayResults() (results []services.ScanResponse) {
3131
return
3232
}
3333

34-
func (r *Results) GetScaScannedTechnologies() []techutils.Technology {
35-
technologies := datastructures.MakeSet[techutils.Technology]()
34+
func (r *Results) GetScaScannedTechnologies(otherTech ...techutils.Technology) []techutils.Technology {
35+
technologies := datastructures.MakeSetFromElements(otherTech...)
3636
for _, scaResult := range r.ScaResults {
3737
technologies.Add(scaResult.Technology)
3838
}

0 commit comments

Comments
 (0)