Skip to content

Commit 907138b

Browse files
committed
External sites list shows each site's DAST grade
The newest finished scan's letter grade sits left of the worst severity tag in the Last dynamic scan column, and the scan date moves to its own line under them. The grade is worked out when the page loads, with the same rules as the PDF report, so a decision or an enhanced review made since the scan already counts. Hovering shows the score and the scan date, and the grade links to the report. Scans that kept only counts show no grade, since a grade needs each finding's category.
1 parent a95eee5 commit 907138b

3 files changed

Lines changed: 67 additions & 2 deletions

File tree

‎app/core/dast_report.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,62 @@ def score(findings: list, triaged: bool, only: str = None) -> dict:
147147
"counted": sum(len(v) for v in by_cat.values())}
148148

149149

150+
def scan_grade(scan, decisions: dict):
151+
"""The report's overall grade for one finished scan, worked out the same
152+
way the PDF works it out: decisions as they stand today, false positives
153+
left out, and the enhanced review's verdicts when it ran. -> {"score",
154+
"grade"}, or None when the scan kept only counts, since a grade needs
155+
each finding's category."""
156+
from app.core import external, external_decisions
157+
158+
if scan is None or scan.status != "done" or scan.summary_only:
159+
return None
160+
raw = external.findings_of(scan)
161+
if not raw and any(getattr(scan, s) or 0 for s in SEVERITIES):
162+
return None
163+
found = [f for f in external_decisions.apply(raw, decisions, scan_id=scan.id)
164+
if (f.get("decision") or {}).get("kind") != "false_positive"]
165+
triaged = (scan.ai_status or "") == "done" and any(f.get("ai_verdict") for f in found)
166+
for f in found:
167+
f["owasp"] = owasp_of(f)
168+
graded = score(found, triaged)
169+
return {"score": graded["score"], "grade": graded["grade"], "triaged": triaged}
170+
171+
172+
def latest_grades(db, asset_ids: list) -> dict:
173+
"""{asset id: grade dict plus scan id and date} for each site's newest
174+
finished scan, the one the list shows as its last dynamic scan. Reads
175+
only those scans' findings, and every site's decisions in one query."""
176+
from app.core.models import ExternalDastScan, ExternalFindingDecision
177+
178+
ids = [int(i) for i in asset_ids if i]
179+
if not ids:
180+
return {}
181+
newest: dict = {}
182+
for asset_id, scan_id in (db.query(ExternalDastScan.asset_id, ExternalDastScan.id)
183+
.filter(ExternalDastScan.asset_id.in_(ids),
184+
ExternalDastScan.status == "done")
185+
.order_by(ExternalDastScan.asset_id,
186+
ExternalDastScan.scanned_at.desc(),
187+
ExternalDastScan.id.desc())):
188+
newest.setdefault(asset_id, scan_id)
189+
if not newest:
190+
return {}
191+
decisions: dict = {}
192+
for d in db.query(ExternalFindingDecision).filter(
193+
ExternalFindingDecision.asset_id.in_(list(newest))):
194+
decisions.setdefault(d.asset_id, {}).setdefault(
195+
d.finding_key, {})[int(d.only_scan_id or 0)] = d
196+
out = {}
197+
for scan in db.query(ExternalDastScan).filter(ExternalDastScan.id.in_(list(newest.values()))):
198+
graded = scan_grade(scan, decisions.get(scan.asset_id, {}))
199+
if graded:
200+
graded.update(scan_id=scan.id, scanned_at=scan.scanned_at,
201+
report=available(scan))
202+
out[scan.asset_id] = graded
203+
return out
204+
205+
150206
def _q(text: str) -> str:
151207
return shlex.quote(str(text or ""))
152208

‎app/web/routes_external.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ def list_page(request: Request, q: str = "", tag: str = "", app: str = "",
232232
page_no = 1
233233
offset = (page_no - 1) * size
234234
rows = rows[offset:offset + size]
235+
# The DAST grade of each shown site's newest scan, worked out now so
236+
# a decision or a review made since the scan already counts.
237+
from app.core import dast_report
238+
239+
grades = dast_report.latest_grades(db, [r["a"].id for r in rows])
240+
for r in rows:
241+
r["dast_grade"] = grades.get(r["a"].id)
235242
from urllib.parse import urlencode
236243

237244
filters = [(k, v) for k, v in (("q", q), ("tag", tag), ("app", app), ("grade", grade),

‎app/web/templates/external.html‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,10 @@
154154
<button class="btn sm danger" type="submit">Stop</button></form>{% endif %}</div>
155155
{% endif %}
156156
{% if a.last_dast_at %}
157-
{% if a.last_dast_severity in ('', 'none') %}<span class="badge ok">no findings</span>{% else %}<span class="badge {{ a.last_dast_severity }}">worst: {{ a.last_dast_severity }}</span>{% endif %}
158-
{{ a.last_dast_at | dt('%Y-%m-%d') }}
157+
<div class="nowrap">{% set g = r.dast_grade %}{% if g %}<a href="{{ base }}/external/{{ a.id }}/scans/{{ g.scan_id }}{{ '/report.pdf' if g.report else '' }}" class="grade {{ g.grade }}" style="text-decoration:none;margin-right:.3rem"
158+
title="DAST grade, score {{ g.score }} of 100, from the scan of {{ g.scanned_at | dt('%Y-%m-%d') }}{{ '' if g.triaged else ', every finding counted in full (no enhanced review)' }}">{{ g.grade }}</a>{% endif %}
159+
{% if a.last_dast_severity in ('', 'none') %}<span class="badge ok">no findings</span>{% else %}<span class="badge {{ a.last_dast_severity }}">worst: {{ a.last_dast_severity }}</span>{% endif %}</div>
160+
<div>{{ a.last_dast_at | dt('%Y-%m-%d') }}</div>
159161
<div class="faint">{{ a.last_dast_server }}</div>
160162
{% if a.last_dast_url %}<div><a rel="noopener noreferrer" {% if a.last_dast_url.startswith('http') %}target="_blank" href="{{ a.last_dast_url }}"{% else %}href="{{ base }}{{ a.last_dast_url }}"{% endif %}>result</a></div>{% endif %}
161163
{% if r.due == 'due' %}<span class="badge pending">overdue</span>{% endif %}

0 commit comments

Comments
 (0)