Security finding triage
Decide whether a static analysis finding is reachable from user input, and how bad it is in context.
Decide whether a static analysis finding is reachable from user input, and how bad it is in context. Pattern: confidence_routing. Model: jev-1.13.0. Outage rule: review.
When to use it
- A code scanner reports more findings than your team can read, and most are false positives in practice.
- A reviewer can tell from the finding, the code around it and where its input comes from whether it matters.
When not to use it
- You need a fix written. That is text generation; keep an LLM or a person for it.
- Deciding reachability needs a whole-program trace across many files. Run a taint analysis tool, then use this set on its output.
- The rule is exact, such as closing every finding in test files. Do that in code before the call.
Questions
| Id | Type | Asks | Answers | Gating |
|---|---|---|---|---|
reachable_from_user_input | noul | Can data an outside user controls reach the flagged code in finding (rule finding.rule in finding.file), judged from the code and data flow in finding.context? | true or false | yes |
severity_in_context | score | If an attacker could trigger finding, how much harm could it cause in this codebase, judged from finding.context and finding.service_exposure? | 4 levels | no |
Reading the result
- Auto-close only when
overallActionisautoandrouteisauto_close. That happens only for a high band "not reachable". Everything else goes to a person. severity_in_contextdoes not gate. Use its value to order the review queue.- Put the code around the finding and where its inputs come from in
finding.context. Without them the set cannot judge reachability.
Spec
Save it as bandwise/sets/security-finding-triage.json and edit it for your data.
{
"schemaVersion": 1,
"model": "jev-1.13.0",
"input": {
"schema": {
"type": "object",
"required": [
"finding"
],
"properties": {
"finding": {
"type": "object",
"required": [
"rule",
"message",
"file",
"context"
],
"properties": {
"rule": {
"type": "string"
},
"message": {
"type": "string"
},
"file": {
"type": "string"
},
"context": {
"type": "string",
"maxLength": 12000
},
"service_exposure": {
"type": "string",
"enum": [
"internet",
"internal",
"offline"
]
}
}
}
}
}
},
"stages": [
{
"id": "triage",
"questions": {
"reachable_from_user_input": {
"type": "noul",
"instructions": "Can data an outside user controls reach the flagged code in `finding` (rule `finding.rule` in `finding.file`), judged from the code and data flow in `finding.context`?",
"criteria": {
"true": "User-controlled data such as a request body, query, header, upload or message can flow to the flagged line.",
"false": "Only constants, trusted configuration, or internal values the user cannot influence reach the flagged line."
},
"meta": {
"label": "Reachable from user input"
}
},
"severity_in_context": {
"type": "score",
"instructions": "If an attacker could trigger `finding`, how much harm could it cause in this codebase, judged from `finding.context` and `finding.service_exposure`?",
"criteria": [
"Informational. No realistic harm.",
"Low. Limited information exposure or a nuisance.",
"High. Access to other users' data, or a way to disrupt the service.",
"Critical. Remote code execution, full data access, or account takeover."
],
"meta": {
"label": "Severity in context"
}
}
}
}
],
"policies": {
"reachable_from_user_input": {
"type": "noul",
"gating": true,
"noul": {
"trueAt": 0.8,
"falseAt": 0.1,
"reviewMargin": 0.05
},
"actions": {
"high": {
"kind": "auto"
},
"medium": {
"kind": "review"
},
"low": {
"kind": "review"
}
}
},
"severity_in_context": {
"type": "score",
"gating": false,
"thresholds": {
"high": 0.6,
"medium": 0.35
},
"actions": {
"high": {
"kind": "auto"
},
"medium": {
"kind": "auto"
},
"low": {
"kind": "auto"
}
}
}
},
"routes": [
{
"when": {
"all": [
{
"q": "reachable_from_user_input",
"eq": false
},
{
"q": "reachable_from_user_input",
"band": "high"
}
]
},
"output": "auto_close"
}
],
"defaultRoute": "review",
"savings": {
"comparatorModel": "claude-haiku-4-5",
"estOutputTokensPerQuestion": 60,
"kind": "decision"
},
"onUnavailable": "review"
}Example states
The expected outcome is what a person would decide. It is not a recorded model answer.
SQL built from a request parameter
Expected: review: user input reaches the query.
{
"finding": {
"rule": "sql-injection",
"message": "Query built with string concatenation.",
"file": "src/routes/search.ts",
"context": "router.get('/search', (req, res) => {\n const q = req.query.q;\n db.query(\"SELECT * FROM items WHERE name LIKE '%\" + q + \"%'\");\n});",
"service_exposure": "internet"
}
}Shell call with a constant
Expected: auto_close: only a constant reaches the call.
{
"finding": {
"rule": "command-injection",
"message": "Call to exec with a non-literal argument.",
"file": "scripts/backup.ts",
"context": "const BACKUP_CMD = 'pg_dump --format=custom app';\nexec(BACKUP_CMD);",
"service_exposure": "offline"
}
}Borderline cases
One case near the line for each question. Use them to test your wording before you trust the thresholds.
reachable_from_user_input
The value comes from a config file that admins can edit through the product's settings page.
{
"finding": {
"rule": "path-traversal",
"message": "File path built from a variable.",
"file": "src/export/write.ts",
"context": "const dir = settings.get('export_dir'); // editable by org admins in Settings\nfs.writeFileSync(path.join(dir, name), data);",
"service_exposure": "internet"
}
}severity_in_context
An open redirect on a login page: low on its own, higher when used for phishing.
{
"finding": {
"rule": "open-redirect",
"message": "Redirect to a URL taken from the request.",
"file": "src/routes/login.ts",
"context": "res.redirect(req.query.next || '/');",
"service_exposure": "internet"
}
}Try it
Save an example state as state.json, then run the spec locally. Local mode makes no network call and needs no key; answers are synthetic unless a recorded fixture matches.
pnpm bandwise run --local bandwise/sets/security-finding-triage.json state.jsonBandwise is an independent product built on TypeSafe's System One models. It is not TypeSafe's documentation. For the System One models themselves, see docs.typesafe.ai.