diff --git a/README.md b/README.md index 4db87e00..a7eebd6d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,29 @@ Once you've installed the CLI, you're ready to scan your project. You can scan a When the scan is complete, you will see the total number of vulnerabilities found and a list of automation rules that have been evaluated. Read more about automations [here](https://debricked.com/docs/automation/automation-overview.html#automation-overview). +### Exit codes +| Code | Meaning | +| ---- | ------- | +| 0 | The scan completed and no triggered automation rule failed the pipeline | +| 1 | The scan failed. This covers triggered automation rules configured to fail the pipeline, resolution failures (see below), and errors such as a bad access token or an unreachable service | +| 3 | The scan completed, but some (not all) dependency files failed to resolve. Only produced by `--resolution-strictness=3` | + +Failed resolution of dependency files affects the scan and its exit code according to +`--resolution-strictness` (default `1`): + +| Level | Meaning | +| ----- | ------- | +| 0 | Always continue the scan, even if any or all files failed to resolve | +| 1 | Exit with code 1 if all files failed to resolve, otherwise continue the scan | +| 2 | Exit with code 1 if any file failed to resolve, otherwise continue the scan | +| 3 | Exit with code 1 if all files failed to resolve. If some but not all files failed to resolve, complete the scan and then exit with code 3 | + +A resolution failure typically means the relevant package manager is not installed or not on the +`PATH` (for example `mvn` or `composer`). + +If Debricked's scan queue is long, the CLI stops polling for progress and exits with code 1, having +printed a link to the results. Pass `--pass-on-timeout` to exit 0 in that case instead. + ### Docker To make a scan directly through Docker based on your current working directory, you can use the following command: ```sh diff --git a/internal/cmd/scan/scan.go b/internal/cmd/scan/scan.go index a246d068..689b5b97 100644 --- a/internal/cmd/scan/scan.go +++ b/internal/cmd/scan/scan.go @@ -8,7 +8,9 @@ import ( "strconv" "strings" + "github.com/debricked/cli/internal/cmd/cmderror" "github.com/debricked/cli/internal/file" + "github.com/debricked/cli/internal/resolution" "github.com/debricked/cli/internal/scan" "github.com/fatih/color" "github.com/spf13/cobra" @@ -36,6 +38,7 @@ var passOnDowntime bool var regenerate int var repositoryName string var repositoryUrl string +var resolutionStrictness int var verbose bool var versionHint bool var sbom string @@ -72,6 +75,7 @@ const ( TagCommitAsReleaseEnv = "TAG_COMMIT_AS_RELEASE" ExperimentalFlag = "experimental" GenerateCommitNameFlag = "generate-commit-name" + ResolutionStrictnessFlag = "resolution-strictness" ) var scanCmdError error @@ -162,9 +166,21 @@ $ debricked scan . --inclusion '**/node_modules/**'`) }, "\n") cmd.Flags().BoolVar(&verbose, VerboseFlag, true, verboseDoc) cmd.Flags().BoolVar(&debug, DebugFlag, false, "write all debug output to stderr") - cmd.Flags().BoolVarP(&passOnDowntime, PassOnTimeOut, "p", false, "pass scan if there is a service access timeout") + cmd.Flags().BoolVarP(&passOnDowntime, PassOnTimeOut, "p", false, "pass scan if there is a service access timeout, or if the scan is still queued once progress polling gives up") cmd.Flags().BoolVar(&noResolve, NoResolveFlag, false, `disables resolution of manifest files that lack lock files. Resolving manifest files enables more accurate dependency scanning since the whole dependency tree will be analysed. For example, if there is a "go.mod" in the target path, its dependencies are going to get resolved onto a lock file, and latter scanned.`) + resolutionStrictnessDoc := strings.Join( + []string{ + "Allows you to configure how failed resolution of manifest files affects the scan and its exit code.\n", + "Strictness Level | Meaning", + "---------------- | -------", + "0 | Always continue the scan, even if any or all files failed to resolve", + "1 (default) | Exit with code 1 if all files failed to resolve, otherwise continue the scan", + "2 | Exit with code 1 if any file failed to resolve, otherwise continue the scan", + "3 | Exit with code 1 if all files failed to resolve. If some but not all files failed to resolve, complete the scan and then exit with code 3", + "\nExample:\n$ debricked scan . --resolution-strictness=3", + }, "\n") + cmd.Flags().IntVar(&resolutionStrictness, ResolutionStrictnessFlag, int(resolution.FailIfAllFail), resolutionStrictnessDoc) cmd.Flags().BoolVar(&noFingerprint, NoFingerprintFlag, false, "Toggle fingerprinting for undeclared component identification. Can be run as a standalone command [fingerprint] with more granular options.") cmd.Flags().BoolVar(&callgraph, CallGraphFlag, false, `Enables call graph generation during scan.`) cmd.Flags().StringVar(&javaCallgraphEngine, JavaCallgraphEngineFlag, "soot", "Java call graph engine to use during scan callgraph generation: soot or sootup.") @@ -232,6 +248,11 @@ func RunE(s *scan.IScanner) func(_ *cobra.Command, args []string) error { tagCommitAsRelease = viper.GetBool(TagCommitAsReleaseFlag) } + strictness, err := resolution.GetStrictnessLevel(viper.GetInt(ResolutionStrictnessFlag)) + if err != nil { + return err + } + options := scan.DebrickedOptions{ Path: path, Resolve: !viper.GetBool(NoResolveFlag), @@ -261,6 +282,7 @@ func RunE(s *scan.IScanner) func(_ *cobra.Command, args []string) error { MinFingerprintContentLength: viper.GetInt(MinFingerprintContentLengthFlag), TagCommitAsRelease: tagCommitAsRelease, Experimental: viper.GetBool(ExperimentalFlag), + ResolutionStrictness: strictness, } if s != nil { scanCmdError = (*s).Scan(options) @@ -268,7 +290,8 @@ func RunE(s *scan.IScanner) func(_ *cobra.Command, args []string) error { scanCmdError = errors.New("scanner was nil") } - if scanCmdError == scan.FailPipelineErr { + var cmdErr cmderror.CommandError + if scanCmdError == scan.FailPipelineErr || scanCmdError == scan.LongQueueErr || errors.As(scanCmdError, &cmdErr) { cmd.SilenceUsage = true cmd.SilenceErrors = true diff --git a/internal/cmd/scan/scan_test.go b/internal/cmd/scan/scan_test.go index f11f50c0..7f6a9997 100644 --- a/internal/cmd/scan/scan_test.go +++ b/internal/cmd/scan/scan_test.go @@ -1,8 +1,11 @@ package scan import ( + "errors" "testing" + "github.com/debricked/cli/internal/cmd/cmderror" + "github.com/debricked/cli/internal/resolution" "github.com/debricked/cli/internal/scan" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -26,6 +29,7 @@ func TestNewScanCmd(t *testing.T) { JavaCallgraphEngineFlag: "", CallGraphUploadTimeoutFlag: "", CallGraphGenerateTimeoutFlag: "", + ResolutionStrictnessFlag: "", } flags := cmd.Flags() for name, shorthand := range flagAssertions { @@ -83,11 +87,124 @@ func TestRunEFailPipelineErr(t *testing.T) { err := runE(cmd, nil) - assert.Error(t, err, scan.FailPipelineErr) + assert.ErrorIs(t, err, scan.FailPipelineErr) assert.True(t, cmd.SilenceUsage, "failed to assert that usage was silenced") assert.True(t, cmd.SilenceErrors, "failed to assert that errors were silenced") } +func TestRunELongQueueErr(t *testing.T) { + var s scan.IScanner + mock := &scannerMock{} + mock.setErr(scan.LongQueueErr) + s = mock + runE := RunE(&s) + cmd := &cobra.Command{} + + err := runE(cmd, nil) + + assert.ErrorIs(t, err, scan.LongQueueErr) + assert.True(t, cmd.SilenceUsage, "failed to assert that usage was silenced") + assert.True(t, cmd.SilenceErrors, "failed to assert that errors were silenced") +} + +func TestRunECommandError(t *testing.T) { + var s scan.IScanner + mock := &scannerMock{} + cmdErr := cmderror.CommandError{Code: 3, Err: errors.New("partial resolution failure")} + mock.setErr(cmdErr) + s = mock + runE := RunE(&s) + cmd := &cobra.Command{} + + err := runE(cmd, nil) + + var gotCmdErr cmderror.CommandError + assert.True(t, errors.As(err, &gotCmdErr), "expected CommandError to be preserved") + assert.Equal(t, 3, gotCmdErr.Code, "expected exit code 3 to be preserved") + assert.True(t, cmd.SilenceUsage, "failed to assert that usage was silenced") + assert.True(t, cmd.SilenceErrors, "failed to assert that errors were silenced") +} + +func TestRunEResolutionStrictness(t *testing.T) { + cases := []struct { + name string + flag interface{} + expected resolution.StrictnessLevel + }{ + {name: "default", flag: nil, expected: resolution.FailIfAllFail}, + {name: "no fail", flag: 0, expected: resolution.NoFail}, + {name: "fail if all fail", flag: 1, expected: resolution.FailIfAllFail}, + {name: "fail if any fail", flag: 2, expected: resolution.FailIfAnyFail}, + {name: "fail or warn", flag: 3, expected: resolution.FailOrWarn}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + viper.Reset() + if c.flag != nil { + viper.Set(ResolutionStrictnessFlag, c.flag) + } else { + // Mirror the flag default that PreRun would have bound. + viper.SetDefault(ResolutionStrictnessFlag, int(resolution.FailIfAllFail)) + } + defer viper.Reset() + + var s scan.IScanner + mock := &scannerMock{} + s = mock + runE := RunE(&s) + + err := runE(&cobra.Command{}, nil) + + assert.NoError(t, err) + options, ok := mock.options.(scan.DebrickedOptions) + assert.True(t, ok, "failed to assert that scan options were passed") + assert.Equal(t, c.expected, options.ResolutionStrictness) + }) + } +} + +// "debricked files find" binds DEBRICKED_STRICT to the global viper key +// "strict". The scan flag must not share that key, or setting the env var for +// one command would silently change resolution behaviour in the other. +func TestRunEStrictEnvDoesNotAffectResolutionStrictness(t *testing.T) { + viper.Reset() + viper.SetEnvPrefix("DEBRICKED") + viper.AutomaticEnv() + viper.MustBindEnv("strict") + viper.SetDefault(ResolutionStrictnessFlag, int(resolution.FailIfAllFail)) + t.Setenv("DEBRICKED_STRICT", "3") + defer viper.Reset() + + var s scan.IScanner + mock := &scannerMock{} + s = mock + runE := RunE(&s) + + err := runE(&cobra.Command{}, nil) + + assert.NoError(t, err) + options, ok := mock.options.(scan.DebrickedOptions) + assert.True(t, ok, "failed to assert that scan options were passed") + assert.Equal(t, resolution.FailIfAllFail, options.ResolutionStrictness) +} + +func TestRunEInvalidResolutionStrictness(t *testing.T) { + viper.Reset() + viper.Set(ResolutionStrictnessFlag, 4) + defer viper.Reset() + + var s scan.IScanner + mock := &scannerMock{} + s = mock + runE := RunE(&s) + + err := runE(&cobra.Command{}, nil) + + assert.ErrorContains(t, err, "invalid strictness level: 4") + assert.Nil(t, mock.options, "failed to assert that the scan was not started") +} + func TestRunEError(t *testing.T) { runE := RunE(nil) err := runE(nil, []string{"."}) @@ -102,9 +219,13 @@ func TestPreRun(t *testing.T) { type scannerMock struct { err error + // options records the options of the most recent Scan call. + options scan.IOptions } -func (s *scannerMock) Scan(_ scan.IOptions) error { +func (s *scannerMock) Scan(o scan.IOptions) error { + s.options = o + return s.err } diff --git a/internal/resolution/testdata/resolver_mock.go b/internal/resolution/testdata/resolver_mock.go index 0fda1eb7..c0cf354f 100644 --- a/internal/resolution/testdata/resolver_mock.go +++ b/internal/resolution/testdata/resolver_mock.go @@ -9,14 +9,17 @@ import ( ) type ResolverMock struct { - Err error - files []string + Err error + // Options records the options of the most recent Resolve call. + Options resolution.IOptions + files []string } func (r *ResolverMock) SetNpmPreferred(_ bool) { } -func (r *ResolverMock) Resolve(_ []string, _ resolution.IOptions) (resolution.IResolution, error) { +func (r *ResolverMock) Resolve(_ []string, options resolution.IOptions) (resolution.IResolution, error) { + r.Options = options for _, f := range r.files { createdFile, err := os.Create(f) if err != nil { diff --git a/internal/scan/scanner.go b/internal/scan/scanner.go index b586ac21..4abf9cbe 100644 --- a/internal/scan/scanner.go +++ b/internal/scan/scanner.go @@ -14,6 +14,7 @@ import ( "github.com/debricked/cli/internal/ci" "github.com/debricked/cli/internal/ci/env" "github.com/debricked/cli/internal/client" + "github.com/debricked/cli/internal/cmd/cmderror" "github.com/debricked/cli/internal/debug" "github.com/debricked/cli/internal/file" "github.com/debricked/cli/internal/fingerprint" @@ -29,6 +30,7 @@ import ( var ( BadOptsErr = errors.New("failed to type case IOptions") FailPipelineErr = errors.New("") + LongQueueErr = errors.New("progress polling terminated due to long scan times") ) type IScanner interface { @@ -77,6 +79,7 @@ type DebrickedOptions struct { TagCommitAsRelease bool Experimental bool Version string + ResolutionStrictness resolution.StrictnessLevel } func NewDebrickedScanner( @@ -129,6 +132,12 @@ func (dScanner *DebrickedScanner) Scan(o IOptions) error { return err } + debug.Log("Running scanResolve...", dOptions.Debug) + resolutionErr := dScanner.scanResolve(dOptions) + if isFatalResolutionErr(resolutionErr) { + return resolutionErr + } + debug.Log("Running scan with initialized scanner...", dOptions.Debug) result, err := dScanner.scan(dOptions, *gitMetaObject) if err != nil { @@ -136,13 +145,40 @@ func (dScanner *DebrickedScanner) Scan(o IOptions) error { } if result.LongQueue { - fmt.Println("Progress polling terminated due to long scan times. Please try again later") - fmt.Printf("For full details, visit: %s\n\n", color.BlueString(result.DetailsUrl)) + return dScanner.handleLongQueue(dOptions, result, resolutionErr) + } - return nil + if dScanner.reportResult(dOptions, result) { + return FailPipelineErr + } + + // A non-fatal resolution failure is deliberately surfaced only here, so that + // its exit code never costs the user the scan results they asked for. + return resolutionErr +} + +// handleLongQueue reports a scan that is still queued once progress polling +// gives up. Passing on it is opt-in via --pass-on-timeout, which also covers +// service access timeouts. +func (dScanner *DebrickedScanner) handleLongQueue( + options DebrickedOptions, + result *upload.UploadResult, + resolutionErr error, +) error { + fmt.Println("Progress polling terminated due to long scan times. Please try again later") + fmt.Printf("For full details, visit: %s\n\n", color.BlueString(result.DetailsUrl)) + + if options.PassOnTimeOut { + return resolutionErr } - WriteApiReplyToJsonFile(dOptions, result) + return LongQueueErr +} + +// reportResult renders a completed scan and reports whether a triggered +// automation rule requires the pipeline to fail. +func (dScanner *DebrickedScanner) reportResult(options DebrickedOptions, result *upload.UploadResult) bool { + WriteApiReplyToJsonFile(options, result) fmt.Printf("\n%d vulnerabilities found\n", result.VulnerabilitiesFound) fmt.Println("") @@ -152,11 +188,25 @@ func (dScanner *DebrickedScanner) Scan(o IOptions) error { failPipeline = failPipeline || (rule.Triggered && rule.FailPipeline()) } fmt.Printf("For full details, visit: %s\n\n", color.BlueString(result.DetailsUrl)) - if failPipeline { - return FailPipelineErr + + return failPipeline +} + +// isFatalResolutionErr reports whether a resolution error should abort the scan +// before anything is uploaded. Resolution reports non-fatal outcomes as a +// CommandError carrying the exit code the CLI should eventually exit with; +// those let the scan run to completion. Anything else stops it. +func isFatalResolutionErr(err error) bool { + if err == nil { + return false } - return nil + var cmdErr cmderror.CommandError + if !errors.As(err, &cmdErr) { + return true + } + + return cmdErr.Code == 1 } func (dScanner *DebrickedScanner) scanReportSBOM(options DebrickedOptions, detailsURL string) error { @@ -183,12 +233,13 @@ func (dScanner *DebrickedScanner) scanReportSBOM(options DebrickedOptions, detai func (dScanner *DebrickedScanner) scanResolve(options DebrickedOptions) error { resolveOptions := resolution.DebrickedOptions{ - Path: options.Path, - Verbose: options.Verbose, - Regenerate: options.Regenerate, - Exclusions: options.Exclusions, - Inclusions: options.Inclusions, - NpmPreferred: options.NpmPreferred, + Path: options.Path, + Verbose: options.Verbose, + Regenerate: options.Regenerate, + Exclusions: options.Exclusions, + Inclusions: options.Inclusions, + NpmPreferred: options.NpmPreferred, + ResolutionStrictness: options.ResolutionStrictness, } if options.Resolve { _, resErr := dScanner.resolver.Resolve([]string{options.Path}, resolveOptions) @@ -229,14 +280,8 @@ func (dScanner *DebrickedScanner) scanFingerprint(options DebrickedOptions) erro func (dScanner *DebrickedScanner) scan(options DebrickedOptions, gitMetaObject git.MetaObject) (*upload.UploadResult, error) { - debug.Log("Running scanResolve...", options.Debug) - err := dScanner.scanResolve(options) - if err != nil { - return nil, err - } - debug.Log("Running scanFingerprint...", options.Debug) - err = dScanner.scanFingerprint(options) + err := dScanner.scanFingerprint(options) if err != nil { return nil, err } diff --git a/internal/scan/scanner_test.go b/internal/scan/scanner_test.go index beb15dc3..894e20a1 100644 --- a/internal/scan/scanner_test.go +++ b/internal/scan/scanner_test.go @@ -27,6 +27,7 @@ import ( "github.com/debricked/cli/internal/ci/travis" "github.com/debricked/cli/internal/client" "github.com/debricked/cli/internal/client/testdata" + "github.com/debricked/cli/internal/cmd/cmderror" "github.com/debricked/cli/internal/file" "github.com/debricked/cli/internal/fingerprint" "github.com/debricked/cli/internal/git" @@ -297,7 +298,7 @@ func TestScanEmptyResult(t *testing.T) { string(out), "Progress polling terminated due to long scan times. Please try again later") - assert.NoError(t, err, "failed to assert that scan ran without errors") + assert.ErrorIs(t, err, LongQueueErr, "failed to assert that scan returned LongQueueErr") assert.True(t, existsMessageInCMDOutput, "failed to assert that scan ran without errors") existsMessageInCMDOutputDetails := strings.Contains( @@ -306,6 +307,41 @@ func TestScanEmptyResult(t *testing.T) { assert.True(t, existsMessageInCMDOutputDetails, "failed to assert that long queue scan contain detailed url") } +func TestScanEmptyResultPassOnTimeOut(t *testing.T) { + if runtime.GOOS == windowsOS { + t.Skipf("TestScan is skipped due to Windows env") + } + clientMock := testdata.NewDebClientMock() + addMockedFormatsResponse(clientMock, "package\\.json") + addMockedFileUploadResponse(clientMock) + addMockedFinishResponse(clientMock, http.StatusNoContent) + addMockedStatusResponse(clientMock, http.StatusOK, 50) + addMockedQueueTooLongStatusResponse(clientMock) + + scanner := makeScanner(clientMock, nil, nil) + cwd, _ := os.Getwd() + defer resetWd(t, cwd) + + opts := DebrickedOptions{ + Path: testdataNpm, + RepositoryName: testdataNpm, + CommitName: testdataNpm, + PassOnTimeOut: true, + } + + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + err := scanner.Scan(opts) + + _ = w.Close() + _, _ = io.ReadAll(r) + os.Stdout = rescueStdout + + assert.NoError(t, err, "failed to assert that a long queue passes when pass-on-timeout is set") +} + func TestScanInCiWithPathSet(t *testing.T) { var debClient client.IDebClient = testdata.NewDebClientMock() scanner := NewDebrickedScanner(&debClient, nil, nil, ciService, nil, nil, nil) @@ -388,6 +424,114 @@ func TestScanWithResolveErr(t *testing.T) { assert.ErrorIs(t, err, resolutionErr) } +// A resolution failure that resolution deems fatal (exit code 1) must stop the +// scan before anything is uploaded. The client mock has no upload responses +// registered, so reaching the upload at all would surface a different error. +func TestScanWithFatalResolveCommandErr(t *testing.T) { + clientMock := testdata.NewDebClientMock() + resolutionErr := cmderror.CommandError{Code: 1, Err: errors.New("resolution failed")} + scanner := makeScanner(clientMock, &resolveTestdata.ResolverMock{Err: resolutionErr}, nil) + cwd, _ := os.Getwd() + defer resetWd(t, cwd) + + opts := DebrickedOptions{ + Path: testdataNpm, + Resolve: true, + RepositoryName: testdataNpm, + CommitName: "testdata/npm-commit", + ResolutionStrictness: resolution.FailIfAllFail, + } + err := scanner.Scan(opts) + + var cmdErr cmderror.CommandError + assert.True(t, errors.As(err, &cmdErr), "failed to assert that a CommandError was returned") + assert.Equal(t, 1, cmdErr.Code) +} + +// A partial resolution failure under FailOrWarn must not cost the user their +// scan. The scan has to run to completion and only then surface exit code 3. +func TestScanWithNonFatalResolveCommandErr(t *testing.T) { + clientMock := testdata.NewDebClientMock() + addMockedFormatsResponse(clientMock, "yarn\\.lock") + addMockedFileUploadResponse(clientMock) + addMockedFinishResponse(clientMock, http.StatusNoContent) + addMockedStatusResponse(clientMock, http.StatusOK, 100) + + resolutionErr := cmderror.CommandError{Code: 3, Err: errors.New("resolution failed")} + resolverMock := resolveTestdata.ResolverMock{Err: resolutionErr} + resolverMock.SetFiles([]string{"yarn.lock"}) + + scanner := makeScanner(clientMock, &resolverMock, nil) + + cwd, _ := os.Getwd() + defer resetWd(t, cwd) + defer cleanUpResolution(t, resolverMock) + + opts := DebrickedOptions{ + Path: testdataNpm, + Resolve: true, + RepositoryName: testdataNpm, + CommitName: "testdata/npm-commit", + ResolutionStrictness: resolution.FailOrWarn, + } + rescueStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + // Drain concurrently. This scan renders a progress bar, and reading only + // after Scan returns deadlocks as soon as the output exceeds the pipe + // buffer - which is small enough on Windows to hit. + outC := make(chan string, 1) + go func() { + var buf bytes.Buffer + _, _ = io.Copy(&buf, r) + outC <- buf.String() + }() + + err := scanner.Scan(opts) + + _ = w.Close() + os.Stdout = rescueStdout + out := <-outC + + var cmdErr cmderror.CommandError + assert.True(t, errors.As(err, &cmdErr), "failed to assert that a CommandError was returned") + assert.Equal(t, 3, cmdErr.Code) + assert.Contains(t, out, "vulnerabilities found", + "failed to assert that the scan ran to completion despite the resolution failure") +} + +func TestScanPassesResolutionStrictnessToResolver(t *testing.T) { + clientMock := testdata.NewDebClientMock() + addMockedFormatsResponse(clientMock, "yarn\\.lock") + addMockedFileUploadResponse(clientMock) + addMockedFinishResponse(clientMock, http.StatusNoContent) + addMockedStatusResponse(clientMock, http.StatusOK, 100) + + resolverMock := resolveTestdata.ResolverMock{} + resolverMock.SetFiles([]string{"yarn.lock"}) + + scanner := makeScanner(clientMock, &resolverMock, nil) + + cwd, _ := os.Getwd() + defer resetWd(t, cwd) + defer cleanUpResolution(t, resolverMock) + + opts := DebrickedOptions{ + Path: testdataNpm, + Resolve: true, + RepositoryName: testdataNpm, + CommitName: "testdata/npm-commit", + ResolutionStrictness: resolution.FailIfAnyFail, + } + err := scanner.Scan(opts) + assert.NoError(t, err) + + resolveOptions, ok := resolverMock.Options.(resolution.DebrickedOptions) + assert.True(t, ok, "failed to assert that resolve options were passed") + assert.Equal(t, resolution.FailIfAnyFail, resolveOptions.ResolutionStrictness) +} + // TestScanWithResolveErr tests that the scan is not aborted if the resolution fails var dOptionsTemplate = DebrickedOptions{ Path: "path",