From 116c43617537d08357718e669d3571592ac6f862 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 17:54:35 +0000 Subject: [PATCH 1/8] Detect committed SARIF history --- README.md | 7 +- detector.go | 1 + go.mod | 2 + go.sum | 4 + repository_files.go | 195 ++++++++++++++++++++++++++++++++++++--- repository_files_test.go | 62 +++++++++++++ 6 files changed, 256 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 4fd932d..70a358f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # signals -A command for tracing security-relevant evidence stored in Git across repository history. It shows when detected evidence appears, changes, or disappears, with the commit and path behind each change. `signals` walks commit trees with [`github.com/git-pkgs/history`](https://github.com/git-pkgs/history). Local parsers detect workflow risks and permissions, artifact-provenance configuration, CI and security tools, threat surface, secrets, certificates and trust stores, Agent Skills, sensitive ignore policy, REUSE metadata, package managers, manifests, lockfiles, dependency pinning, update configuration, licenses, SBOMs, fuzzers, and binary artifacts. Commit data adds dependency-bot and signature events plus contributor, maintenance, review, and signing metrics. The output contains the evidence and its provenance without calculating a score. The scan reads local Git objects, with forge APIs, release APIs, credential validation, and external services disabled. +A command for tracing security-relevant evidence stored in Git across repository history. It shows when detected evidence appears, changes, or disappears, with the commit and path behind each change. `signals` walks commit trees with [`github.com/git-pkgs/history`](https://github.com/git-pkgs/history). Local parsers detect workflow risks and permissions, artifact-provenance configuration, CI and security tools, threat surface, secrets, certificates and trust stores, stored SARIF results, Agent Skills, sensitive ignore policy, REUSE metadata, package managers, manifests, lockfiles, dependency pinning, update configuration, licenses, SBOMs, fuzzers, and binary artifacts. Commit data adds dependency-bot and signature events plus contributor, maintenance, review, and signing metrics. The output contains the evidence and its provenance without calculating a score. The scan reads local Git objects, with forge APIs, release APIs, credential validation, and external services disabled. The local-only timeline answers questions about when repository evidence changed. Examples include: @@ -154,6 +154,7 @@ The additional detectors use the same persistent signal model. A threat-surface | Packaging | Literal packaging and publication commands or known publication Actions; presence does not show that a workflow ran | | Package-Provenance | Explicit npm provenance flags or environment settings, and PyPI publication configured with an OIDC token | | Runner-Selection | Explicit self-hosted runner labels, runner groups, and dynamic runner expressions in GitHub Actions jobs | +| SARIF | Bounded summaries of committed SARIF tools, result counts and levels, baselines, rules, suppressions, and CWE taxa with names and categories | | Package-Parse | Package files that were identified but could not be parsed | | Secret | Findings from the embedded `git-pkgs/secrets` rule corpus; output contains `[redacted]` in place of the credential | | Sensitive-Ignore | Effective `.gitignore` rules covering a bounded list of environment, credential, private-key, secret, state, and database paths, with the source pattern and line | @@ -163,7 +164,7 @@ The additional detectors use the same persistent signal model. A threat-surface The root commit and missing history boundaries receive a full scan. Later commits reuse their first parent's signal state and update the paths changed by that commit. Cross-file detectors, including Agent Skills, dependency-update configuration, REUSE metadata, and sensitive ignore policy, receive a full commit tree when one of their inputs changes. Agent Skills rerun when a `SKILL.md` or a file inside a known skill package changes. Sensitive ignore policy reruns only when a project `.gitignore` changes. -Parser results for changed files are cached by Git blob OID and path for the whole run. Repository files that cannot contain a certificate, license, SBOM, fuzzer, policy, or SAST configuration need only a 1 KB prefix for binary classification. Commit reading can run concurrently, while signal-state updates remain ordered. `github.com/git-pkgs/languages` keeps a language inventory so Brief can rerun language-gated detection only when needed. `github.com/git-pkgs/roles` removes bundled and derived paths from fuzzing and license detection, and removes test, fixture, example, cache, and build-output paths from certificate detection. Test paths remain eligible for fuzz detection because fuzz targets commonly use them. +Parser results for changed files are cached by Git blob OID and path for the whole run. Repository files that cannot contain a certificate, license, SARIF log, SBOM, fuzzer, policy, or SAST configuration need only a 1 KB prefix for binary classification. SARIF candidates use streaming byte, tool, and distinct-value limits. Commit reading can run concurrently, while signal-state updates remain ordered. `github.com/git-pkgs/languages` keeps a language inventory so Brief can rerun language-gated detection only when needed. `github.com/git-pkgs/roles` removes bundled and derived paths from fuzzing and license detection, and removes test, fixture, example, cache, and build-output paths from certificate detection. Test paths remain eligible for fuzz detection because fuzz targets commonly use them. Brief reads commit trees through `history.TreeFS` and reruns only when Brief's changed-file filter identifies an input to one of the emitted tool signals. Package files are parsed when their paths change, and the parse result supplies dependency constraints, pins, Actions references, container images, lockfile resolution, integrity records, and manifest-declared license values or file references. Results are cached by blob OID and path. Shell commands that download or install packages are not yet included in this local package detector. Package-manager detection uses `managers.AffectedDirectories` to rescan only the affected projects. Secret scanning updates changed paths and caches results by blob OID and path because some rules depend on the filename. REUSE metadata changes trigger a full REUSE scan; ordinary changes update the affected file state and coverage count. @@ -179,6 +180,8 @@ The local detectors share parsers and changed-path caches across related signals - [`github.com/git-pkgs/managers`](https://github.com/git-pkgs/managers) identifies package managers and their manifest or lockfile evidence from commit trees. - [`github.com/git-pkgs/sbom`](https://github.com/git-pkgs/sbom) identifies and parses SPDX and CycloneDX JSON documents. - [`github.com/git-pkgs/spdx`](https://github.com/git-pkgs/spdx) validates and normalizes SPDX expressions declared by package manifests. +- [`github.com/git-pkgs/sarif`](https://github.com/git-pkgs/sarif) reads bounded summaries from committed SARIF logs. +- [`github.com/git-pkgs/cwe`](https://github.com/git-pkgs/cwe) adds names and categories to CWE taxa referenced by SARIF results. - [`github.com/git-pkgs/brief`](https://github.com/git-pkgs/brief) detects CI systems, build tools, security tools, and pre-commit runners from an `fs.FS` commit tree. - [`github.com/git-pkgs/languages`](https://github.com/git-pkgs/languages) detects source languages from paths and blob content. - [`github.com/git-pkgs/reuse`](https://github.com/git-pkgs/reuse) resolves SPDX headers, sidecars, REUSE.toml annotations, and DEP5 metadata from an `fs.FS` commit tree. diff --git a/detector.go b/detector.go index a51f011..63adb92 100644 --- a/detector.go +++ b/detector.go @@ -14,6 +14,7 @@ const ( checkDependencyUpdateTool = "Dependency-Update-Tool" checkFuzzing = "Fuzzing" checkLicense = "License" + checkSARIF = "SARIF" checkSAST = "SAST" checkSBOM = "SBOM" checkSecurityPolicy = "Security-Policy" diff --git a/go.mod b/go.mod index a000e16..f691941 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( actionlint.kjanat.dev v1.17.0 github.com/git-pkgs/brief v0.15.1-0.20261107204040-d1ed5a631f83 github.com/git-pkgs/certs v0.1.1-0.20261104093717-2e1d0e2e0f63 + github.com/git-pkgs/cwe v0.1.2-0.20261102114243-2d9276da42a5 github.com/git-pkgs/gitignore v1.3.1-0.20261108194038-d09d5ccc19b9 github.com/git-pkgs/history v0.2.0 github.com/git-pkgs/languages v0.1.0 @@ -16,6 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 + github.com/git-pkgs/sarif v0.1.4-0.20261108175116-14294dee0dad github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index 5adfd2d..5baca2d 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,8 @@ github.com/git-pkgs/citation v0.1.0 h1:0toQBjxVOtaUsxRtcDJL9uZ6SYFb6IgbHgKpTpP0c github.com/git-pkgs/citation v0.1.0/go.mod h1:4vrS78/jjHeusS+eozjP/d3pX2NogEi1R/7gLVqkgqs= github.com/git-pkgs/codemeta v0.3.0 h1:zkWndmquCv2tPIHCHb+JXx3eKjmxRoD48f4/3sNAlGw= github.com/git-pkgs/codemeta v0.3.0/go.mod h1:R1vGXVohTa22vVKaV3/8xVs3+tcgvqc4ziaMikdXc/I= +github.com/git-pkgs/cwe v0.1.2-0.20261102114243-2d9276da42a5 h1:6RiZJiHYlzqQM2SteX96wk4l9eZ6CONI2ji4UT9D8fg= +github.com/git-pkgs/cwe v0.1.2-0.20261102114243-2d9276da42a5/go.mod h1:NYsSggIvPa2LcKe0nrTj7v+dJs8v3BFpPkLKqtBiZOA= github.com/git-pkgs/gitignore v1.3.1-0.20261108194038-d09d5ccc19b9 h1:+1HyWVLjMCi4Xoq9Gh1oq2tmbNn1Aa52tShtuPGddOo= github.com/git-pkgs/gitignore v1.3.1-0.20261108194038-d09d5ccc19b9/go.mod h1:zleLGZ4PhQrUXrJSsVQWKoeEllEjJH49+DjyKpzIFPM= github.com/git-pkgs/history v0.2.0 h1:UCbrp/PX6rfLukUCRnYme8PtyCqKO8Q5wrpDUY0SOTE= @@ -66,6 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= +github.com/git-pkgs/sarif v0.1.4-0.20261108175116-14294dee0dad h1:k0EAsiSaSaIG6nNeheAlDqqAkRqYU1z8a8HW8PvJllA= +github.com/git-pkgs/sarif v0.1.4-0.20261108175116-14294dee0dad/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= diff --git a/repository_files.go b/repository_files.go index 97e6448..cfa3d12 100644 --- a/repository_files.go +++ b/repository_files.go @@ -19,11 +19,13 @@ import ( "unicode" gitcerts "github.com/git-pkgs/certs" + "github.com/git-pkgs/cwe" githistory "github.com/git-pkgs/history" gitlanguages "github.com/git-pkgs/languages" gitlicenses "github.com/git-pkgs/licenses" "github.com/git-pkgs/magic" "github.com/git-pkgs/roles" + sarifsummary "github.com/git-pkgs/sarif/summary" "github.com/git-pkgs/sbom" ) @@ -95,6 +97,9 @@ const ( asn1HeaderBytes = 2 asn1MaxLengthBytes = 4 bitsPerByte = 8 + sarifMaxBytes = 16 << 20 + sarifMaxUniqueValues = 5_000 + sarifMaxTools = 100 ) var certificateFileExtensions = map[string]bool{ @@ -226,19 +231,11 @@ func repositoryFileSignalsFromFS( }) } - if sbomContentPath(filePath) { - parsed, err := scanRepositoryFile(tree, filePath, func(reader io.Reader) ([]Signal, error) { - signal, ok, inspectErr := sbomSignal(filePath, reader) - if !ok { - return nil, inspectErr - } - return []Signal{signal}, inspectErr - }) - if err != nil { - return nil, err - } - signals = append(signals, parsed...) + structured, err := repositoryStructuredSignals(tree, filePath) + if err != nil { + return nil, err } + signals = append(signals, structured...) if securityPolicyPath(filePath) { parsed, err := scanRepositoryFile(tree, filePath, func(reader io.Reader) ([]Signal, error) { @@ -274,6 +271,24 @@ func repositoryFileSignalsFromFS( return signals, nil } +func repositoryStructuredSignals(tree fs.FS, filePath string) ([]Signal, error) { + if sarifFilePath(filePath) { + return scanRepositoryFile(tree, filePath, func(reader io.Reader) ([]Signal, error) { + return sarifSignals(filePath, reader) + }) + } + if !sbomContentPath(filePath) { + return nil, nil + } + return scanRepositoryFile(tree, filePath, func(reader io.Reader) ([]Signal, error) { + signal, ok, err := sbomSignal(filePath, reader) + if !ok { + return nil, err + } + return []Signal{signal}, err + }) +} + func readRepositoryFile(tree fs.FS, filePath string, limit int64) ([]byte, error) { var data []byte err := scanRepositoryFileError(tree, filePath, func(reader io.Reader) error { @@ -657,6 +672,160 @@ func sbomFileName(filePath string) bool { return false } +func sarifFilePath(filePath string) bool { + labels, err := roles.Match(filePath) + if err == nil && (labels.BundledOrDerived() || labels.Has(roles.Test) || labels.Has(roles.Example)) { + return false + } + lower := strings.ToLower(path.Base(filePath)) + return strings.HasSuffix(lower, ".sarif") || strings.HasSuffix(lower, ".sarif.json") +} + +func sarifSignals(filePath string, reader io.Reader) ([]Signal, error) { + inspection, err := sarifsummary.Inspect(reader, sarifsummary.Options{ + MaxBytes: sarifMaxBytes, + MaxUniqueValues: sarifMaxUniqueValues, + MaxTools: sarifMaxTools, + }) + if errors.Is(err, sarifsummary.ErrInvalid) { + return []Signal{{ + Check: checkSARIF, + Kind: "candidate", + Name: "invalid", + Path: filePath, + }}, nil + } + if err != nil && !errors.Is(err, sarifsummary.ErrLimit) { + return nil, err + } + + signals := []Signal{{ + Check: checkSARIF, + Kind: "document", + Name: inspection.Version, + Value: "runs=" + strconv.Itoa(inspection.Runs) + + "; results=" + strconv.Itoa(inspection.Results) + + "; occurrences=" + strconv.Itoa(inspection.Occurrences) + + "; suppressed=" + strconv.Itoa(inspection.SuppressedResults), + Path: filePath, + }} + for _, tool := range inspection.Tools { + value := "" + if tool.SemanticVersion != "" { + value = "semantic-version=" + tool.SemanticVersion + } + if tool.Version != "" { + if value != "" { + value += "; " + } + value += "version=" + tool.Version + } + signals = append(signals, Signal{ + Check: checkSARIF, + Kind: "tool", + Name: tool.Name, + Value: value, + Path: filePath, + }) + } + signals = appendSARIFCounts(signals, filePath, "level", inspection.Levels) + signals = appendSARIFCounts(signals, filePath, "baseline", inspection.BaselineStates) + signals = appendSARIFCounts(signals, filePath, "rule", inspection.Rules) + signals = appendSARIFCounts(signals, filePath, "taxon", inspection.Taxa) + signals = appendSARIFCWE(signals, filePath, inspection.Taxa) + signals = appendSARIFCounts(signals, filePath, "suppression", inspection.Suppressions) + if inspection.Truncated || errors.Is(err, sarifsummary.ErrLimit) { + signals = append(signals, Signal{ + Check: checkSARIF, + Kind: "coverage", + Name: "truncated", + Path: filePath, + }) + } + sortSignals(signals) + return deduplicateSignals(signals), nil +} + +func appendSARIFCWE(signals []Signal, filePath string, taxa map[string]int) []Signal { + type weakness struct { + count int + name string + categoryID string + } + weaknesses := make(map[string]weakness) + categories := make(map[string]int) + categoryNames := make(map[string]string) + for rawID, count := range taxa { + id, entry, ok := cwe.Lookup(rawID) + if !ok { + continue + } + categoryID := cwe.CategoryID(entry.Category) + current := weaknesses[id] + current.count += count + current.name = entry.Name + current.categoryID = categoryID + weaknesses[id] = current + if categoryID != "" { + categories[categoryID] += count + categoryNames[categoryID] = entry.Category + } + } + + ids := make([]string, 0, len(weaknesses)) + for id := range weaknesses { + ids = append(ids, id) + } + sort.Strings(ids) + for _, id := range ids { + item := weaknesses[id] + value := "count=" + strconv.Itoa(item.count) + "; name=" + item.name + if item.categoryID != "" { + value += "; category=" + item.categoryID + } + signals = append(signals, Signal{ + Check: checkSARIF, + Kind: "weakness", + Name: id, + Value: value, + Path: filePath, + }) + } + ids = ids[:0] + for id := range categories { + ids = append(ids, id) + } + sort.Strings(ids) + for _, id := range ids { + signals = append(signals, Signal{ + Check: checkSARIF, + Kind: "weakness-category", + Name: id, + Value: "count=" + strconv.Itoa(categories[id]) + "; name=" + categoryNames[id], + Path: filePath, + }) + } + return signals +} + +func appendSARIFCounts(signals []Signal, filePath, kind string, counts map[string]int) []Signal { + names := make([]string, 0, len(counts)) + for name := range counts { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + signals = append(signals, Signal{ + Check: checkSARIF, + Kind: kind, + Name: name, + Value: strconv.Itoa(counts[name]), + Path: filePath, + }) + } + return signals +} + func securityPolicyPath(filePath string) bool { switch strings.ToLower(filePath) { case "security.md", ".github/security.md", "docs/security.md", @@ -887,7 +1056,7 @@ func (d *localDetectors) runRepositoryFiles( } delete(current.errors, detectorRepositoryFiles) - checks := []string{checkBinaryArtifacts, checkCertificate, checkFuzzing, checkLicense, checkSAST, checkSBOM, checkSecurityPolicy} + checks := []string{checkBinaryArtifacts, checkCertificate, checkFuzzing, checkLicense, checkSARIF, checkSAST, checkSBOM, checkSecurityPolicy} if forceFull { for _, check := range checks { switch check { diff --git a/repository_files_test.go b/repository_files_test.go index 704ca29..490e71b 100644 --- a/repository_files_test.go +++ b/repository_files_test.go @@ -32,6 +32,7 @@ const ( testJSONLFlag = "--jsonl" testLogCommand = "log" testPackageName = "example" + testSARIFPath = "results.sarif" testSecurityPolicyPath = ".github/SECURITY.md" testStatsFlag = "--stats" testWorkersFlag = "--workers" @@ -293,6 +294,39 @@ func TestRepositoryFileSignalsReportsUnrecognizedSBOMCandidate(t *testing.T) { } } +func TestRepositoryFileSignalsSummarizesSARIF(t *testing.T) { + signals := repositorySignals(t, testSARIFPath, []byte(testSARIFLog)) + for _, wanted := range []Signal{ + {Check: checkSARIF, Kind: "document", Name: "2.1.0", Value: "runs=1; results=2; occurrences=3; suppressed=1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "tool", Name: "CodeQL", Value: "semantic-version=2.20.0", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "level", Name: "error", Value: "1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "level", Name: "warning", Value: "1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "rule", Name: "js/sql-injection", Value: "1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "rule", Name: "js/xss", Value: "1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "taxon", Name: "CWE-79", Value: "1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "weakness", Name: "CWE-79", Value: "count=1; name=Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'); category=CWE-1409", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "weakness-category", Name: "CWE-1409", Value: "count=1; name=Injection", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "suppression", Name: "inSource/accepted", Value: "1", Path: testSARIFPath}, + } { + if !containsExactSignal(signals, wanted) { + t.Errorf("signal missing: %#v\ngot: %#v", wanted, signals) + } + } + + invalid := repositorySignals(t, "invalid.sarif.json", []byte(`{"name":"not SARIF"}`)) + if !containsExactSignal(invalid, Signal{ + Check: checkSARIF, + Kind: "candidate", + Name: "invalid", + Path: "invalid.sarif.json", + }) { + t.Fatalf("invalid SARIF signals = %#v", invalid) + } + if signals := repositorySignals(t, "testdata/results.sarif", []byte(testSARIFLog)); len(signals) != 0 { + t.Fatalf("fixture SARIF signals = %#v", signals) + } +} + func TestRepositoryFileSignalsDoesNotTreatSourceNamesAsLegalEvidence(t *testing.T) { for _, filePath := range []string{"notice.go", "licenses/_spdx.py"} { if signals := repositorySignals(t, filePath, []byte("package example\n")); len(signals) != 0 { @@ -473,6 +507,7 @@ func TestLogDetectsRepositoryFileSignals(t *testing.T) { writeFile(t, repo, testLicensePath, mitLicenseText) writeFile(t, repo, "SECURITY.md", "Report issues to security@example.com.\n") writeFile(t, repo, "bom.json", `{"bomFormat":"CycloneDX","specVersion":"1.6","version":1}`) + writeFile(t, repo, testSARIFPath, testSARIFLog) writeFile(t, repo, "tool.wasm", string([]byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00})) paddingLine := "// history padding\n" paddingLines := int(repositoryFileContentBytes)/len(paddingLine) + 1 @@ -489,6 +524,8 @@ func TestLogDetectsRepositoryFileSignals(t *testing.T) { for _, expected := range []string{ "+ Binary-Artifacts/artifact wasm at tool.wasm = application/wasm", "+ License/license MIT at LICENSE", + "+ SARIF/rule js/xss at results.sarif = 1", + "+ SARIF/tool CodeQL at results.sarif = semantic-version=2.20.0", "+ SBOM/document cyclonedx at bom.json:1 = 1.6", "+ Security-Policy/policy at SECURITY.md:1", "+ Security-Policy/email at SECURITY.md:1 = security@example.com", @@ -538,6 +575,31 @@ func TestLogDetectsCertificateChangesAcrossHistory(t *testing.T) { } } +const testSARIFLog = `{ + "version": "2.1.0", + "runs": [{ + "tool": {"driver": { + "name": "CodeQL", + "semanticVersion": "2.20.0", + "rules": [{ + "id": "js/xss", + "defaultConfiguration": {"level": "error"} + }, {"id": "js/sql-injection"}] + }}, + "results": [{ + "ruleId": "js/xss", + "baselineState": "new", + "occurrenceCount": 2, + "message": {"text": "finding"}, + "suppressions": [{"kind": "inSource", "status": "accepted"}], + "taxa": [{"id": "CWE-79"}] + }, { + "ruleIndex": 1, + "message": {"text": "uses default warning level"} + }] + }] +}` + func repositorySignals(t *testing.T, filePath string, data []byte) []Signal { t.Helper() signals, err := repositoryFileSignalsFromFS( From af43f68b750091e16e1dbe443c50a25f1642c486 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 18:51:02 +0000 Subject: [PATCH 2/8] Cover bounded SARIF detection --- README.md | 2 +- go.mod | 2 +- go.sum | 4 ++-- repository_files_test.go | 18 ++++++++++++++++-- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 70a358f..1438166 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ The additional detectors use the same persistent signal model. A threat-surface The root commit and missing history boundaries receive a full scan. Later commits reuse their first parent's signal state and update the paths changed by that commit. Cross-file detectors, including Agent Skills, dependency-update configuration, REUSE metadata, and sensitive ignore policy, receive a full commit tree when one of their inputs changes. Agent Skills rerun when a `SKILL.md` or a file inside a known skill package changes. Sensitive ignore policy reruns only when a project `.gitignore` changes. -Parser results for changed files are cached by Git blob OID and path for the whole run. Repository files that cannot contain a certificate, license, SARIF log, SBOM, fuzzer, policy, or SAST configuration need only a 1 KB prefix for binary classification. SARIF candidates use streaming byte, tool, and distinct-value limits. Commit reading can run concurrently, while signal-state updates remain ordered. `github.com/git-pkgs/languages` keeps a language inventory so Brief can rerun language-gated detection only when needed. `github.com/git-pkgs/roles` removes bundled and derived paths from fuzzing and license detection, and removes test, fixture, example, cache, and build-output paths from certificate detection. Test paths remain eligible for fuzz detection because fuzz targets commonly use them. +Parser results for changed files are cached by Git blob OID and path for the whole run. Repository files that cannot contain a certificate, license, SARIF log, SBOM, fuzzer, policy, or SAST configuration need only a 1 KB prefix for binary classification. SARIF candidates use streaming byte, tool, and distinct-value limits, and bundled, derived, test, and example paths are skipped for SARIF detection. Commit reading can run concurrently, while signal-state updates remain ordered. `github.com/git-pkgs/languages` keeps a language inventory so Brief can rerun language-gated detection only when needed. `github.com/git-pkgs/roles` removes bundled and derived paths from fuzzing and license detection, and removes test, fixture, example, cache, and build-output paths from certificate detection. Test paths remain eligible for fuzz detection because fuzz targets commonly use them. Brief reads commit trees through `history.TreeFS` and reruns only when Brief's changed-file filter identifies an input to one of the emitted tool signals. Package files are parsed when their paths change, and the parse result supplies dependency constraints, pins, Actions references, container images, lockfile resolution, integrity records, and manifest-declared license values or file references. Results are cached by blob OID and path. Shell commands that download or install packages are not yet included in this local package detector. Package-manager detection uses `managers.AffectedDirectories` to rescan only the affected projects. Secret scanning updates changed paths and caches results by blob OID and path because some rules depend on the filename. REUSE metadata changes trigger a full REUSE scan; ordinary changes update the affected file state and coverage count. diff --git a/go.mod b/go.mod index f691941..e9be2bc 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108175116-14294dee0dad + github.com/git-pkgs/sarif v0.1.4-0.20261108184815-c2e53906ee51 github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index 5baca2d..e2b95de 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108175116-14294dee0dad h1:k0EAsiSaSaIG6nNeheAlDqqAkRqYU1z8a8HW8PvJllA= -github.com/git-pkgs/sarif v0.1.4-0.20261108175116-14294dee0dad/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108184815-c2e53906ee51 h1:lApROrCK8egoAelqtt9Sf8G2m71HFPuaSgOU5uWODlk= +github.com/git-pkgs/sarif v0.1.4-0.20261108184815-c2e53906ee51/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= diff --git a/repository_files_test.go b/repository_files_test.go index 490e71b..03120b4 100644 --- a/repository_files_test.go +++ b/repository_files_test.go @@ -29,6 +29,7 @@ const ( testFuzzPath = "fuzz_test.go" testFuzzerKind = "fuzzer" testGoFuzzerName = "GoBuiltInFuzzer" + testDocumentKind = "document" testJSONLFlag = "--jsonl" testLogCommand = "log" testPackageName = "example" @@ -119,7 +120,7 @@ func TestRepositoryFileSignalsUsesGitPkgsParsers(t *testing.T) { }`)) if !containsExactSignal(sbomSignals, Signal{ Check: checkSBOM, - Kind: "document", + Kind: testDocumentKind, Name: "cyclonedx", Value: "1.6", Path: "artifacts/inventory.json", @@ -297,7 +298,7 @@ func TestRepositoryFileSignalsReportsUnrecognizedSBOMCandidate(t *testing.T) { func TestRepositoryFileSignalsSummarizesSARIF(t *testing.T) { signals := repositorySignals(t, testSARIFPath, []byte(testSARIFLog)) for _, wanted := range []Signal{ - {Check: checkSARIF, Kind: "document", Name: "2.1.0", Value: "runs=1; results=2; occurrences=3; suppressed=1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: testDocumentKind, Name: "2.1.0", Value: "runs=1; results=2; occurrences=3; suppressed=1", Path: testSARIFPath}, {Check: checkSARIF, Kind: "tool", Name: "CodeQL", Value: "semantic-version=2.20.0", Path: testSARIFPath}, {Check: checkSARIF, Kind: "level", Name: "error", Value: "1", Path: testSARIFPath}, {Check: checkSARIF, Kind: "level", Name: "warning", Value: "1", Path: testSARIFPath}, @@ -327,6 +328,19 @@ func TestRepositoryFileSignalsSummarizesSARIF(t *testing.T) { } } +func TestRepositoryFileSignalsReportsTruncatedSARIF(t *testing.T) { + data := append([]byte(testSARIFLog), bytes.Repeat([]byte(" "), sarifMaxBytes-len(testSARIFLog)+1)...) + signals := repositorySignals(t, testSARIFPath, data) + for _, wanted := range []Signal{ + {Check: checkSARIF, Kind: testDocumentKind, Name: "2.1.0", Value: "runs=1; results=2; occurrences=3; suppressed=1", Path: testSARIFPath}, + {Check: checkSARIF, Kind: "coverage", Name: "truncated", Path: testSARIFPath}, + } { + if !containsExactSignal(signals, wanted) { + t.Errorf("signal missing: %#v\ngot: %#v", wanted, signals) + } + } +} + func TestRepositoryFileSignalsDoesNotTreatSourceNamesAsLegalEvidence(t *testing.T) { for _, filePath := range []string{"notice.go", "licenses/_spdx.py"} { if signals := repositorySignals(t, filePath, []byte("package example\n")); len(signals) != 0 { From 6d8a44c5874dfe194c030c1129a7c0da1951e04e Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 19:02:26 +0000 Subject: [PATCH 3/8] Update SARIF rule resolution --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e9be2bc..8e98265 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108184815-c2e53906ee51 + github.com/git-pkgs/sarif v0.1.4-0.20261108190120-c865f2716858 github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index e2b95de..0136811 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108184815-c2e53906ee51 h1:lApROrCK8egoAelqtt9Sf8G2m71HFPuaSgOU5uWODlk= -github.com/git-pkgs/sarif v0.1.4-0.20261108184815-c2e53906ee51/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108190120-c865f2716858 h1:MDZdDccYgI9XnSFDC6D3HpipyLqN/kptjfZm+2qAy1M= +github.com/git-pkgs/sarif v0.1.4-0.20261108190120-c865f2716858/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= From c8c5babaea5d53223e9c3f59e269b7b839c63429 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 19:13:07 +0000 Subject: [PATCH 4/8] Use reviewed SARIF summary --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8e98265..e81b740 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108190120-c865f2716858 + github.com/git-pkgs/sarif v0.1.4-0.20261108190644-05983351b29e github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index 0136811..b96be58 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108190120-c865f2716858 h1:MDZdDccYgI9XnSFDC6D3HpipyLqN/kptjfZm+2qAy1M= -github.com/git-pkgs/sarif v0.1.4-0.20261108190120-c865f2716858/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108190644-05983351b29e h1:y80Qjh/cKrnp4nGyBr6lhVivLpM1L+of0mslafOVhXk= +github.com/git-pkgs/sarif v0.1.4-0.20261108190644-05983351b29e/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= From f8baa782fc2767ba5cdf4f17673c37494f90d447 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 19:40:20 +0000 Subject: [PATCH 5/8] Use merged SARIF summary --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e81b740..a216d11 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108190644-05983351b29e + github.com/git-pkgs/sarif v0.1.4-0.20261108193929-cf8065f2f389 github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index b96be58..f66e946 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108190644-05983351b29e h1:y80Qjh/cKrnp4nGyBr6lhVivLpM1L+of0mslafOVhXk= -github.com/git-pkgs/sarif v0.1.4-0.20261108190644-05983351b29e/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108193929-cf8065f2f389 h1:7o2HZUvylHx0HF3hN9EGIe6LtqkNdQ39rikeqEthAEg= +github.com/git-pkgs/sarif v0.1.4-0.20261108193929-cf8065f2f389/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= From d569454fbf7b45b9a5ff2fcaf154977f562b23f9 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 20:45:25 +0000 Subject: [PATCH 6/8] Preserve SARIF taxonomy identity --- go.mod | 2 +- go.sum | 4 ++-- repository_files.go | 48 +++++++++++++++++++++++++++++++++++----- repository_files_test.go | 47 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index a216d11..6525e05 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108193929-cf8065f2f389 + github.com/git-pkgs/sarif v0.1.4-0.20261108204304-5e37a9bfdcdf github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index f66e946..e88e6bd 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108193929-cf8065f2f389 h1:7o2HZUvylHx0HF3hN9EGIe6LtqkNdQ39rikeqEthAEg= -github.com/git-pkgs/sarif v0.1.4-0.20261108193929-cf8065f2f389/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108204304-5e37a9bfdcdf h1:wGpeUnb87oM0BOCU11jsEXYVFgsCuNXOe/zrBzbzE98= +github.com/git-pkgs/sarif v0.1.4-0.20261108204304-5e37a9bfdcdf/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= diff --git a/repository_files.go b/repository_files.go index cfa3d12..89083ac 100644 --- a/repository_files.go +++ b/repository_files.go @@ -731,7 +731,7 @@ func sarifSignals(filePath string, reader io.Reader) ([]Signal, error) { signals = appendSARIFCounts(signals, filePath, "level", inspection.Levels) signals = appendSARIFCounts(signals, filePath, "baseline", inspection.BaselineStates) signals = appendSARIFCounts(signals, filePath, "rule", inspection.Rules) - signals = appendSARIFCounts(signals, filePath, "taxon", inspection.Taxa) + signals = appendSARIFTaxa(signals, filePath, inspection.Taxa) signals = appendSARIFCWE(signals, filePath, inspection.Taxa) signals = appendSARIFCounts(signals, filePath, "suppression", inspection.Suppressions) if inspection.Truncated || errors.Is(err, sarifsummary.ErrLimit) { @@ -746,7 +746,36 @@ func sarifSignals(filePath string, reader io.Reader) ([]Signal, error) { return deduplicateSignals(signals), nil } -func appendSARIFCWE(signals []Signal, filePath string, taxa map[string]int) []Signal { +func appendSARIFTaxa(signals []Signal, filePath string, taxa []sarifsummary.Taxon) []Signal { + for _, taxon := range taxa { + name := taxon.ID + if name == "" { + name = taxon.GUID + } + switch { + case isCWETaxonomy(taxon.TaxonomyName): + if id, _, ok := cwe.Lookup(name); ok { + name = id + } else { + name = "CWE:" + name + } + case taxon.TaxonomyName != "": + name = taxon.TaxonomyName + ":" + name + case taxon.TaxonomyGUID != "": + name = taxon.TaxonomyGUID + ":" + name + } + signals = append(signals, Signal{ + Check: checkSARIF, + Kind: "taxon", + Name: name, + Value: strconv.Itoa(taxon.Count), + Path: filePath, + }) + } + return signals +} + +func appendSARIFCWE(signals []Signal, filePath string, taxa []sarifsummary.Taxon) []Signal { type weakness struct { count int name string @@ -755,19 +784,22 @@ func appendSARIFCWE(signals []Signal, filePath string, taxa map[string]int) []Si weaknesses := make(map[string]weakness) categories := make(map[string]int) categoryNames := make(map[string]string) - for rawID, count := range taxa { - id, entry, ok := cwe.Lookup(rawID) + for _, taxon := range taxa { + if !isCWETaxonomy(taxon.TaxonomyName) { + continue + } + id, entry, ok := cwe.Lookup(taxon.ID) if !ok { continue } categoryID := cwe.CategoryID(entry.Category) current := weaknesses[id] - current.count += count + current.count += taxon.Count current.name = entry.Name current.categoryID = categoryID weaknesses[id] = current if categoryID != "" { - categories[categoryID] += count + categories[categoryID] += taxon.Count categoryNames[categoryID] = entry.Category } } @@ -808,6 +840,10 @@ func appendSARIFCWE(signals []Signal, filePath string, taxa map[string]int) []Si return signals } +func isCWETaxonomy(name string) bool { + return strings.EqualFold(name, "CWE") || strings.EqualFold(name, "Common Weakness Enumeration") +} + func appendSARIFCounts(signals []Signal, filePath, kind string, counts map[string]int) []Signal { names := make([]string, 0, len(counts)) for name := range counts { diff --git a/repository_files_test.go b/repository_files_test.go index 03120b4..043229d 100644 --- a/repository_files_test.go +++ b/repository_files_test.go @@ -600,13 +600,24 @@ const testSARIFLog = `{ "defaultConfiguration": {"level": "error"} }, {"id": "js/sql-injection"}] }}, + "taxonomies": [{ + "name": "CWE", + "guid": "33333333-3333-4333-8333-333333333333", + "taxa": [{"id": "79"}] + }], "results": [{ "ruleId": "js/xss", "baselineState": "new", "occurrenceCount": 2, "message": {"text": "finding"}, "suppressions": [{"kind": "inSource", "status": "accepted"}], - "taxa": [{"id": "CWE-79"}] + "taxa": [{ + "id": "79", + "toolComponent": { + "name": "CWE", + "guid": "33333333-3333-4333-8333-333333333333" + } + }] }, { "ruleIndex": 1, "message": {"text": "uses default warning level"} @@ -614,6 +625,40 @@ const testSARIFLog = `{ }] }` +func TestRepositoryFileSignalsKeepsCustomTaxaOutOfCWE(t *testing.T) { + input := []byte(`{ + "version": "2.1.0", + "runs": [{ + "tool": {"driver": { + "name": "custom-scanner", + "taxa": [{"id": "79"}] + }}, + "results": [{ + "ruleId": "custom-rule", + "message": {"text": "finding"}, + "taxa": [{"index": 0}] + }] + }] +}`) + + signals := repositorySignals(t, testSARIFPath, input) + if !containsExactSignal(signals, Signal{ + Check: checkSARIF, + Kind: "taxon", + Name: "custom-scanner:79", + Value: "1", + Path: testSARIFPath, + }) { + t.Fatalf("custom taxon missing: %#v", signals) + } + for _, signal := range signals { + if signal.Check == checkSARIF && + (signal.Kind == "weakness" || signal.Kind == "weakness-category") { + t.Fatalf("custom taxon classified as CWE: %#v", signal) + } + } +} + func repositorySignals(t *testing.T, filePath string, data []byte) []Signal { t.Helper() signals, err := repositoryFileSignalsFromFS( From ff3a9653bd9b876968c5a2aac11b511e238e6120 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 20:52:18 +0000 Subject: [PATCH 7/8] Use merged SARIF taxonomy summary --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6525e05..e6663a2 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108204304-5e37a9bfdcdf + github.com/git-pkgs/sarif v0.1.4-0.20261108205126-fbd75774be94 github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index e88e6bd..9bf7feb 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108204304-5e37a9bfdcdf h1:wGpeUnb87oM0BOCU11jsEXYVFgsCuNXOe/zrBzbzE98= -github.com/git-pkgs/sarif v0.1.4-0.20261108204304-5e37a9bfdcdf/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108205126-fbd75774be94 h1:oG52xc/TQckt7dg8337bllCB7VKXjqTEJAHZrU1FXGM= +github.com/git-pkgs/sarif v0.1.4-0.20261108205126-fbd75774be94/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= From 460a67913b21f15d897bcb56ee4c037fd462b2a0 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 8 Nov 2026 21:29:14 +0000 Subject: [PATCH 8/8] Use merged SARIF result semantics --- go.mod | 2 +- go.sum | 4 ++-- repository_files.go | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index e6663a2..3eba773 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/git-pkgs/manifests v0.13.1-0.20261107212808-69ba49d733ae github.com/git-pkgs/reuse v0.2.0 github.com/git-pkgs/roles v0.1.3 - github.com/git-pkgs/sarif v0.1.4-0.20261108205126-fbd75774be94 + github.com/git-pkgs/sarif v0.1.4-0.20261108212606-7e62951486f6 github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 github.com/git-pkgs/secrets v0.2.1-0.20261107154440-a977b8886af8 github.com/git-pkgs/skills v0.2.1 diff --git a/go.sum b/go.sum index 9bf7feb..4842e43 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/git-pkgs/reuse v0.2.0 h1:zftBZEVsAsiLAN6kb7CqOHfxz7ykX0+QThHFLtSxDKs= github.com/git-pkgs/reuse v0.2.0/go.mod h1:qYnkGISyubaTfY8/a4qSIoyS8Hmw1urMepveCPKiGYg= github.com/git-pkgs/roles v0.1.3 h1:BkXoBFg0/4szCvBes8mD+UApRe1jITaZ/4bs+XtRxdw= github.com/git-pkgs/roles v0.1.3/go.mod h1:USQTDlh22S7jNyAxU5SwtMfXjbX0lyJpVsGLTccyYis= -github.com/git-pkgs/sarif v0.1.4-0.20261108205126-fbd75774be94 h1:oG52xc/TQckt7dg8337bllCB7VKXjqTEJAHZrU1FXGM= -github.com/git-pkgs/sarif v0.1.4-0.20261108205126-fbd75774be94/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= +github.com/git-pkgs/sarif v0.1.4-0.20261108212606-7e62951486f6 h1:9kW358S2DIq/Lf63r3EF9qrxbU3auD/NAX2TjVnbWfo= +github.com/git-pkgs/sarif v0.1.4-0.20261108212606-7e62951486f6/go.mod h1:dxohVtow0t8jXFaJ1g6Lmn2d/POHxXKUukKPQU0/jgs= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6 h1:2xvJPZQJcYe8eHKStvfNx5TUN0tw3R9LHMdO9UI0nuE= github.com/git-pkgs/sbom v0.1.10-0.20261107160107-38436af679c6/go.mod h1:KkUYj+Q0RQIuINEj35XNmQCv/LA3xJN7VfZbOMQj6Dk= github.com/git-pkgs/scan v0.1.1 h1:q5VsnGgd4wpjt/EZ2YnuIpjUZkj1OWzySv3IEwo8wPk= diff --git a/repository_files.go b/repository_files.go index 89083ac..f70f773 100644 --- a/repository_files.go +++ b/repository_files.go @@ -98,6 +98,7 @@ const ( asn1MaxLengthBytes = 4 bitsPerByte = 8 sarifMaxBytes = 16 << 20 + sarifInvalidName = "invalid" sarifMaxUniqueValues = 5_000 sarifMaxTools = 100 ) @@ -691,7 +692,7 @@ func sarifSignals(filePath string, reader io.Reader) ([]Signal, error) { return []Signal{{ Check: checkSARIF, Kind: "candidate", - Name: "invalid", + Name: sarifInvalidName, Path: filePath, }}, nil }