A SAST report is a structured list of claims: each finding asserts that data can flow from a source to a dangerous sink along a specific path in your code, tagged with a rule, a severity, and a trace. Reading one well means checking the trace, not trusting the label — the severity tells you what class of bug this would be if the claim holds; the trace tells you whether it holds. Teams that internalize that distinction triage in minutes per finding. Teams that don't either fix phantom bugs or, worse, learn to ignore the whole report.
What is actually inside a SAST report?
Strip away the tool branding and every SAST report contains the same anatomy per finding:
- Rule identifier — which pattern fired, usually mapped to a CWE (say, CWE-89 for SQL injection). The CWE mapping is what lets you compare findings across tools.
- Location — file, line, and the sink expression that triggered the match.
- Severity — the tool's estimate of impact if the finding is real. This is an upper bound, not a verdict.
- Confidence — the tool's estimate that the finding is real at all. Severity and confidence are independent axes; a critical-severity, low-confidence finding is a question, not an emergency.
- Data-flow trace — the ordered list of code steps from source to sink. This is the evidence, and it is the part worth reading first.
- Fingerprint — a stable hash of the finding, used to recognize it across scans so a line-number shift doesn't resurrect triaged items.
Most tools export this in SARIF, the standard JSON format that lets IDEs, pipelines, and dashboards render the same findings consistently.
How do you read a data-flow trace?
A trace reads as a chain: source, propagation steps, and sink. Consider a typical finding in an Express handler:
// Step 1 (source): untrusted input enters
const name = req.query.name;
// Step 2 (propagation): flows through a helper
const clause = buildFilter(name);
// Step 3 (sink): reaches a query API as a string
db.raw(`SELECT * FROM users WHERE ${clause}`);
You are auditing three questions. Is the source genuinely attacker-controlled — req.query is; a config file usually is not. Does anything along the path neutralize the value — a parameterized query, an allowlist check, a proper escaping call the tool didn't recognize? And does the sink actually interpret the value dangerously in context? If all three answers hold, the finding is a true positive regardless of what the confidence field says. If a sanitizer sits on the path unrecognized, you have found a false positive and, more usefully, a tuning opportunity.
How should you triage SAST findings?
Order the queue by three factors, in this sequence: trace validity (is it real?), exposure (does the code path face untrusted users, or is it an internal admin tool behind SSO?), and asset value (what does the sink touch — payments data or a debug log?). Then assign every finding exactly one state:
- True positive — fix, with an owner and a severity-based deadline.
- True positive — accepted risk, with a written justification and an expiry date. Risk acceptances without expiry are how backlogs rot.
- False positive, with the reason recorded — "sanitized at line 84" — so the suppression is auditable and reversible.
- Not applicable — test code, dead code, generated code that scanners should be configured to skip.
The single highest-leverage move for an existing codebase: baseline the historical backlog and gate only on new findings. A team that keeps the new-finding queue at zero while burning down the baseline deliberately is in a defensible position; a team facing one undifferentiated 4,000-item list is not.
Why do SAST reports contain false positives?
Because static analysis over-approximates by design. The engine cannot execute your code, so when it cannot prove a path is impossible, it reports the path. The usual culprits: custom sanitizers the tool doesn't recognize, framework indirection the analyzer cannot follow, and validation living far from the flow it protects. The fix is configuration, not resignation — register your sanitizer functions with the tool, write custom rules for house frameworks, and suppress with recorded reasons rather than blanket ignores. Every hour spent tuning repays itself on every subsequent scan; every silent bulk-dismissal teaches the team the report is noise.
How do you turn a SAST report into engineering work?
A report becomes work when findings are routed, not broadcast. Deduplicate by fingerprint so re-scans do not multiply tickets. Route each finding to the owning team via code ownership mappings, inside the tools engineers already watch. Attach remediation guidance at the finding level — the difference between "CWE-89" and "replace string interpolation with a parameterized query, example below" is the difference between a week and an hour. Enforce severity-based SLAs, and scan pull request diffs so new findings arrive while the author still has context — the cheapest possible fix window. Platforms that pair SAST with DAST also let you correlate a static claim with dynamic confirmation, which cuts triage argument time dramatically. For a deeper treatment of prioritization at scale, see our guides on the Safeguard blog.
FAQ
What severity should block a merge?
New critical and high findings with high confidence, on changed code only. Blocking on the whole historical backlog or on medium-and-below noise is the fastest way to get the gate disabled.
What is SARIF?
Static Analysis Results Interchange Format — the OASIS-standard JSON schema for static analysis output. It is what lets GitHub, IDEs, and security dashboards display findings from different engines in one view, and it is the format to standardize on for aggregation.
How is a SAST report different from a DAST report?
A SAST report claims a vulnerable code path exists, with file-and-line evidence but no proof of exploitability. A DAST report demonstrates behavior against the running application — proof it is reachable, but little help locating the code. The formats reflect this: traces versus HTTP request/response pairs. Used together, one localizes and the other confirms.
What false-positive rate is normal?
It varies too much with codebase and tuning for an honest single number — from single digits on a well-tuned deployment to a large majority on an untuned one against a framework-heavy codebase. Track your own rate per rule and disable or fix rules that consistently waste triage time.