Skip to content

Commit fc6448c

Browse files
committed
Show on a code finding when a dynamic scan has confirmed it live, and warn before waiving
A dynamic finding already names the code finding it matches. Read the other way, that means a code finding can now say when a dynamic scan reached the same weakness on a running site: not only in the code, but exploitable now. The code finding shows which site, with a link to the scan, drawn from the newest scan of each site linked to its repository. This matters most at waiver time. When somebody asks for an exception on a code finding a dynamic scan has confirmed, the request shows a plain warning that the risk is proven and reachable, not theoretical, and that "this code does not get deployed" does not apply, because the scan reached it on a live site.
1 parent 7e38057 commit fc6448c

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

‎app/core/external.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4104,6 +4104,60 @@ def removed_of(scan: ExternalDastScan) -> list:
41044104
if isinstance(summary, dict) else []
41054105

41064106

4107+
def dast_confirmations_for(db, finding) -> list:
4108+
"""Live dynamic findings that point back at one code (SAST) finding.
4109+
4110+
A DAST finding names the code finding it matches in ai_sast (the GCR id).
4111+
When one does, and it was not dismissed, a dynamic scan has shown this exact
4112+
code weakness is reachable on a running site: it is not only in the code, it
4113+
is exploitable now. -> a short list to show on the finding and to warn on
4114+
before it is waived. Looks only at the newest done scan of each site linked
4115+
to the finding's repository, so it is a couple of queries, not a sweep.
4116+
"""
4117+
from app.core import dast_risk, external_decisions
4118+
from app.core.models import ExternalAssetRepo, Repository
4119+
4120+
vid = (getattr(finding, "vuln_id", "") or "")
4121+
repo = (getattr(finding, "repository", "") or "")
4122+
if not vid or not repo:
4123+
return []
4124+
repo_row = (db.query(Repository)
4125+
.filter(Repository.full_name == repo).one_or_none())
4126+
if repo_row is None:
4127+
return []
4128+
asset_ids = [r.asset_id for r in db.query(ExternalAssetRepo)
4129+
.filter(ExternalAssetRepo.repository_id == repo_row.id).all()]
4130+
out = []
4131+
for aid in asset_ids:
4132+
scan = (db.query(ExternalDastScan)
4133+
.filter(ExternalDastScan.asset_id == aid,
4134+
ExternalDastScan.status == "done")
4135+
.order_by(ExternalDastScan.id.desc()).first())
4136+
if scan is None:
4137+
continue
4138+
asset = db.get(ExternalAsset, aid)
4139+
found = external_decisions.apply(findings_of(scan),
4140+
external_decisions.for_asset(db, aid),
4141+
scan_id=scan.id)
4142+
for f in found:
4143+
if vid not in str(f.get("ai_sast") or ""):
4144+
continue
4145+
if f.get("ai_verdict") == "likely_false_positive":
4146+
continue
4147+
if (f.get("decision") or {}).get("kind") == "false_positive":
4148+
continue
4149+
lk = int(f.get("likelihood") or 0)
4150+
out.append({
4151+
"host": asset.host if asset else "",
4152+
"asset_id": aid, "scan_id": scan.id,
4153+
"title": f.get("title") or "", "severity": f.get("severity") or "",
4154+
"likelihood": lk,
4155+
"likelihood_label": dast_risk.LIKELIHOOD_LABEL.get(lk, ""),
4156+
"verdict": f.get("ai_verdict") or "", "url": f.get("url") or "",
4157+
"when": scan.scanned_at or scan.started_at})
4158+
return out
4159+
4160+
41074161
def restore_removed(db, scan: ExternalDastScan, key: str, actor: str) -> dict:
41084162
"""Put a finding the model took out back on the scan. -> the finding,
41094163
or {} when it was not taken out."""

‎app/web/routes_pages.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,8 +1898,11 @@ def finding_detail(finding_id: int, request: Request, msg: str = "",
18981898
exclusions_here = [r for r in exclusions_store.covering(db, row)
18991899
if r.id in mine]
19001900

1901+
from app.core import external as external_mod
1902+
19011903
context = _shared(db, user)
19021904
context.update(active_page="findings", f=row, waiver=waiver,
1905+
dast_confirmed=external_mod.dast_confirmations_for(db, row),
19031906
exclusions_here=exclusions_here,
19041907
can_revoke=rbac.can(user, "waiver.revoke"),
19051908
host_name=host_name,

‎app/web/templates/finding_detail.html‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,24 @@ <h1><span class="badge {{ f.severity.value }}"
294294
</div>
295295
</div>
296296

297+
{% if dast_confirmed %}
298+
<div class="notice bad" id="dast-confirmed">
299+
<div><strong>Confirmed live by dynamic testing.</strong>
300+
A dynamic scan reached this exact weakness on a running site, so it is not only in the code, it is exploitable now.
301+
{% for d in dast_confirmed %}
302+
<div class="small" style="margin-top:.3rem">
303+
<span class="badge {{ d.severity }}">{{ d.severity }}</span>
304+
on <a href="{{ base }}/external/{{ d.asset_id }}/scans/{{ d.scan_id }}">{{ d.host }}</a>
305+
{%- if d.title %} &mdash; {{ d.title }}{% endif %}
306+
{%- if d.likelihood_label %} <span class="faint">({{ d.likelihood_label }})</span>{% endif %}
307+
{%- if d.when %} <span class="faint">on {{ d.when | dt('%Y-%m-%d') }}</span>{% endif %}
308+
</div>
309+
{% endfor %}
310+
<div class="small faint" style="margin-top:.3rem">Weigh this before writing an exception: a waiver here accepts a risk a scanner has already proven is reachable, not a theoretical one.</div>
311+
</div>
312+
</div>
313+
{% endif %}
314+
297315
{% if report_fp %}
298316
<div class="notice warn">
299317
{% set others = report_fp.awaiting | length - 1 %}
@@ -1326,6 +1344,14 @@ <h1><span class="badge {{ f.severity.value }}"
13261344
<button type="button" class="modal-close" data-close-modal>&times;</button></div>
13271345
<div class="modal-body">
13281346
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
1347+
{% if dast_confirmed %}
1348+
<div class="notice bad" style="margin-bottom:.6rem">
1349+
<strong>A dynamic scan confirmed this is live.</strong>
1350+
It was reached on {{ dast_confirmed[0].host }}{% if dast_confirmed | length > 1 %} and {{ dast_confirmed | length - 1 }} other site{{ '' if dast_confirmed | length - 1 == 1 else 's' }}{% endif %}.
1351+
Waiving it accepts a proven, exploitable risk, not a theoretical one.
1352+
{%- if f.kind.value != 'credential' %} "Not in scope: this code does not get deployed" does not apply here, because the scan reached it on a running site.{% endif %}
1353+
</div>
1354+
{% endif %}
13291355
{% if f.kind.value != 'credential' %}
13301356
{# Not offered on a credential. A key works whether or not the code
13311357
it sits in is ever deployed. #}

0 commit comments

Comments
 (0)