diff --git a/internal/tool/tool.go b/internal/tool/tool.go index 6ecd42b..c5119e9 100644 --- a/internal/tool/tool.go +++ b/internal/tool/tool.go @@ -81,32 +81,43 @@ func (t codacyTrivy) Run(ctx context.Context, toolExecution codacy.ToolExecution // This is the only way to suppress Trivy logs. log.InitLogger(false, true) - report, err := t.runBaseScan(ctx, toolExecution.SourceDir) - if err != nil { - return nil, err - } + allIssues := []codacy.Result{} - sbom, err := t.getSBOM(ctx, report) - if err != nil { - return nil, err - } + // The dependency scan covers the whole source directory regardless of the requested files, and the SBOM is derived + // from it, so both are wasted work when the execution has no SCA pattern to report them under. + if scaScanningEnabled(*toolExecution.Patterns) { + report, err := t.runBaseScan(ctx, toolExecution.SourceDir) + if err != nil { + return nil, err + } - vulnerabilityScanningIssues, err := t.getVulnerabilities(ctx, report, toolExecution) - if err != nil { - return nil, err - } + sbom, err := t.getSBOM(ctx, report) + if err != nil { + return nil, err + } - secretScanningIssues := t.runSecretScanning(toolExecution) + vulnerabilityScanningIssues, err := t.getVulnerabilities(ctx, report, toolExecution) + if err != nil { + return nil, err + } - maliciousPackagesIssues := t.maliciousPackagesScanner.Scan(report, toolExecution) + allIssues = append(allIssues, vulnerabilityScanningIssues...) + allIssues = append(allIssues, t.maliciousPackagesScanner.Scan(report, toolExecution)...) + allIssues = append(allIssues, sbom) + } - allIssues := append(vulnerabilityScanningIssues, secretScanningIssues...) - allIssues = append(allIssues, maliciousPackagesIssues...) - allIssues = append(allIssues, sbom) + allIssues = append(allIssues, t.runSecretScanning(toolExecution)...) return allIssues, nil } +// scaScanningEnabled returns whether any of the given patterns needs the dependency scan. +func scaScanningEnabled(patterns []codacy.Pattern) bool { + return lo.SomeBy(patterns, func(p codacy.Pattern) bool { + return p.ID == ruleIDMaliciousPackages || lo.Contains(ruleIDsVulnerability, p.ID) + }) +} + // runBaseScan will run a vulnerability scan that produces a report to be used for SBOM generation or for vulnerability issues. func (t codacyTrivy) runBaseScan(ctx context.Context, sourceDir string) (ptypes.Report, error) { config := flag.Options{ diff --git a/internal/tool/tool_test.go b/internal/tool/tool_test.go index f0015fc..ed79676 100644 --- a/internal/tool/tool_test.go +++ b/internal/tool/tool_test.go @@ -627,6 +627,75 @@ func TestRunVulnerabilityScanningNotEnabled(t *testing.T) { assert.Empty(t, results) } +func TestRunSkipsDependencyScanWhenScaNotEnabled(t *testing.T) { + // Arrange + srcDir, err := os.MkdirTemp("", "") + if err != nil { + assert.FailNow(t, "Failed to create tmp directory", err.Error()) + } + defer os.RemoveAll(srcDir) + + f, err := os.CreateTemp(srcDir, "file-") + if err != nil { + assert.FailNow(t, "Failed to create tmp file", err.Error()) + } + defer f.Close() + + if _, err := f.Write([]byte("AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF")); err != nil { + assert.FailNow(t, "Failed to write to tmp file", err.Error()) + } + + toolExecution := codacy.ToolExecution{ + Patterns: &[]codacy.Pattern{{ID: ruleIDSecret}}, + Files: &[]string{filepath.Base(f.Name())}, + SourceDir: srcDir, + } + + // A factory that only fails: reaching it at all would surface as an error from Run. + underTest := codacyTrivy{runnerFactory: errorRunnerFactory{err: assert.AnError}} + + // Act + results, err := underTest.Run(context.Background(), toolExecution) + + // Assert + assert.NoError(t, err) + assert.NotEmpty(t, results, "secret scanning still reports") + + sboms := lo.Filter(results, func(result codacy.Result, _ int) bool { + _, isSBOM := result.(codacy.SBOM) + return isSBOM + }) + assert.Empty(t, sboms, "the SBOM is derived from the dependency scan, so there is none") +} + +func TestScaScanningEnabled(t *testing.T) { + // Arrange + type testData struct { + name string + patterns []codacy.Pattern + expected bool + } + + tests := []testData{ + {name: "no patterns", patterns: []codacy.Pattern{}, expected: false}, + {name: "only secret", patterns: []codacy.Pattern{{ID: ruleIDSecret}}, expected: false}, + {name: "only unknown", patterns: []codacy.Pattern{{ID: "unknown"}}, expected: false}, + {name: "a vulnerability severity", patterns: []codacy.Pattern{{ID: ruleIDVulnerabilityMinor}}, expected: true}, + {name: "malicious packages", patterns: []codacy.Pattern{{ID: ruleIDMaliciousPackages}}, expected: true}, + { + name: "secret alongside a vulnerability severity", + patterns: []codacy.Pattern{{ID: ruleIDSecret}, {ID: ruleIDVulnerabilityCritical}}, + expected: true, + }, + } + + for _, testData := range tests { + t.Run(testData.name, func(t *testing.T) { + assert.Equal(t, testData.expected, scaScanningEnabled(testData.patterns)) + }) + } +} + func TestRunSecretScanningNotEnabled(t *testing.T) { toolExecution := codacy.ToolExecution{ Patterns: &[]codacy.Pattern{{ID: ruleIDVulnerabilityMedium}},