security scanner agent's SARIF report failed to parse: finding triage stalled
A step-by-step skill for unblocking an agent's security triage when the SARIF report won't parse: validating against the SARIF schema, and normalizing the common dialect deviations (null results, missing driver info). Use when a scanner agent stalls on a parse error instead of findings. Triggers: 'SARIF failed to parse', 'SARIF invalid', 'triage stalled'. Not for: scanners that produce no findings, GitHub code scanning upload rejections handled at the source, or non-SARIF formats.
security scanner agent's SARIF report failed to parse: finding triage stalled
TL;DR
The SARIF is malformed or uses a shape your parser doesn't expect. Validate it against the SARIF schema, normalize the common deviations (null results, missing driver info), and only then feed it to triage.
security scanner agent's SARIF report failed to parse: finding triage stalledUse this when
- An agent's security triage stalls on a parse error instead of findings
- The scanner wrote a file but the parser rejects it
- A scanner version upgrade changed the output shape
- GitHub code scanning or another strict consumer rejects the upload
Not for this skill when
- The scanner ran fine and produced zero findings (that's a scope question)
- The format isn't SARIF at all
- The file is fine but the triage logic itself is wrong
Steps
- Check the file is actually JSON and complete; truncated writes are the top cause:
python3 -c "import json; d=json.load(open('results.sarif')); print(d.get('version'), len(d.get('runs', [])))"Expected: prints something like 2.1.0 1. A JSON decode error means the write was truncated; re-run the scanner.
- Validate against the SARIF JSON schema (install the jsonschema package if needed):
python3 -c "
import json, jsonschema
schema = json.load(open('sarif-schema.json'))
doc = json.load(open('results.sarif'))
jsonschema.validate(doc, schema)
print('valid')
"Expected: prints "valid", or a precise error path like runs[0].results telling you what's wrong.
- Normalize the deviations parsers choke on: null results become an empty list, a missing tool driver name gets a fallback:
python3 -c "
import json
d = json.load(open('results.sarif'))
for run in d.get('runs', []):
if run.get('results') is None:
run['results'] = []
drv = run.setdefault('tool', {}).setdefault('driver', {})
drv.setdefault('name', 'unknown-scanner')
json.dump(d, open('results.fixed.sarif', 'w'), indent=2)
print('normalized')
"Expected: prints "normalized", and your triage parser now reads the file.
- Count findings per rule to confirm the triage input is sane:
python3 -c "
import json
from collections import Counter
d = json.load(open('results.fixed.sarif'))
c = Counter(r.get('ruleId', 'no-rule') for run in d['runs'] for r in run['results'])
print(len(c), 'distinct rules')
print(c.most_common(5))
"Expected: a plausible rule count, not zero and not millions.
Variant: GitHub code scanning rejects the upload
It enforces the schema strictly. Fix at the source rather than normalizing after the fact; normalized files are for your tooling, the upload wants the real thing.
Variant: SARIF from a custom tool missing the version field
Add "version": "2.1.0" and the schema pointer. Strict parsers require both.
Variant: huge SARIF files
Stream-parse instead of loading it all into memory. The structure is the same; the reading strategy changes.
Why this happens
SARIF is a big spec and every scanner emits a slightly different dialect: null vs empty arrays, optional fields omitted, extensions in odd places. Strict parsers reject what lenient ones accept, so the agent's triage stalls on a parse error instead of a security question.
Edge cases and pitfalls
- Don't silently drop results during normalization; log what you changed.
- If the scanner version changed recently, diff its output against the last good one before blaming your parser.
- Keep the original file; normalized copies are for tooling, the original is the audit record.
- An empty results list is valid SARIF; don't treat "zero findings" as a parse failure.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_WDL5XJBQUbcgOsSri07HVg