Skip to content

feat(sarif): carry each finding's declared context, additively; rewrite SPEC §6 - #49

Merged
Animesh-Sri-bugb merged 4 commits into
mainfrom
fm/fx-guardlink-sarif
Oct 1, 2026
Merged

Animesh-Sri-bugb merged 4 commits into
mainfrom
fm/fx-guardlink-sarif

Conversation

@Animesh-Sri-bugb

@Animesh-Sri-bugb Animesh-Sri-bugb commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What changed

The SARIF export now carries the model around each finding, without changing any existing result. Each @exposes and @confirmed result also carries these SARIF members:

  • locations[0].logicalLocations: the handler the claim is attached to. properties["guardlink/anchor"] holds the anchor span. The region stays on the annotation line.
  • taxa: the claim's cwe: and owasp: refs, with run.taxonomies. properties.externalRefs is kept.
  • relatedLocations: the @boundary, @assumes, @handles and @audit annotations on the claim's asset, plus @transfers of the claim's threat. Each one is sited and carries its claim key.
  • codeFlows: the declared @flows chain into the claim. Each hop is at its own line, and a hop that crosses a boundary has kinds: ["flow","boundary-crossing"]. A route channel adds webRequest.

The run also gains:

  • graphs[0]: every @flows and @boundary as an edge with its claim key. Each boundary lists the flows that cross it. When exactly one side of a boundary is undeclared, the edge says which side is outer.
  • automationDetails
  • versionControlProvenance, only when the repository has a web-hosted origin. It is rebuilt from host and path, so credentials in the remote never reach the export.
  • properties.sarif_profile_version: 1

Rules gain help, tags and a GitHub security-severity band: confirmed 9.0, critical/high 8.9, other exposures 5.0. Diagnostics are not tagged as security.

A chain is never guessed. @flows hops are not linked by call site, so codeFlows is emitted only when the chain's last hop is declared on the claim's own handler or file. guardlink/flowAttribution says which. Upstream hops that are joined only because one hop's target is the next hop's source are marked guardlink/hopAttribution: "graph".

Two front-end defects:

  • MCP guardlink_sarif passed [] for dangling refs. It now computes them as the CLI does, so a model gives one SARIF whichever front end exports it.
  • --min-severity has only ever filtered unmitigated exposures. It is now documented and tested as never dropping a @confirmed result, because a reproduced exploit is the strongest evidence the export carries.

SPEC §6 rewrite. §6 promised the following, which the exporter never produced:

  • results for @accepts, @assumes, @audit, @mitigates and @handles
  • low severity mapped to note
  • a cwe result property
  • TS200x rule ids

§6.1–§6.4 now describe what is exported, and say what each other annotation becomes and why it is not a result. §6.5 is unchanged. New §6.6 covers the declared context and how chains are attributed. New §6.7 covers the run envelope and the compatibility invariants.

What did not change, and how that is proven

No result is added, dropped or reordered, and no rule id, message.text, fingerprint or existing property changes. No new member is derived from @entitles or @actor.

  • tests/sarif-enrichment.test.ts deletes every new member. It then compares results and tool byte for byte with exports cut before this change (tests/fixtures/sarif-baseline/, written by guardlink sarif at the base commit). It does this on two fixtures: expense-api and the new sarif-shop. sarif-shop adds route channels, a @confirmed, OWASP refs, a boundary between two declared assets, a transfer, a covered exposure, a parse error and a dangling ref.
  • Every export is validated against the official OASIS SARIF 2.1.0 schema (vendored, unmodified) with ajv. The exports checked are both fixtures, this repository's own export, an export with provenance and a partial model. ajv, ajv-draft-04 and ajv-formats are new devDependencies, pinned to the ajv version already in the lockfile.
  • tests/actor-entitlement.test.ts gains a case that compares results, tool, graphs and taxonomies with and without entitlements and actors. The check is byte-identical, on a model where every new member is present.

Things a reviewer should know

  • automationDetails.id is guardlink/threat-model/. A GitHub upload with no explicit category will now file GuardLink alerts under that category. Setting category on the upload overrides it.
  • No uriBaseId. Adding it would change the existing location objects, so artifact URIs stay relative as before.
  • No kind on logicalLocations. The anchor knows the scope of the code, not whether it is a function, method or class.
  • Chain enumeration is bounded. It walks breadth-first, capped by depth, by chains collected and by a step budget, and emits at most 3 chains per result. A dense-graph test pins that it terminates and still finds the short entry chain. Depth-first enumeration lost that chain inside a 12-node clique.
  • The threat graph count moved. Reading the git remote gives Workspace.Metadata (#report-metadata) its first exposure, with a mitigation. That adds one node to this repository's whole-model threat graph. I re-measured it in Chrome as tests/query-views.test.ts requires: mermaid@11 under the dashboard's options at a confirmed 1440x900 viewport gave filtered {34 nodes, 88 links} (unchanged) and whole model {48 nodes, 179 links}. The counter agrees.
  • Out of scope here: a pentest profile, suppressions, boundary-claim results or any other new result kind. Each would change the result index consumers key on, so it belongs behind an opt-in profile that appends results.

Local evidence (GitHub Actions is unavailable for this repository right now)

npm run build && npx vitest run
 Test Files  112 passed (112)
      Tests  2007 passed | 1 skipped (2008)
npm run lint                         # clean
npx tsc --noEmit -p .                # clean
guardlink validate . --artifacts     # ✓ Artifacts are current. ✓ Artifacts are drawable.
guardlink validate .                 # passed, 16 unmitigated exposures (unchanged)
guardlink diff HEAD~4                # +2 exposures, each with its mitigation; "1 NEW unmitigated — risk unchanged"
                                     # is the existing #sarif exposure, moved from line 34 to 42 by doc lines

Measured on the exports:

Export results with codeFlows flowAttribution with relatedLocations graph nodes / edges taxonomies
expense-api 11 9 handler 8, file 1 11 10 / 24 CWE 7
sarif-shop 9 5 handler 3, file 2 7 6 / 11 CWE 4, OWASP 2
this repository 16 16 file 16 16 77 / 202 CWE 5

Each @exposes and @confirmed result now also carries, as SARIF members
existing consumers ignore: logicalLocations and the anchor of the code it
is attached to; taxa for its cwe:/owasp: refs, with run.taxonomies;
relatedLocations for the @boundary, @assumes, @Handles, @Transfers and
@Audit annotations on its asset; and codeFlows with the declared @flows
chain into it. The run gains graphs[0] (every @flows and @boundary, with
claim keys, crossings and the inferable outer side), automationDetails,
versionControlProvenance when the repository has a web-hosted origin, and
properties.sarif_profile_version. Rules gain help, tags and a GitHub
security-severity band.

No result is added, dropped or reordered, and no rule id, message.text,
fingerprint or existing property changes. tests/sarif-enrichment.test.ts
strips every new member and compares the rest byte for byte with exports
cut before this change (tests/fixtures/sarif-baseline), and validates the
output against the official SARIF 2.1.0 schema. A chain is emitted only
when its last hop is declared on the claim's own handler or file; nothing
is derived from @entitles or @Actor, which actor-entitlement.test.ts now
also pins for the new members.

Also fixes two front-end defects: the MCP guardlink_sarif tool now
computes dangling refs as the CLI does, and --min-severity is documented
and tested as filtering exposures only, never a @confirmed result.

The provenance read gives Workspace.Metadata its first exposure (a git
remote can embed credentials; only host and path are kept), which adds a
node to this repository's whole-model threat graph: re-measured in Chrome
with mermaid@11 at 1440x900 as 48 nodes and 179 links, matching the
counter.
§6.1-§6.4 promised results for @accepts, @assumes, @Audit, @mitigates and
@Handles, low severity as note, a cwe result property and TS200x rule ids;
the exporter has produced none of them. They now state the five rules, the
result order and levels, what every other annotation becomes and why it is
not a result, the rule help and security-severity bands, and the taxa
mapping, with an example cut from the sarif-shop fixture. §6.5 is
unchanged. New §6.6 describes the declared context and how a chain is
attributed; new §6.7 the run envelope and the compatibility invariants.
@Animesh-Sri-bugb
Animesh-Sri-bugb merged commit 5366e89 into main Oct 1, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant