Skip to content

Commit 53c7a72

Browse files
committed
Name the exact code finding when the scan and the code agree on a weakness
When a dynamic finding sits on a route whose code holds a matching dangerous sink, the scan already raised it to confirmed. Now it also names the exact code finding behind that sink, by its id, drawn from the open code findings of that weakness type in the repositories behind the site. So the two way link is explicit: the dynamic report says which code finding it matches, and the code finding shows that a scan confirmed it live. The code telemetry is doing what a plain dynamic scanner cannot, pointing the match at the real weakness.
1 parent fc6448c commit 53c7a72

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

‎app/core/dast_report.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,29 @@ def available(scan) -> bool:
302302
and scan.provider == "insightappsec")
303303

304304

305+
def _sast_by_type(db, asset, user) -> dict:
306+
"""Open code (SAST) findings behind this site, grouped by the dynamic sink
307+
type they correspond to, as {type: [GCR id, ...]}. The weakness type is
308+
read from the finding's rule, title and description with the same patterns
309+
the risk engine uses, so a reachable code sink can be matched to the exact
310+
code finding it came from."""
311+
from app.core import dast_risk, external
312+
from app.core.models import ExternalAssetRepo, Repository
313+
314+
names = [r.full_name for r in db.query(Repository)
315+
.join(ExternalAssetRepo, ExternalAssetRepo.repository_id == Repository.id)
316+
.filter(ExternalAssetRepo.asset_id == asset.id).all()]
317+
if not names:
318+
return {}
319+
out = {}
320+
for s in external.sast_findings(db, names, 300, user):
321+
text = f"{s.rule or ''} {s.title or ''} {s.description or ''}"
322+
for typ, rx in dast_risk._SINK_FINDING.items():
323+
if rx.search(text):
324+
out.setdefault(typ, []).append(s.vuln_id)
325+
return out
326+
327+
305328
def build(db, asset, scan, user=None, organization: str = "") -> dict:
306329
"""Everything the template needs for one scan."""
307330
from app.core import dast_engines, external
@@ -362,8 +385,12 @@ def build(db, asset, scan, user=None, organization: str = "") -> dict:
362385
code_sinks = []
363386
reachable_sinks = [s for s in code_sinks if s.get("reachable")]
364387
context_sinks = [s for s in code_sinks if not s.get("reachable")]
388+
# Real SAST findings behind this site, grouped by the weakness type they are,
389+
# so a corroboration can name the exact code finding (its GCR id), not just
390+
# say "a code sink". That is the SAST telemetry driving a positive match.
391+
sast_by_type = _sast_by_type(db, asset, user)
365392
# A finding a reachable sink corroborates is confirmed by code and scan both.
366-
dast_risk.corroborate(findings, reachable_sinks)
393+
dast_risk.corroborate(findings, reachable_sinks, sast_by_type)
367394
risk_matrix = dast_risk.matrix(findings)
368395
from app.core.external_decisions import counted as _counted
369396

‎app/core/dast_risk.py‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,15 @@ def cell(lk: int, br: int) -> str:
117117
}
118118

119119

120-
def corroborate(findings: list, reachable_sinks: list) -> int:
120+
def corroborate(findings: list, reachable_sinks: list, sast_by_type: dict = None) -> int:
121121
"""Raise a finding to confirmed when a reachable code sink of the matching
122122
kind sits on the same route: the code does the dangerous thing and the scan
123-
reached it, which is the strongest evidence there is. -> how many."""
123+
reached it, which is the strongest evidence there is. -> how many.
124+
125+
sast_by_type maps a sink type to the ids of the real code findings of that
126+
type, so a match can name the exact code finding (ai_sast) and the link
127+
shows on both the dynamic report and the code finding."""
128+
sast_by_type = sast_by_type or {}
124129
by_route = {}
125130
for s in reachable_sinks or []:
126131
by_route.setdefault(s.get("reached_route") or "", set()).add(s.get("type"))
@@ -132,7 +137,16 @@ def corroborate(findings: list, reachable_sinks: list) -> int:
132137
text = f"{f.get('title') or ''} {f.get('cwe') or ''} {f.get('evidence') or ''}"
133138
matched = [t for t in sorted(types)
134139
if _SINK_FINDING.get(t) and _SINK_FINDING[t].search(text)]
135-
if matched and f.get("likelihood", 0) < 5:
140+
if not matched:
141+
continue
142+
# Name the exact code finding the sink came from, when one of that type
143+
# is known, so the two way link is explicit and not only the AI's.
144+
if not f.get("ai_sast"):
145+
for t in matched:
146+
if sast_by_type.get(t):
147+
f["ai_sast"] = sast_by_type[t][0]
148+
break
149+
if f.get("likelihood", 0) < 5:
136150
f["likelihood"] = 5
137151
f["likelihood_label"] = LIKELIHOOD_LABEL[5]
138152
f["sink_confirmed"] = True

0 commit comments

Comments
 (0)