Skip to content

Commit 7de13ac

Browse files
committed
Confirm a finding by code and scan both, and label its evidence as proof
When a reachable code sink of the matching kind sits on the same route as a dynamic finding, the finding is raised to confirmed: the code does the dangerous thing and the scan reached it, which is the strongest evidence there is. A heuristic SQL injection on a route whose handler builds a query by string joining, both reachable, now reads as confirmed and lands in the top risk cell instead of the middle. The report labels a confirmed finding's evidence as Proof rather than Evidence, so a reader can tell what is shown to be real from what is reported but not re-proven. Deterministic, no AI.
1 parent c829ea8 commit 7de13ac

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

‎app/core/dast_report.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,6 @@ def build(db, asset, scan, user=None, organization: str = "") -> dict:
333333
from app.core import dast_risk
334334

335335
dast_risk.annotate(findings, getattr(asset, "exposure", "") or "")
336-
risk_matrix = dast_risk.matrix(findings)
337336
# Code sinks from the mapped repositories, split by whether the scan reached
338337
# the route they sit behind. Reachable ones are leads to prove; the rest
339338
# are context to review, not dynamic bugs.
@@ -343,6 +342,9 @@ def build(db, asset, scan, user=None, organization: str = "") -> dict:
343342
code_sinks = []
344343
reachable_sinks = [s for s in code_sinks if s.get("reachable")]
345344
context_sinks = [s for s in code_sinks if not s.get("reachable")]
345+
# A finding a reachable sink corroborates is confirmed by code and scan both.
346+
dast_risk.corroborate(findings, reachable_sinks)
347+
risk_matrix = dast_risk.matrix(findings)
346348
from app.core.external_decisions import counted as _counted
347349

348350
by_sev = {s: sum(1 for f in findings if f.get("severity") == s and _counted(f))

‎app/core/dast_risk.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from __future__ import annotations
1414

1515
import re
16+
from urllib.parse import urlparse
1617

1718
LIKELIHOOD_LABEL = {1: "Rare", 2: "Unlikely", 3: "Possible", 4: "Likely",
1819
5: "Confirmed"}
@@ -97,6 +98,44 @@ def cell(lk: int, br: int) -> str:
9798
return _TABLE.get(max(1, min(5, lk)), {}).get(max(1, min(5, br)), "low")
9899

99100

101+
# A code sink type and the dynamic finding it would corroborate.
102+
_SINK_FINDING = {
103+
"sql-injection": re.compile(r"sql\s*inj|\bsqli\b|CWE-?89\b", re.I),
104+
"command-injection": re.compile(r"command inj|\brce\b|code exec|os command|CWE-?(?:78|94|77)\b", re.I),
105+
"deserialization": re.compile(r"deserial|CWE-?502\b", re.I),
106+
"template-injection": re.compile(r"\bssti\b|template inj|CWE-?1336\b", re.I),
107+
"xss-sink": re.compile(r"\bxss\b|cross.site.script|CWE-?79\b", re.I),
108+
"path-traversal": re.compile(r"path travers|directory travers|local file|\blfi\b|CWE-?(?:22|98)\b", re.I),
109+
}
110+
111+
112+
def corroborate(findings: list, reachable_sinks: list) -> int:
113+
"""Raise a finding to confirmed when a reachable code sink of the matching
114+
kind sits on the same route: the code does the dangerous thing and the scan
115+
reached it, which is the strongest evidence there is. -> how many."""
116+
by_route = {}
117+
for s in reachable_sinks or []:
118+
by_route.setdefault(s.get("reached_route") or "", set()).add(s.get("type"))
119+
raised = 0
120+
for f in findings:
121+
types = by_route.get(urlparse(str(f.get("url") or "")).path)
122+
if not types:
123+
continue
124+
text = f"{f.get('title') or ''} {f.get('cwe') or ''} {f.get('evidence') or ''}"
125+
matched = [t for t in sorted(types)
126+
if _SINK_FINDING.get(t) and _SINK_FINDING[t].search(text)]
127+
if matched and f.get("likelihood", 0) < 5:
128+
f["likelihood"] = 5
129+
f["likelihood_label"] = LIKELIHOOD_LABEL[5]
130+
f["sink_confirmed"] = True
131+
f["risk"] = cell(5, f.get("breachability", 3))
132+
f["risk_basis"] = ((f.get("risk_basis") or "")
133+
+ f" Corroborated by a code sink ({', '.join(matched)}) "
134+
"on the same reachable route.")
135+
raised += 1
136+
return raised
137+
138+
100139
def annotate(findings: list, exposure: str = "") -> list:
101140
"""Add likelihood, breachability, risk and a basis to each finding, in place."""
102141
for f in findings:

‎app/web/templates/dast_report_pdf.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ <h3>{{ f.ref }} {{ f.title }}</h3>
251251
{% if f.decision and f.decision.kind == 'exception' %}<tr><td class="k">Exception</td><td>Accepted until {{ f.decision.expires[:10] }} by {{ f.decision.by }}. {{ f.decision.reason | sentence_spaced }} Not counted toward the grade.</td></tr>{% endif %}
252252
{% if f.ai_verdict %}<tr><td class="k">Enhanced review</td><td>{{ f.ai_verdict | replace('_', ' ') }}{% if f.ai_why %}. {{ f.ai_why | sentence_spaced }}{% endif %}{% if f.ai_sast %} Matches code finding {{ f.ai_sast }}.{% endif %}</td></tr>{% endif %}
253253
</table>
254-
{% if f.evidence %}<div class="label"><strong>Evidence</strong></div><pre>{{ f.evidence }}</pre>{% endif %}
254+
{% if f.evidence %}<div class="label"><strong>{{ 'Proof' if f.likelihood and f.likelihood >= 4 else 'Evidence' }}</strong></div><pre>{{ f.evidence }}</pre>{% endif %}
255255
{% if f.repro.command %}
256256
<div style="margin-top:2mm"><strong>To see it again</strong></div>
257257
<pre>{{ f.repro.command }}</pre>

0 commit comments

Comments
 (0)