Skip to content

Commit 665fa75

Browse files
committed
feat(sast): test-suite detection with config-file and dependency corroboration
Detect findings located in the project's test suite via an exhaustive path table corroborated by test-runner config files and declared test dependencies; emit the attribution as SARIF result properties and typed wire fields, upload detected config files as env metadata, and add --suppress-test-code.
1 parent 774ff09 commit 665fa75

13 files changed

Lines changed: 1206 additions & 7 deletions

File tree

cmd/sarif_persist.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ var sarifKinds = []sarifScanKind{
5252
// to its matching /v2/cli.<kind> endpoint. When baseSnapshotUuid is set, the
5353
// first SARIF request attaches to that existing SCA snapshot; otherwise the
5454
// SARIF endpoint creates its own snapshot as before.
55-
func postScanSARIF(report *sast.SASTReport, enabledKinds map[string]bool, gitCtx *gitctx.GitContext, rootPath string, snippetContext int, baseSnapshotUuid string, suppressions []vdb.CliSuppressionMint, w io.Writer) ([]snapshotLink, map[string]string, []vdb.CliSuppressionResult) {
55+
func postScanSARIF(report *sast.SASTReport, enabledKinds map[string]bool, gitCtx *gitctx.GitContext, rootPath string, snippetContext int, baseSnapshotUuid string, suppressions []vdb.CliSuppressionMint, testConfigs []vdb.CliTestConfigMetadata, w io.Writer) ([]snapshotLink, map[string]string, []vdb.CliSuppressionResult) {
5656
if report == nil {
5757
return nil, nil, nil
5858
}
@@ -88,7 +88,14 @@ func postScanSARIF(report *sast.SASTReport, enabledKinds map[string]bool, gitCtx
8888
thisSupp = pendingSupp
8989
sentSupp = true // attempted on this kind; never resend (avoid dup upsert)
9090
}
91-
link, snapshotUuid, results, ok := submitSARIFKind(client, env, sk, bucket, memRecords, rootPath, snippetContext, baseSnapshotUuid, thisSupp, w)
91+
// Detected test-runner config files describe the SAST test surface, so
92+
// they ride the SAST submission's env only (each kind gets its own
93+
// snapshot; attaching everywhere would duplicate the rows).
94+
kindEnv := env
95+
if sk.kind == "sast" {
96+
kindEnv.TestConfigs = testConfigs
97+
}
98+
link, snapshotUuid, results, ok := submitSARIFKind(client, kindEnv, sk, bucket, memRecords, rootPath, snippetContext, baseSnapshotUuid, thisSupp, w)
9299
if len(thisSupp) > 0 && len(results) > 0 {
93100
suppResults = results
94101
}
@@ -243,6 +250,12 @@ func buildAPISARIFFinding(f sast.Finding, memRecords map[string]memory.FindingRe
243250
MemoryVexStatus: mem.Status,
244251
MemoryVexJustification: mem.Justification,
245252
MemoryVexAction: mem.ActionResponse,
253+
IsTestSuite: f.IsTestSuite,
254+
TestFramework: f.TestFramework,
255+
TestLanguage: f.TestLanguage,
256+
TestConfidence: f.TestConfidence,
257+
TestMatchedPattern: f.TestMatchedPattern,
258+
TestEvidence: f.TestEvidence,
246259
}
247260
}
248261

cmd/scan.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/vulnetix/cli/v3/internal/memory"
2222
"github.com/vulnetix/cli/v3/internal/sast"
2323
"github.com/vulnetix/cli/v3/internal/scan"
24+
"github.com/vulnetix/cli/v3/internal/testsuite"
2425
"github.com/vulnetix/cli/v3/internal/triage"
2526
"github.com/vulnetix/cli/v3/internal/tui"
2627
"github.com/vulnetix/cli/v3/internal/update"
@@ -379,6 +380,7 @@ func runScanWithFeatures(ctx context.Context, cmd *cobra.Command, noSAST, noSCA,
379380
if cmd.Flags().Changed("snippet-context") {
380381
snippetContext, _ = cmd.Flags().GetInt("snippet-context")
381382
}
383+
suppressTestCode, _ = cmd.Flags().GetBool("suppress-test-code")
382384
excludes, _ := cmd.Flags().GetStringArray("exclude")
383385
ignoreGlobs, _ := cmd.Flags().GetStringArray("ignore")
384386
ignoreGit, _ := cmd.Flags().GetBool("ignore-git")
@@ -1508,6 +1510,31 @@ func runLocalScan(
15081510
fmt.Sprintf("%d finding(s) suppressed by ignore rules", n))
15091511
}
15101512
}
1513+
1514+
// Test-suite attribution: mark findings that live in the project's
1515+
// test code, corroborated by test-runner config files and declared
1516+
// test-framework dependencies found in the repo. The resulting
1517+
// metadata rides the SARIF (result properties) + typed wire findings,
1518+
// and the detected config files ride the SAST env. Run before SARIF
1519+
// build so both on-disk and uploaded artefacts carry it.
1520+
var testSuppressionMints []vdb.CliSuppressionMint
1521+
testActive := testsuite.Scan(rootPath)
1522+
testMarked := testsuite.Annotate(sastReport.Findings, testActive)
1523+
testConfigMeta := testConfigsToWire(testActive.Configs)
1524+
if testMarked > 0 {
1525+
sastReport.Degradations = append(sastReport.Degradations,
1526+
fmt.Sprintf("%d finding(s) attributed to test suites", testMarked))
1527+
}
1528+
// Optionally suppress test-code SAST findings when the user opts in.
1529+
if suppressTestCode && testMarked > 0 {
1530+
if kept, mints := suppressTestFindings(sastReport.Findings, gitCtx); len(mints) > 0 {
1531+
sastReport.Findings = kept
1532+
testSuppressionMints = mints
1533+
sastReport.Degradations = append(sastReport.Degradations,
1534+
fmt.Sprintf("%d test-code finding(s) suppressed (--suppress-test-code)", len(mints)))
1535+
}
1536+
}
1537+
15111538
rec.SASTRulesLoaded = sastReport.RulesLoaded
15121539
rec.SASTFindingCount = len(sastReport.Findings)
15131540

@@ -1662,6 +1689,8 @@ func runLocalScan(
16621689
if !disableMemory {
16631690
suppressionMints = reconcileScanSuppressions(mem, gitCtx, nosecHits, rootPath, time.Now().Unix())
16641691
}
1692+
// Test-code suppressions (--suppress-test-code) ride the same mint list.
1693+
suppressionMints = append(suppressionMints, testSuppressionMints...)
16651694
if !isUnauthenticatedScan() {
16661695
// Which SARIF-family scanners actually ran — an enabled one that
16671696
// found nothing still submits so the backend records coverage.
@@ -1672,7 +1701,7 @@ func runLocalScan(
16721701
"oci": !noContainers,
16731702
}
16741703
var suppResults []vdb.CliSuppressionResult
1675-
sarifSnapshots, sarifSnapshotUuids, suppResults = postScanSARIF(sastReport, enabledKinds, gitCtx, rootPath, snippetContext, scaSnapshotUuid, suppressionMints, progressStderr)
1704+
sarifSnapshots, sarifSnapshotUuids, suppResults = postScanSARIF(sastReport, enabledKinds, gitCtx, rootPath, snippetContext, scaSnapshotUuid, suppressionMints, testConfigMeta, progressStderr)
16761705
applyMintedSuppressionUUIDs(mem, suppResults)
16771706
}
16781707
}
@@ -4408,8 +4437,14 @@ func addSASTFlags(cmd *cobra.Command) {
44084437
"Override default registry (https://github.com) for all --rule repos")
44094438
cmd.Flags().String("rule-id", "",
44104439
"Run only the single SAST rule with this ID (e.g. VNX-GQL-004); skips SCA and license checks")
4440+
cmd.Flags().Bool("suppress-test-code", false,
4441+
"Suppress SAST findings located in the project's test suite (test files corroborated by test-runner config/dependencies)")
44114442
}
44124443

4444+
// suppressTestCode is set from the --suppress-test-code flag in
4445+
// runScanWithFeatures and read by runLocalScan.
4446+
var suppressTestCode bool
4447+
44134448
// filterFilesByFeature removes detected files excluded by the active feature flags.
44144449
// noSCA removes ordinary package manifests; noContainers removes docker/OCI
44154450
// manifests; noIAC removes HCL and Nix manifests.

cmd/testsuite_scan.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package cmd
2+
3+
import (
4+
"github.com/vulnetix/cli/v3/internal/gitctx"
5+
"github.com/vulnetix/cli/v3/internal/sast"
6+
"github.com/vulnetix/cli/v3/internal/testsuite"
7+
"github.com/vulnetix/cli/v3/pkg/vdb"
8+
)
9+
10+
// testConfigsToWire converts detected test-runner config files into the typed
11+
// env metadata carried on the SAST submission (metadata only, no raw body).
12+
func testConfigsToWire(configs []testsuite.Config) []vdb.CliTestConfigMetadata {
13+
if len(configs) == 0 {
14+
return nil
15+
}
16+
out := make([]vdb.CliTestConfigMetadata, 0, len(configs))
17+
for _, c := range configs {
18+
out = append(out, vdb.CliTestConfigMetadata{
19+
Path: c.Path,
20+
Framework: c.Framework,
21+
Language: c.Language,
22+
ContentType: c.ContentType,
23+
SHA256: c.SHA256,
24+
Size: c.Size,
25+
})
26+
}
27+
return out
28+
}
29+
30+
// suppressTestFindings removes findings attributed to the test suite from the
31+
// report and returns a CliSuppressionMint per removed finding, so the backend
32+
// records them as suppressed (analogous to nosec-driven suppressions) rather
33+
// than as active findings. Only used when --suppress-test-code is set.
34+
func suppressTestFindings(findings []sast.Finding, gitCtx *gitctx.GitContext) ([]sast.Finding, []vdb.CliSuppressionMint) {
35+
branch := ""
36+
if gitCtx != nil {
37+
branch = gitCtx.CurrentBranch
38+
}
39+
kept := make([]sast.Finding, 0, len(findings))
40+
var mints []vdb.CliSuppressionMint
41+
for _, f := range findings {
42+
if !f.IsTestSuite {
43+
kept = append(kept, f)
44+
continue
45+
}
46+
category := "sast"
47+
if f.Metadata != nil && f.Metadata.Kind != "" {
48+
category = f.Metadata.Kind
49+
}
50+
reason := "Located in test suite"
51+
if f.TestFramework != "" {
52+
reason += " (" + f.TestFramework + ")"
53+
}
54+
mints = append(mints, vdb.CliSuppressionMint{
55+
RuleID: f.RuleID,
56+
Category: category,
57+
SuppressionType: "test-code",
58+
Reason: reason,
59+
FilePath: f.ArtifactURI,
60+
LineNumber: f.StartLine,
61+
CodeSnippet: f.Snippet,
62+
BranchName: branch,
63+
Origin: "cli-test-code",
64+
Active: true,
65+
Fingerprint: f.Fingerprint,
66+
})
67+
}
68+
return kept, mints
69+
}

internal/sast/rule.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ type Finding struct {
3333
Snippet string `json:"snippet"`
3434
Fingerprint string `json:"-"`
3535
Metadata *RuleMetadata `json:"-"`
36+
37+
// Test-suite attribution, set post-evaluation by internal/testsuite when the
38+
// finding's file belongs to the project's test suite. IsTestSuite drives the
39+
// SARIF `vulnetix/test-*` result properties and the typed wire fields.
40+
IsTestSuite bool `json:"-"`
41+
TestFramework string `json:"-"`
42+
TestLanguage string `json:"-"`
43+
TestConfidence string `json:"-"`
44+
TestMatchedPattern string `json:"-"`
45+
TestEvidence []string `json:"-"`
3646
}
3747

3848
// SeverityToLevel maps severity to the default SARIF level when a rule

internal/sast/sarif.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,24 @@ func BuildSARIF(findings []Finding, rules []RuleMetadata, toolVersion string) *S
224224
result.Properties = SARIFPropertyBag{
225225
"severity": f.Severity,
226226
}
227+
if f.IsTestSuite {
228+
result.Properties["vulnetix/test-suite"] = true
229+
if f.TestFramework != "" {
230+
result.Properties["vulnetix/test-framework"] = f.TestFramework
231+
}
232+
if f.TestLanguage != "" {
233+
result.Properties["vulnetix/test-language"] = f.TestLanguage
234+
}
235+
if f.TestConfidence != "" {
236+
result.Properties["vulnetix/test-confidence"] = f.TestConfidence
237+
}
238+
if f.TestMatchedPattern != "" {
239+
result.Properties["vulnetix/test-matched-pattern"] = f.TestMatchedPattern
240+
}
241+
if len(f.TestEvidence) > 0 {
242+
result.Properties["vulnetix/test-evidence"] = f.TestEvidence
243+
}
244+
}
227245

228246
results = append(results, result)
229247
}

0 commit comments

Comments
 (0)