Skip to content

Feeding findings into cxg

An @exposes annotation is a hypothesis: this asset, this threat, this code location, nobody has shown it holds. cxg is what turns a hypothesis into a confirmed or refuted finding by attacking a running instance.

The interface between them is one file. guardlink writes SARIF; cxg pentest reads it. This page covers the guardlink half: what the export contains, what makes a finding useful downstream, and what never crosses.

Terminal window
mkdir -p whitebox
npx guardlink sarif . -o whitebox/findings.sarif
✓ Wrote SARIF to whitebox/findings.sarif
SARIF: 1 result(s) — 1 error(s), 0 warning(s)

cxg pentest run --codebase <dir> loads <dir>/whitebox/findings.sarif. The path is fixed, so the filename and directory above are not a convention you can change. See cxg pentest.

The -o flag does not create the directory. Without the mkdir -p, guardlink exits 1 with a raw Node ENOENT stack trace rather than a message.

Without -o, SARIF goes to stdout, which is what you want in a pipeline. The SARIF: 1 result(s) line goes to stderr either way.

SARIF 2.1.0, one run, five rules:

Rule Level From
guardlink/unmitigated-exposure warning @exposes with no matching @mitigates or @accepts
guardlink/unmitigated-critical error the same, at critical or high
guardlink/confirmed-exploitable error @confirmed
guardlink/parse-error error a malformed annotation
guardlink/dangling-ref warning a #id that resolves to nothing

A single result:

{
"ruleId": "guardlink/unmitigated-critical",
"level": "error",
"message": {
"text": "#users is exposed to sqli: email is interpolated into the SQL string"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": { "uri": "src/users.js" },
"region": { "startLine": 3 }
}
}
],
"partialFingerprints": {
"guardlink/threatId": "gl-5cc827267186"
},
"properties": {
"threatId": "gl-5cc827267186",
"severity": "critical",
"asset": "#users",
"threat": "#sqli",
"codegraph_reachability": {
"http_method": "GET",
"http_path": "/api/users"
}
}
}

Three fields carry the weight.

threatId, the identity that survives the round trip

Section titled “threatId, the identity that survives the round trip”

gl- plus twelve hex characters, derived from the normalised asset, the normalised threat, and the file. Not the line, and not the message text.

That means it is stable across re-runs and across code movement, and it is the same id whether the annotation is an @exposes or the @confirmed that later proves it. That is deliberate, because those are the same threat at the same place at two points in its life. Downstream tools carry the string and compare it; they never recompute it. guardlink is the sole authority for the algorithm.

It is emitted twice: in partialFingerprints, which is the SARIF-native mechanism any SARIF consumer understands, and in properties, for consumers without a SARIF library.

The accepted limitation: two genuinely distinct weaknesses at an identical (asset, threat, file) collapse to one id.

codegraph_reachability, where to point the traffic

Section titled “codegraph_reachability, where to point the traffic”

A source-code location does not tell an HTTP scanner what to request. This does, and it comes from your @flows annotations.

guardlink builds it by looking for a flow whose mechanism is a route, written METHOD./path:

@flows #api -> #users via GET./api/users

That produced the codegraph_reachability block above. The method must be one of GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS; the path must start with /. A query string is stripped, and so is anything in parentheses.

The route is matched to a finding by the flow’s own source file first, then by the flow’s target asset. So a @flows written in the handler file wins over one written elsewhere, which is what you want when an asset fronts several routes.

No route annotation, no reachability field. The finding still exports, and a downstream tool has nothing but a file path to work from. If you want your annotations tested against a running service, annotating the routes is the highest-value thing you can do, higher than adding more exposures.

The path is emitted verbatim, with no base path assumed. If your service is mounted under /v2, write /v2/api/users.

critical and high are what make guardlink emit unmitigated-critical at error level rather than unmitigated-exposure at warning. cxg ranks the hypotheses it loads against the operator’s stated goal, so severity is an input to that ranking rather than the whole of it.

Severity on an @exposes is optional; when omitted it inherits from the @threat declaration. Written as:

@exposes #users to #sqli -- "no severity written here"

against @threat SQL_Injection (#sqli) [critical], the exported result is guardlink/unmitigated-critical at error, with "severity": "critical". Leaving it unset in both places is how a real finding ends up carrying nothing to rank on.

Filter before exporting if you only want the top of the list:

Terminal window
npx guardlink sarif . --min-severity high -o whitebox/findings.sarif

@mitigates and @accepts remove the exposure entirely. They do not annotate it, flag it, or downgrade it. A covered (asset, threat) pair is absent from the export, so it never becomes a finding and never gets probed. That is the intended behaviour, and it is also the failure mode to keep in mind: a mitigation you wrote and have not verified silently removes the thing that would have verified it.

guardlink sarif writes the same file whether or not the control it names still exists. Deleting an @exposes and deleting a real risk look identical from downstream.

@entitles crosses in neither direction, by design. An entitlement records that an actor is legitimately allowed a capability. SARIF for a model with entitlements is byte-identical to one without: the exposure is exported the same, probed the same, and a reader of the file cannot tell an entitlement exists. Only the downstream recommendation changes.

The reasoning is worth stating, because it is the opposite of how a suppression usually works: a suppression that also prevents verification is how a threat model becomes confidently wrong. An over-granted entitlement should be able to change how a finding is written up. It should never be able to stop the finding being tested.

The coverage block does not appear in SARIF at all. No field in the export carries it.

Four things, in the order they cost you the most.

  1. Annotate the routes. Without codegraph_reachability, a finding names a file and nothing an HTTP client can act on.
  2. Set severity. It decides ordering and rule level.
  3. Make sure the exposures you care about are still exposures. Anything mitigated or accepted is gone from the file.
  4. Check the file count. SARIF: N result(s) on stderr, against what guardlink status . reports as unmitigated. A number lower than you expect usually means a @mitigates you forgot about, or --min-severity.