Dependency Triage
Overview
Reads upstream scan findings, evaluates each in the repo's actual usage
context, and emits the same findings augmented with triage_outcome
(confirmed | false_positive) and a one-sentence triage_reason.
This is the triage step — does not run scanners itself. Always
downstream of /dep-scan (or another scan agent emitting the same
finding shape: cve, package, installed_version, fixed_version).
When to use
- After
/dep-scanin a chained agent (needs: [scan-deps]). - Whenever raw scanner output needs filtering before human review.
- Periodically (e.g., scheduled re-triage to update prior-run outcomes as the codebase evolves).
Required tools
Read,Grep,Glob— to inspect repo source for package usageBash(git diff:*)— to scope which dependencies the PR touchedSkill— invocation onlyWrite— auto-granted; forstage-output.json
Inputs
upstream/scan-deps/stage-output.json— scan findings from the upstreamscan-depsagent (or compatible scanner)
Triage rubric
For each finding under payload.findings[]:
Confirmed (triage_outcome: confirmed)
Mark confirmed if all three hold:
- The package is imported by repo source under
apps/,libs/, orpackages/. - The vulnerable code path is plausibly reachable from a request, user input, or build output.
- The package is NOT exclusively a dev-time dependency.
False positive (triage_outcome: false_positive)
Mark FP if any one of these holds:
- Not a runtime dep. Listed only under
devDependencies/peerDependenciesand not exposed at build/runtime (test fixtures, type generators, bundler internals). - Vulnerable function unused. The specific CVE-affected
function/API is not called anywhere in
apps/,libs/, orpackages/. - Network/host context excludes risk. E.g., DoS vuln on internal service the project doesn't expose; SSRF on URL the project never constructs; XSS in a sanitizer the project doesn't reach.
- Already mitigated transitively. Repo's package overrides / resolutions pin a safe version.
- Out-of-scope target. Scanner flagged a vendored binary, build artifact, or test fixture not part of the deployed application.
Conservative tie-break
If you can't establish FP via one of the rules above, mark
confirmed. Better one extra reviewer click than missing a real CVE.
Capture uncertainty in triage_reason so the reviewer can re-evaluate
quickly.
Process
Read upstream output.
upstream/scan-deps/stage-output.json— parsepayload.findings[].For each finding, follow the rubric:
- Use
Grepto find imports ofpackageinapps/,libs/,packages/. - If imports exist,
Readthe importing files to check whether the vulnerable API is actually used. - Inspect
package.jsonfiles to determinedependenciesvsdevDependencies(path matters — rootpackage.jsonvs workspacepackage.jsons differ in semantics). - Consider transitive resolutions: check
pnpm-lock.yaml/package-lock.jsonfor pinned-safe versions.
- Use
Augment each finding with two new fields:
triage_outcome:"confirmed"or"false_positive"triage_reason: one short sentence explaining the call. Reference the rule number (1-5) for FPs.
Recompute summary. Update the top-level
summaryfield to reflect post-triage counts:"trivy: <N> raw → <C> confirmed, <F> false_positive".
Output
Same top-level shape as the upstream scan, with two augmentations:
{
"stage": "triage-deps",
"status": "<see below>",
"summary": "trivy: <N> raw → <C> confirmed, <F> false positive",
"payload": {
"findings": [
{
"severity": "high",
"file": "package-lock.json",
"message": "<CVE>: ... (unchanged from scan)",
"suggested_fix": "...",
"cve": "...",
"package": "lodash",
"installed_version": "4.17.20",
"fixed_version": "4.17.21",
"triage_outcome": "confirmed",
"triage_reason": "Imported by libs/foo and the vulnerable .pickBy() is called in src/utils/groupBy.ts."
},
{
"severity": "high",
"file": "package-lock.json",
"message": "<CVE>: ...",
...,
"triage_outcome": "false_positive",
"triage_reason": "FP rule 2: vulnerable .template() function is not called anywhere under apps/ or libs/."
}
],
"triage_summary": {
"raw": <N>,
"confirmed": <C>,
"false_positive": <F>
}
}
}
Status
passed— all findings false_positive (no confirmed real risks)passed_with_findings— confirmed findings exist but only atinfo/low/mediumseverityfailed— any confirmed finding athighorcriticalseverity
Severity discipline
Do not downgrade severity. A confirmed-but-low-likelihood finding keeps its original severity (high/critical); the human reviewer decides whether to accept the risk. Triage filters the noise; it does not soften the signal.
Heuristics
- Grep first, read second. Don't blindly Read every file in
libs/. A focusedGrepforfrom '<package>'orrequire('<package>')scopes the work. - Multiple CVEs per package are independent. Same package can have one confirmed CVE (vulnerable function called) and one FP CVE (different function unused). Evaluate each on its own.
- Capture audit context in
triage_reason. A future scheduled re-triage can diff outcomes only if the reasoning is preserved. - Don't fabricate file paths. If you can't find an import of the affected package in the repo, that's FP rule 1 (not a runtime dep) or rule 5 (out-of-scope target). Mark it accordingly, don't invent evidence.
Output preservation vs sticky-comment filtering
Emit all findings (confirmed and FP) under payload.findings[] for
the artifact — the artifact is the audit trail. The renderer (which
posts the sticky PR comment) can filter to confirmed-only for the
human view; the artifact retains the full record.