NullPointer Studio Pentest Report Generator
Purpose
Read findings.json from the current pentest session and generate a complete, client-ready penetration test report as a styled PDF. The report follows the NullPointer Studio dark theme with healthcare-aware business risk language per finding.
Load refs/style.md for the full CSS specification and color palette before writing any HTML.
Tools Available
| Tool |
Use for |
Read |
Read findings.json, session.json, pocs/*.http files |
Bash |
Run python3 report_generator.py to produce the PDF |
Write |
Write the generated Python script |
report(action="note", ...) |
Log generation decisions |
Workflow
Step 0 — Collect inputs
Determine the findings.json path:
- If
$ARGUMENTS provides a path, use it
- Otherwise use
./findings.json (repo root)
Read findings.json — extract all entries where type == "finding" (skip diagram and note entries). For each finding, capture:
id, title, severity, target, description, evidence, tool_used
reproduction (command + steps) if present
remediation (diff / before / after / verification) if present
cve if present
Read session.json if it exists — extract:
target (base URL)
depth
start_time (format as date for report)
skill (used as engagement type)
Scan pocs/ for .http files — map each file to its finding by matching filename keywords against finding titles.
Call report(action="note", ...) with: finding count by severity, target, output path.
Step 1 — Deduplicate and classify findings
Group findings by severity. Within each group, deduplicate on normalized title (same title = same finding, keep the one with more evidence). Sort order for report:
CRITICAL → HIGH → MEDIUM → LOW → INFO
Compute stat box counts:
- Count findings in each severity bucket
- Total = sum of all
Step 2 — Write the generator script
Write a Python script report_generator.py to the repo root. The script must:
Import json, base64, html, pathlib.Path, datetime, weasyprint
Embed the NullPointer Studio CSS from refs/style.md verbatim as CSS_STR
Load the logo:
LOGO_PATH = Path("/Users/riccardo.tencate/Desktop/agent-smith/templates/FullLogo_Transparent.png")
with open(LOGO_PATH, "rb") as f:
LOGO_SRC = "data:image/png;base64," + base64.b64encode(f.read()).decode()
Define SEV_META dict:
SEV_META = {
"critical": {"label": "CRITICAL", "color": "#ff4d6d", "bg": "rgba(255,77,109,0.12)", "border": "#ff4d6d"},
"high": {"label": "HIGH", "color": "#ff8c42", "bg": "rgba(255,140,66,0.12)", "border": "#ff8c42"},
"medium": {"label": "MEDIUM", "color": "#ffd166", "bg": "rgba(255,209,102,0.1)", "border": "#ffd166"},
"low": {"label": "LOW", "color": "#5bf29b", "bg": "rgba(91,242,155,0.08)", "border": "#5bf29b"},
"info": {"label": "INFO", "color": "#7b78ff", "bg": "rgba(123,120,255,0.08)", "border": "#7b78ff"},
}
Define helper functions:
def esc(s): return html.escape(str(s))
def badge(sev): ... # colored inline badge span
def code(text): ... # <pre class="code-block"><code>...</code></pre>
Define finding_section(np_id, severity, title, owasp, asvs, endpoint, auth, confirmed, description, business_risk, evidence, steps, remediation) — see refs/style.md for the exact HTML structure.
Build the full HTML document (cover page → stat boxes → exec summary → scope → risk dashboard → findings → remediation table → clean controls) and write it via weasyprint.
Script skeleton:
#!/usr/bin/env python3
import json, base64, html as html_mod, datetime
from pathlib import Path
from weasyprint import HTML as WeasyprintHTML
BASE_DIR = Path(".")
OUTPUT = BASE_DIR / "report_{target_slug}_{date}.pdf"
# ... helpers, CSS, content sections ...
if __name__ == "__main__":
html_content = build_html()
Path("report.html").write_text(html_content, encoding="utf-8")
WeasyprintHTML(string=html_content, base_url=str(BASE_DIR)).write_pdf(str(OUTPUT))
print(f"PDF → {OUTPUT}")
Step 3 — Populate report sections
Cover page
[Logo — 180px height]
CONFIDENTIAL (red monospace badge)
Penetration Test Report (28pt Chakra Petch bold)
{target domain} (14pt green Chakra Petch)
───────────────────────────────
Client | {client name}
Target | {base URL}
Test type | Web App Pentest + White-Box Code Review
Framework | {detected framework if known}
Test date | {date}
Report date | {today}
Prepared by | NullPointer Studio
Version | 1.0
Stat boxes (one per severity)
Show counts for CRITICAL, HIGH, MEDIUM, LOW, INFO, and Total. Color each number with the severity accent color. Only include severity boxes that have at least 1 finding — always show Total.
Executive Summary
Write a 3–4 paragraph summary covering:
- What was tested, what methodology, what environment
- The most critical finding(s) — name them by NP-ID and title, explain the real impact in one sentence each
- Medium/Low/Info summary in aggregate (count + theme)
- What was found to be solid (clean controls) — 1–2 sentences
Do NOT use generic templates — derive every sentence from the actual findings in findings.json. Healthcare impact language where relevant.
Risk Dashboard table
Columns: ID | Severity | Title | OWASP | Status
Show all findings in severity order. Use badge(sev) for the severity cell. Status = "Confirmed" for all non-info findings, "Informational" for info.
Finding cards
For each finding, render a card using finding_section(...). Map findings.json fields as follows:
| Card field |
Source |
| np_id |
Assign sequentially: NP-001, NP-002, ... in severity order |
| severity |
finding.severity |
| title |
finding.title |
| owasp |
finding.owasp if present, else derive from category (see mapping below) |
| asvs |
finding.asvs if present, else "—" |
| endpoint |
finding.target (strip base URL if possible) |
| auth |
Infer from description ("Yes" if mentions session/auth, "No" if unauthenticated) |
| confirmed |
"Yes — live PoC" if poc file exists, "Yes — code review" if code-only, else "Yes" |
| description |
finding.description — wrap in <p> tags, convert code blocks to code() helper |
| business_risk |
<div class='risk-box'><strong>Impact:</strong> ...</div> — derive from description; if healthcare context: reference GDPR/AVG, WBGO, PHI, care continuity |
| evidence |
finding.evidence + PoC file content if available |
| steps |
finding.reproduction.steps if present, else derive 3-step reproduction from description |
| remediation |
finding.remediation code/diff if present, else derive from description |
OWASP category mapping (use if not explicit in finding):
| Keywords in title/description |
OWASP |
| injection, sqli, xss, ssti, xxe |
A03:2021 — Injection |
| auth, password, session, token, mfa, 2fa |
A07:2021 — Identification and Authentication Failures |
| access control, idor, privilege, admin |
A01:2021 — Broken Access Control |
| rate limit, config, header, tls, cors |
A05:2021 — Security Misconfiguration |
| upload, file, deserialization |
A04:2021 — Insecure Design |
| api, endpoint, no auth |
API2:2023 — Broken Authentication |
| crypto, hash, weak |
A02:2021 — Cryptographic Failures |
Remediation Summary table
Columns: ID | Severity | Title | Priority | Effort | Fix (one-line)
Assign priority based on severity:
- CRITICAL → P0 — Immediate
- HIGH → P0 — Immediate
- MEDIUM → P2 — Next sprint (or P1 — This sprint for the most impactful)
- LOW → P3 — Milestone or P4 — Backlog
- INFO → — (no priority)
Effort: Low (config change, 1-liner fix), Medium (refactor needed), High (architecture change).
Clean controls
If session.json or the findings list contains explicit "tested clean" notes, include a "Controls Tested — No Issues Found" table. If no clean controls are documented, omit this section.
Step 4 — Generate the PDF
python3 report_generator.py
If weasyprint is not installed: pip install weasyprint first.
After generation, print:
Report generated:
PDF → ./report_{target}_{date}.pdf
HTML → ./report_{target}_{date}.html
Findings: {N} total ({crit} critical, {high} high, {med} medium, {low} low, {info} info)
Rules
- Never invent findings — only include what is in
findings.json
- Business risk is mandatory for every finding — derive from the actual finding; never write "could potentially" — write what an attacker concretely achieves
- NP-IDs must be sequential in severity order (NP-001 = most severe)
- INFO findings get a simplified card: Description + Business Risk + Recommendations only (no Evidence/Steps headers unless evidence is meaningful)
- Logo path is always
/Users/riccardo.tencate/Desktop/agent-smith/templates/FullLogo_Transparent.png
- Output filename:
report_{target_slug}_{YYYY-MM-DD}.pdf where target_slug = domain with dots replaced by underscores
- weasyprint is the only supported PDF engine — do not use pdfkit, xhtml2pdf, or headless Chrome
- Load
refs/style.md before writing any HTML or CSS — never invent new colors or fonts
1---2name: report3description: Generate a NullPointer Studio styled PDF penetration test report from findings.json, producing a professional dark-themed PDF with executive summary, risk dashboard, per-finding cards, and remediation summary.4---56# NullPointer Studio Pentest Report Generator78## Purpose910Read `findings.json` from the current pentest session and generate a complete, client-ready penetration test report as a styled PDF. The report follows the NullPointer Studio dark theme with healthcare-aware business risk language per finding.1112Load `refs/style.md` for the full CSS specification and color palette before writing any HTML.1314---1516## Tools Available1718| Tool | Use for |19|------|---------|20| `Read` | Read findings.json, session.json, pocs/*.http files |21| `Bash` | Run `python3 report_generator.py` to produce the PDF |22| `Write` | Write the generated Python script |23| `report(action="note", ...)` | Log generation decisions |2425---2627## Workflow2829### Step 0 — Collect inputs30311. Determine the `findings.json` path:32 - If `$ARGUMENTS` provides a path, use it33 - Otherwise use `./findings.json` (repo root)34352. Read `findings.json` — extract all entries where `type == "finding"` (skip `diagram` and `note` entries). For each finding, capture:36 - `id`, `title`, `severity`, `target`, `description`, `evidence`, `tool_used`37 - `reproduction` (command + steps) if present38 - `remediation` (diff / before / after / verification) if present39 - `cve` if present40413. Read `session.json` if it exists — extract:42 - `target` (base URL)43 - `depth`44 - `start_time` (format as date for report)45 - `skill` (used as engagement type)46474. Scan `pocs/` for `.http` files — map each file to its finding by matching filename keywords against finding titles.48495. Call `report(action="note", ...)` with: finding count by severity, target, output path.5051---5253### Step 1 — Deduplicate and classify findings5455Group findings by severity. Within each group, deduplicate on normalized title (same title = same finding, keep the one with more evidence). Sort order for report:5657```58CRITICAL → HIGH → MEDIUM → LOW → INFO59```6061Compute stat box counts:62- Count findings in each severity bucket63- Total = sum of all6465---6667### Step 2 — Write the generator script6869Write a Python script `report_generator.py` to the repo root. The script must:70711. Import `json`, `base64`, `html`, `pathlib.Path`, `datetime`, `weasyprint`72732. Embed the NullPointer Studio CSS from `refs/style.md` verbatim as `CSS_STR`74753. Load the logo:76 ```python77 LOGO_PATH = Path("/Users/riccardo.tencate/Desktop/agent-smith/templates/FullLogo_Transparent.png")78 with open(LOGO_PATH, "rb") as f:79 LOGO_SRC = "data:image/png;base64," + base64.b64encode(f.read()).decode()80 ```81824. Define `SEV_META` dict:83 ```python84 SEV_META = {85 "critical": {"label": "CRITICAL", "color": "#ff4d6d", "bg": "rgba(255,77,109,0.12)", "border": "#ff4d6d"},86 "high": {"label": "HIGH", "color": "#ff8c42", "bg": "rgba(255,140,66,0.12)", "border": "#ff8c42"},87 "medium": {"label": "MEDIUM", "color": "#ffd166", "bg": "rgba(255,209,102,0.1)", "border": "#ffd166"},88 "low": {"label": "LOW", "color": "#5bf29b", "bg": "rgba(91,242,155,0.08)", "border": "#5bf29b"},89 "info": {"label": "INFO", "color": "#7b78ff", "bg": "rgba(123,120,255,0.08)", "border": "#7b78ff"},90 }91 ```92935. Define helper functions:94 ```python95 def esc(s): return html.escape(str(s))96 def badge(sev): ... # colored inline badge span97 def code(text): ... # <pre class="code-block"><code>...</code></pre>98 ```991006. Define `finding_section(np_id, severity, title, owasp, asvs, endpoint, auth, confirmed, description, business_risk, evidence, steps, remediation)` — see `refs/style.md` for the exact HTML structure.1011027. Build the full HTML document (cover page → stat boxes → exec summary → scope → risk dashboard → findings → remediation table → clean controls) and write it via `weasyprint`.103104**Script skeleton:**105106```python107#!/usr/bin/env python3108import json, base64, html as html_mod, datetime109from pathlib import Path110from weasyprint import HTML as WeasyprintHTML111112BASE_DIR = Path(".")113OUTPUT = BASE_DIR / "report_{target_slug}_{date}.pdf"114115# ... helpers, CSS, content sections ...116117if __name__ == "__main__":118 html_content = build_html()119 Path("report.html").write_text(html_content, encoding="utf-8")120 WeasyprintHTML(string=html_content, base_url=str(BASE_DIR)).write_pdf(str(OUTPUT))121 print(f"PDF → {OUTPUT}")122```123124---125126### Step 3 — Populate report sections127128#### Cover page129130```131[Logo — 180px height]132CONFIDENTIAL (red monospace badge)133Penetration Test Report (28pt Chakra Petch bold)134{target domain} (14pt green Chakra Petch)135───────────────────────────────136Client | {client name}137Target | {base URL}138Test type | Web App Pentest + White-Box Code Review139Framework | {detected framework if known}140Test date | {date}141Report date | {today}142Prepared by | NullPointer Studio143Version | 1.0144```145146#### Stat boxes (one per severity)147148Show counts for CRITICAL, HIGH, MEDIUM, LOW, INFO, and Total. Color each number with the severity accent color. Only include severity boxes that have at least 1 finding — always show Total.149150#### Executive Summary151152Write a 3–4 paragraph summary covering:1531. What was tested, what methodology, what environment1542. The most critical finding(s) — name them by NP-ID and title, explain the real impact in one sentence each1553. Medium/Low/Info summary in aggregate (count + theme)1564. What was found to be solid (clean controls) — 1–2 sentences157158Do NOT use generic templates — derive every sentence from the actual findings in `findings.json`. Healthcare impact language where relevant.159160#### Risk Dashboard table161162Columns: ID | Severity | Title | OWASP | Status163164Show all findings in severity order. Use `badge(sev)` for the severity cell. Status = "Confirmed" for all non-info findings, "Informational" for info.165166#### Finding cards167168For each finding, render a card using `finding_section(...)`. Map `findings.json` fields as follows:169170| Card field | Source |171|---|---|172| np_id | Assign sequentially: NP-001, NP-002, ... in severity order |173| severity | `finding.severity` |174| title | `finding.title` |175| owasp | `finding.owasp` if present, else derive from category (see mapping below) |176| asvs | `finding.asvs` if present, else `"—"` |177| endpoint | `finding.target` (strip base URL if possible) |178| auth | Infer from description ("Yes" if mentions session/auth, "No" if unauthenticated) |179| confirmed | "Yes — live PoC" if poc file exists, "Yes — code review" if code-only, else "Yes" |180| description | `finding.description` — wrap in `<p>` tags, convert code blocks to `code()` helper |181| business_risk | `<div class='risk-box'><strong>Impact:</strong> ...</div>` — derive from description; if healthcare context: reference GDPR/AVG, WBGO, PHI, care continuity |182| evidence | `finding.evidence` + PoC file content if available |183| steps | `finding.reproduction.steps` if present, else derive 3-step reproduction from description |184| remediation | `finding.remediation` code/diff if present, else derive from description |185186**OWASP category mapping** (use if not explicit in finding):187188| Keywords in title/description | OWASP |189|---|---|190| injection, sqli, xss, ssti, xxe | A03:2021 — Injection |191| auth, password, session, token, mfa, 2fa | A07:2021 — Identification and Authentication Failures |192| access control, idor, privilege, admin | A01:2021 — Broken Access Control |193| rate limit, config, header, tls, cors | A05:2021 — Security Misconfiguration |194| upload, file, deserialization | A04:2021 — Insecure Design |195| api, endpoint, no auth | API2:2023 — Broken Authentication |196| crypto, hash, weak | A02:2021 — Cryptographic Failures |197198#### Remediation Summary table199200Columns: ID | Severity | Title | Priority | Effort | Fix (one-line)201202Assign priority based on severity:203- CRITICAL → P0 — Immediate204- HIGH → P0 — Immediate205- MEDIUM → P2 — Next sprint (or P1 — This sprint for the most impactful)206- LOW → P3 — Milestone or P4 — Backlog207- INFO → — (no priority)208209Effort: Low (config change, 1-liner fix), Medium (refactor needed), High (architecture change).210211#### Clean controls212213If `session.json` or the findings list contains explicit "tested clean" notes, include a "Controls Tested — No Issues Found" table. If no clean controls are documented, omit this section.214215---216217### Step 4 — Generate the PDF218219```bash220python3 report_generator.py221```222223If weasyprint is not installed: `pip install weasyprint` first.224225After generation, print:226```227Report generated:228 PDF → ./report_{target}_{date}.pdf229 HTML → ./report_{target}_{date}.html230 Findings: {N} total ({crit} critical, {high} high, {med} medium, {low} low, {info} info)231```232233---234235## Rules236237- **Never invent findings** — only include what is in `findings.json`238- **Business risk is mandatory for every finding** — derive from the actual finding; never write "could potentially" — write what an attacker concretely achieves239- **NP-IDs must be sequential** in severity order (NP-001 = most severe)240- **INFO findings** get a simplified card: Description + Business Risk + Recommendations only (no Evidence/Steps headers unless evidence is meaningful)241- **Logo path** is always `/Users/riccardo.tencate/Desktop/agent-smith/templates/FullLogo_Transparent.png`242- **Output filename**: `report_{target_slug}_{YYYY-MM-DD}.pdf` where target_slug = domain with dots replaced by underscores243- **weasyprint** is the only supported PDF engine — do not use pdfkit, xhtml2pdf, or headless Chrome244- Load `refs/style.md` before writing any HTML or CSS — never invent new colors or fonts