Learning Goals
3 minBy the end of this lesson you can:
- Structure a finding: title, severity, evidence, impact, remediation.
- Rank findings by severity (CVSS-style) and write an executive summary.
- Generate machine-readable output (JSON/CSV) for tooling and tracking.
- Produce a polished PDF report for stakeholders (reportlab, Level 7).
Warm-Up · Findings → Action
5 minYou've found vulnerabilities all level — but a finding only matters if someone fixes it. That requires communicating it well: an executive who decides priorities needs a one-paragraph summary; a developer who writes the fix needs exact reproduction steps and remediation; a ticketing system needs structured data. One report, multiple audiences.
Security reporting is a first-class skill, not an afterthought. A good report turns raw findings into a ranked, evidenced, actionable document: an executive summary for decision-makers, detailed findings (severity + evidence + impact + how-to-fix) for engineers, and structured JSON/CSV for tools and trend-tracking. This is also the perfect place to reuse your Level 7 reporting skills (PDF, CSV, JSON).
New Concept · Anatomy of a Security Report
14 minThe anatomy of a finding
TITLE short, specific: "SQL injection in /login username field"
SEVERITY Critical / High / Medium / Low / Info (with a score)
CATEGORY map to OWASP (A03 Injection) — shared language (L8-27)
DESCRIPTION what the issue is, in plain terms
EVIDENCE proof: the request, the response, a screenshot, the vulnerable code
IMPACT what an attacker could do (auth bypass → full data theft)
REMEDIATION exactly how to fix it ("use parameterised queries", L8-31)
REFERENCES OWASP/CWE links, the relevant standardEvery finding follows this shape. Evidence and remediation are what separate a useful report from a vague one: prove it's real, and tell them precisely how to fix it.
Severity & ranking
# rank by severity so the most dangerous findings come first. SEVERITY_ORDER = {"critical": 4, "high": 3, "medium": 2, "low": 1, "info": 0} def rank(findings: list[dict]) -> list[dict]: return sorted(findings, key=lambda f: SEVERITY_ORDER[f["severity"]], reverse=True) # severity can come from CVSS (industry standard 0-10) or a simple # likelihood × impact (L8-02). Either way: fix Critical/High first.
Industry uses CVSS (Common Vulnerability Scoring System, 0-10) for a standardized severity; for internal reports the likelihood × impact model (Lesson 2) is fine. The point is a clear order so limited time goes to the worst issues first.
The executive summary
The first thing anyone reads, and often the only thing leadership reads: a few sentences stating the overall risk posture, the count of findings by severity, and the top 2-3 things to fix. No jargon — translate "A03 SQLi" into "an attacker could read your entire customer database."
Two output formats, two audiences
import json, csv # JSON — for tools, ticketing, trend-tracking, diffing between scans def to_json(findings, path="report.json"): open(path, "w", encoding="utf-8").write(json.dumps(findings, indent=2)) # CSV — for spreadsheets, sorting, sharing with non-technical teams def to_csv(findings, path="report.csv"): fields = ["title", "severity", "category", "impact", "remediation"] with open(path, "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=fields, extrasaction="ignore") w.writeheader(); w.writerows(findings)
Machine-readable output (JSON/CSV — Level 7) lets findings flow into trackers, dashboards, and diffs ("what's new since last scan?"). The human-readable PDF tells the story. Produce both from one finding list.
A security report is a map of how to attack the system — it lists exact vulnerabilities and reproduction steps. Treat it like a secret: encrypt it at rest (Lesson 14), share it over secure channels, restrict access, and don't leave it on a public bucket. A leaked pentest report is a gift to attackers.
Worked Example · A Report Generator
12 minGoal: take a list of findings and produce all three outputs — a ranked JSON, a CSV, and a PDF with an executive summary — reusing Level 7 reporting. The deliverable of any assessment.
import json, csv from collections import Counter from datetime import date from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas SEV = {"critical": 4, "high": 3, "medium": 2, "low": 1, "info": 0} def generate_report(findings: list[dict], target: str) -> None: findings = sorted(findings, key=lambda f: SEV[f["severity"]], reverse=True) counts = Counter(f["severity"] for f in findings) # 1) JSON (for tooling) json.dump({"target": target, "date": str(date.today()), "summary": dict(counts), "findings": findings}, open("report.json", "w"), indent=2) # 2) CSV (for spreadsheets) with open("report.csv", "w", newline="", encoding="utf-8") as f: w = csv.DictWriter(f, fieldnames=["title", "severity", "category", "impact", "remediation"], extrasaction="ignore") w.writeheader(); w.writerows(findings) # 3) PDF (for stakeholders) c = canvas.Canvas("report.pdf", pagesize=A4) c.setFont("Helvetica-Bold", 18) c.drawString(50, 800, f"Security Assessment — {target}") c.setFont("Helvetica", 10) c.drawString(50, 782, f"Date: {date.today()}") # executive summary c.setFont("Helvetica-Bold", 12); c.drawString(50, 755, "Executive Summary") c.setFont("Helvetica", 10) summary = (f"{len(findings)} findings: " + ", ".join(f"{n} {sev}" for sev, n in counts.most_common())) c.drawString(50, 738, summary) crit_high = [f for f in findings if f["severity"] in ("critical", "high")] c.drawString(50, 722, f"Priority: fix {len(crit_high)} critical/high issue(s) first.") # findings y = 690 for f in findings: c.setFont("Helvetica-Bold", 11) c.drawString(50, y, f"[{f['severity'].upper()}] {f['title']}") c.setFont("Helvetica", 9) c.drawString(60, y - 14, f"Impact: {f['impact'][:90]}") c.drawString(60, y - 26, f"Fix: {f['remediation'][:90]}") y -= 50 if y < 80: c.showPage(); y = 800 c.save() print("wrote report.json, report.csv, report.pdf") findings = [ {"title": "SQL injection in /login", "severity": "critical", "category": "A03", "impact": "auth bypass → full DB read", "remediation": "use parameterised queries (L8-31)"}, {"title": "Missing HSTS header", "severity": "low", "category": "A05", "impact": "downgrade to HTTP possible", "remediation": "add Strict-Transport-Security header (L8-34)"}, {"title": "IDOR on /note/<id>", "severity": "high", "category": "A01", "impact": "read other users' notes", "remediation": "ownership check on every access (L8-28)"}, ] generate_report(findings, target="demo-blog (localhost)")
wrote report.json, report.csv, report.pdf
report.pdf:
Security Assessment — demo-blog (localhost) 2026-05-28
Executive Summary
3 findings: 1 critical, 1 high, 1 low
Priority: fix 2 critical/high issue(s) first.
[CRITICAL] SQL injection in /login
Impact: auth bypass → full DB read Fix: use parameterised queries (L8-31)
[HIGH] IDOR on /note/<id> ...
[LOW] Missing HSTS header ...Read the code
One finding list drives three audiences: JSON for trackers/diffs, CSV for spreadsheets, and a PDF that opens with an executive summary (counts by severity, "fix these N first") then lists findings ranked worst-first with impact + fix. Each finding maps to its OWASP category and the lesson that remediates it — so the report is a guided to-do list, not a wall of jargon. This composes your Level 7 reporting (reportlab/CSV/JSON) with everything you found this level. Remember to treat the output as sensitive.
Try It Yourself
13 minTake a real bug you found this level (your IDOR, SQLi, etc.) and write it up in the full finding format: title, severity, category, description, evidence, impact, remediation. Make the remediation specific.
Build the JSON + CSV generators from a list of findings, ranked by severity. Confirm the JSON round-trips and the CSV opens cleanly in a spreadsheet, sorted worst-first.
Generate the PDF with an executive summary and ranked findings (reportlab). Then write the executive summary twice — once for a CTO (business impact, priorities) and once for a developer (technical specifics) — and reflect on the difference in language.
Mini-Challenge · Scan-to-Report Pipeline
8 minConnect your tools to your report: take the JSON output of an earlier scanner (the port scanner audit, Lesson 20; the security-headers auditor, Lesson 34; or the dependency checker, Lesson 35), normalise the findings into the standard finding schema, and feed them into the report generator. One pipeline: scan → findings → ranked multi-format report.
Show the normalisation approach
def normalise_port_scan(scan_json: dict) -> list[dict]: """Turn the L8-20 scanner's output into standard findings.""" findings = [] for p in scan_json.get("open_ports", []): if "HIGH" in p.get("note", ""): findings.append({ "title": f"Exposed {p['service']} on port {p['port']}", "severity": "high", "category": "A05", "impact": "sensitive service reachable from the network", "remediation": "bind to localhost / firewall the port (L8-34)", "evidence": f"port {p['port']} open: {p.get('banner','')}", }) return findings # pipeline: scan → normalise → report import json scan = json.load(open("scan.json")) generate_report(normalise_port_scan(scan), target=scan["host"])
Non-negotiables: take a real scanner's JSON, map to the standard finding schema, produce the ranked multi-format report.
Recap
3 minA finding nobody acts on is worthless — so reporting is a core security skill. Structure each finding with title, severity, OWASP category, evidence, impact, and remediation; rank by severity (CVSS or likelihood × impact) so the worst comes first. Open with an executive summary in plain language for decision-makers, then detailed findings for engineers. Produce both human-readable PDF (the story) and machine-readable JSON/CSV (for tools, tickets, and diffs) from one finding list — reusing your Level 7 reporting. And treat the report as sensitive: it's a map of how to attack the system.
Vocabulary Card
- finding
- A documented vulnerability: title, severity, evidence, impact, remediation.
- CVSS
- Common Vulnerability Scoring System — a standard 0-10 severity score.
- executive summary
- A plain-language overview of risk and priorities for decision-makers.
- remediation
- The specific fix for a finding — the most actionable part.
Homework
4 minRun one of your scanners against your own app, normalise its output into findings, and generate the full report (JSON + CSV + PDF with an executive summary). Include at least three findings of differing severity with evidence and specific remediation. Write the executive summary for a non-technical reader. Note how you'd store/share the report securely.
Sample · executive summary + secure handling
EXECUTIVE SUMMARY (for a non-technical reader) We assessed the demo-blog (on our own test server) and found 3 issues: 1 critical, 1 high, 1 low. The critical issue would let an attacker log in WITHOUT a password and read our entire user database — this must be fixed first (it's a well-known flaw with a simple, standard fix). The high issue lets one logged-in user read OTHER users' private notes. The low issue makes it slightly easier to intercept traffic on insecure networks. Recommended priority: fix the critical and high issues this week (both are small code changes); schedule the low issue next sprint. Secure handling: this report names exact vulnerabilities and how to exploit them, so it's effectively an attack map. I store it encrypted (Fernet, L8-14), share it only via our access-controlled drive with named recipients, and delete the working copies after the fixes ship. Outputs: report.pdf (this), report.json (for our issue tracker), report.csv (for the team spreadsheet).
Non-negotiables: scan→findings→full multi-format report with ≥3 ranked findings (evidence+remediation), a lay executive summary, and a secure-handling note.