back to projects
// case study — project 02
Live in production

Security Report Automator

Submit a domain, receive a professional penetration testing report in your inbox — fully automated, no manual analysis required. Under 90 seconds, end to end.

FastAPIGroq · Claude · GPT-4oWeasyPrintJinja2SupabaseResendn8ncrt.shShodanSSL LabsNVDHackerTargetHIBPPydantictenacitystructlogRailway
// problem & solution
The Problem

A basic security recon report takes a skilled analyst 3–6 hours — querying cert logs, running port scans, checking CVE databases, grading SSL configs, and writing it up. Most small businesses never get one because it's too expensive.

The Solution

A fully automated passive recon pipeline. Domain in → branded PDF in under 3 minutes. Six APIs run in parallel, findings are structured and scored, Groq AI writes the report in a pentester's voice, WeasyPrint renders it, Resend delivers it.

// how it works

The pipeline, end-to-end.

Eight stages. One submission. A pentest-grade PDF on the other side.

01

Submission

User submits domain + email via the branded frontend. FastAPI validates input, creates a Supabase job (status: queued), returns job_id + poll URL in <100ms. A BackgroundTask kicks off the async pipeline.

02

Parallel Recon

Six API modules fire concurrently — crt.sh, HackerTarget, Shodan, SSL Labs, NVD, HIBP. Each is fault-tolerant: one failure never blocks the others.

03

Aggregation + Score

Findings merged, deduplicated, structured into a typed AggregatedFindings object. Deterministic risk score (0–100) calculated from subdomain count, exposed services, CVE severity (CVSS-weighted), SSL grade, cert expiry, and breach exposure.

04

AI Report Writing

Structured findings JSON passed to Groq llama-3.3-70b-versatile (max 8,192 tokens) with a senior pentester system prompt. Forces a 6-section output: Exec Summary, Methodology, Risk Table, Detailed Findings, Recommendations, Technical Appendix.

05

PDF Rendering

Markdown → HTML via python-markdown → Jinja2 template → WeasyPrint PDF. Dark cyber design: Space Grotesk, JetBrains Mono, severity badges, CONFIDENTIAL headers, branded cover with risk score.

06

Email Delivery

Resend sends the PDF as a base64 attachment from noreply@kedrichai.xyz. Subject includes risk level + score. HTML body with color-coded risk badge. Non-blocking — job completes even if email fails.

07

Job Completion

Supabase record updated to completed with pdf_path, pdf_url, report_markdown, findings_json, email_sent. Status page polls every 8s and shows the live pipeline with per-stage indicators.

08

Slack Notification

n8n receives a webhook on submission, polls /jobs/{id} every 10s via Wait → HTTP → IF loop, then posts to #make-testing on completion — domain, risk score, subdomain count, CVE count, PDF link.

// watch it run

Domain in, report out.

The full pipeline in real time — from submission to the finished PDF landing in the inbox.

security-report-automator — demo
// the report

What lands in your inbox.

The PDF isn't a template fill-in. It's 11 structured sections written by the AI in a senior pentester's voice — backed by real evidence.

Report Sections
  1. 01Cover Page — domain, reference, date, classification, risk score, scan stats
  2. 02Executive Summary — 3–4 paragraphs for non-technical readers
  3. 03Assessment Scope and Methodology — what was scanned, how, limitations
  4. 04Risk Summary Table — all findings ranked by severity
  5. 05Detailed Findings — Description / Evidence / Business Impact / Remediation
  6. 06Priority Recommendations — every fix, prioritized
  7. 07Technical Appendix A — complete subdomain list
  8. 08Technical Appendix B — exposed services table (IP / Port / Product / Version)
  9. 09Technical Appendix C — CVE reference table (CVSS / Severity / Description)
  10. 10Technical Appendix D — SSL/TLS technical details
  11. 11Disclaimer — passive recon only, confidential, authorized recipient
Example Findings (sanitized)
HIGHCVSS 7.5

Exposed SSH Service on Port 22

Evidence: Shodan detected OpenSSH 7.4 on 203.0.113.45:22. Version is end-of-life and no longer receives security patches.
Impact: Brute-force attacks, credential stuffing, or exploitation of known OpenSSH CVEs could grant unauthorized shell access.
Fix: Upgrade to OpenSSH 9.x immediately. Restrict port 22 to known IP ranges via firewall. Enable key-based auth only.
MEDIUMRisk 6/10

SSL Certificate Expiring in 12 Days

Evidence: SSL Labs detected certificate issued to mail.example.com expiring 2026-06-09.
Impact: Certificate expiry causes browser warnings that destroy user trust and block automated services.
Fix: Renew within 48 hours. Implement automated renewal (Let's Encrypt + certbot).
// live demo

Scan a domain you own.

Submit a domain + email. In 45–90 seconds you'll get a real PDF report delivered to your inbox. Only scan domains you own or have authorization to test — every scan is logged.

app.kedrichai.xyzopen ↗
Open demo fullscreen
// engineering decisions

The trade-offs, owned.

Five calls that shaped the system more than the spec did.

Provider-agnostic AI layer

Built with an OpenAI-compatible SDK pattern from day one — Groq, Anthropic Claude, and OpenAI GPT-4o all swap via a single AI_PROVIDER env var, no code changes. Groq Llama 3.3 70B runs in production (free tier, 100k tokens/day, no credit card). Claude or GPT-4o can take over for higher-stakes runs in seconds — clients prefer the flexibility over vendor lock-in.

WeasyPrint over ReportLab

ReportLab makes you build the PDF in Python — paragraph here, table there. WeasyPrint takes HTML/CSS (which I already know) and renders to PDF. Matched the portfolio design system in hours, not days. Trade-off: requires Pango/Cairo/GLib system libs → custom Dockerfile on Railway. Worth it.

Sequential await over asyncio.gather()

Tasks are created concurrently but awaited sequentially. Each module handles its own exception without one failure cancelling others. gather(return_exceptions=True) would work but needs unwrapping — sequential await is cleaner for 5–6 tasks.

Deterministic risk score over ML

A formula-based score (subdomain count + service exposure + CVE severity + SSL grade) is reproducible, auditable, and explainable. "Your score went from 30 to 55 because Shodan found an exposed FTP service running vsftpd 2.3.4" is a sentence you can actually say. A black-box model score isn't.

Pydantic models as a hallucination fence

All findings are validated through Pydantic models before being serialized to JSON for the AI. The AI never receives malformed data — meaning it can't invent a CVE ID that wasn't in the NVD response, because the CVE list is finite and pre-validated. Garbage in, garbage out. Pydantic stops the garbage.

// notable

Things worth pointing out.

Passive only — legal by design

Every recon source is passive: certificate logs, public DNS, Shodan's already-crawled index, SSL Labs' public API. Zero packets are sent to the target domain. This legal boundary is stated in the PDF disclaimer, the email footer, and the frontend — and the system is architected around it, not retrofitted to claim it.

The prompt is a contract

The AI system prompt doesn't just set tone — it enforces structure. Exact section headings, exact column names, exact severity labels (CRITICAL/HIGH/MEDIUM/LOW/INFORMATIONAL), no placeholders allowed. The output is consistently parseable because the prompt treats the AI like a template engine with judgment, not a creative writer.

n8n as operator layer, not user layer

n8n isn't in the user-facing flow — it's the operator's nervous system. Monitors every job, catches failures, pings Slack on completion. Cleanly separates concerns: FastAPI handles the work, n8n handles observability.

// challenges

What broke. How I fixed it.

WeasyPrint on Windows (and Railway)

Needs Pango, Cairo, GLib — Linux system libraries that don't exist on Windows or Railway's default image. Debugged 'gobject-2.0-0 not found' locally before accepting that PDF testing had to happen on the deployed container. Dockerfile installs every required lib explicitly. libgdk-pixbuf2.0-0 was renamed to libgdk-pixbuf-xlib-2.0-0 in Debian trixie — caught that on first Railway build.

pydyf version conflict

WeasyPrint 62.3 shipped with pydyf 0.10.0. A newer pydyf (0.11+) removed transform() from its parent class, breaking WeasyPrint's PDF stream generation with AttributeError: 'super' object has no attribute 'transform'. Fix: pin pydyf==0.10.0 in requirements.txt.

Shodan free tier returns 403, not 401

Shodan's free key doesn't support hostname search — returns 403 "Requires membership". The original handler only caught 401, so the pipeline retried 3 times, waited 30 seconds, then failed. Fix: treat both 401 and 403 as graceful skip conditions.

BASE_URL env var misconfiguration

Railway's variable editor made it easy to paste "BASE_URL = https://..." (including the key name) as the value. This propagated into every generated PDF URL as a literal string. Added to the "screenshot before debugging" checklist.

Groq daily token limit

Free tier is 100k tokens/day. A full-scan report uses ~2,700 tokens (1,264 input + 1,544 output). About 37 reports/day before hitting the cap. Fine for portfolio demo; needs paid tier or scan deduplication for production.

// inside the build

Frontend, PDF, ops layer.

Every screen from the production system. Click any to expand.

Frontend scan submission form at app.kedrichai.xyz — domain input, email, scan depth
click to expand ⤢
Frontend scan submission form at app.kedrichai.xyz — domain input, email, scan depth
Live status page mid-scan — pipeline stages with active/done indicators
click to expand ⤢
Live status page mid-scan — pipeline stages with active/done indicators
Completed scan results — risk score, badge, subdomain/service/CVE counts, download button
click to expand ⤢
Completed scan results — risk score, badge, subdomain/service/CVE counts, download button
Generated PDF — branded cover page with domain, risk score, scan metadata, CONFIDENTIAL label
click to expand ⤢
Generated PDF — branded cover page with domain, risk score, scan metadata, CONFIDENTIAL label
PDF findings section — severity badge, evidence, business impact, remediation
click to expand ⤢
PDF findings section — severity badge, evidence, business impact, remediation
Resend delivery — email in inbox from noreply@kedrichai.xyz with PDF attached
click to expand ⤢
Resend delivery — email in inbox from noreply@kedrichai.xyz with PDF attached
n8n Slack alert — domain, risk score, CVE count, PDF link in #make-testing
click to expand ⤢
n8n Slack alert — domain, risk score, CVE count, PDF link in #make-testing
Supabase scan_jobs table — completed rows showing the status pipeline columns
click to expand ⤢
Supabase scan_jobs table — completed rows showing the status pipeline columns
n8n canvas — Webhook → HTTP Request → IF → Wait loop → Slack notification
click to expand ⤢
n8n canvas — Webhook → HTTP Request → IF → Wait loop → Slack notification
// tech breakdown

Each layer, its job.

FastAPI 0.115
API framework
Async routes, BackgroundTasks for non-blocking pipeline, StaticFiles serving the frontend.
Multi-provider LLM
AI report writer
Provider-agnostic by design — Groq Llama 3.3 70B, Anthropic Claude, and OpenAI GPT-4o all swap via a single AI_PROVIDER env var. Groq runs in production today (free tier, 100k tokens/day, no credit card). Claude or GPT-4o take over for higher-stakes runs without code changes.
WeasyPrint 62.3 + pydyf 0.10.0
PDF renderer
HTML/CSS → PDF. Custom Dockerfile installs Pango/Cairo/GLib system libs. pydyf pinned for compatibility.
Jinja2 3.1.4
Report templating
Single HTML template with risk-level context and the CSS design system injected at render time.
6 recon APIs
Passive recon
crt.sh (cert transparency), HackerTarget (DNS), Shodan (services + banners), SSL Labs (TLS grade), NVD (CVEs), HIBP (breach check). Each is independently fault-tolerant.
Supabase
Job database
PostgreSQL via REST SDK. Service-role key bypasses RLS. 15-column scan_jobs table with the full status pipeline.
Resend
Email delivery
Transactional email with PDF attachment. noreply@kedrichai.xyz via SPF + DKIM + DMARC DNS records.
n8n
Workflow automation
Self-hosted on Railway. Webhook → polling loop → Slack notification. Operator-layer, not user-layer.
Railway
Deployment
Dockerfile-based. Custom image installs WeasyPrint system deps. App runs at app.kedrichai.xyz.
tenacity
Retry logic
Exponential backoff on Shodan, NVD, Groq. Prevents cascading failures on transient API errors.
Pydantic v2
Validation
Domain normalization on input. Typed findings models prevent AI hallucinations from leaking into the PDF.
structlog
Observability
Structured JSON logs per pipeline stage. Every API call, token count, and failure logged with job_id.
// results

The numbers.

45–90s
Full scan time
20–35s
Quick scan time
~2.7k
Tokens per report
~$0.002
Cost per report
45–60 KB
PDF size
~37/day
Free-tier capacity
// at scale

What I'd do differently.

The current build runs free-tier everything. Here's where I'd invest first.

Persistent PDF storage

Save to Supabase Storage or S3. Railway's ephemeral filesystem resets on redeploy and loses all generated reports.

Scan result caching

Cache recon findings per domain with a 24-hour TTL. Running crt.sh + Shodan + SSL Labs on google.com 50 times is wasteful and burns rate limits.

Redis + Celery job queue

FastAPI BackgroundTasks works at low volume but doesn't survive restarts. Celery + Redis would handle retries, priority, and multi-worker scaling.

Per-user rate limiting + auth

Current API key is one shared secret. Production needs per-client keys, usage quotas, and a tenant model so one user can't exhaust the Groq quota for everyone.

Human-review gate for high-risk reports

Automated passive recon can surface false positives. A CRITICAL-rated report probably warrants a human analyst review before delivery to a paying client.

// let's build

Want a recon pipeline
for your security ops?

Book a free 30-minute call — or copy my email and reach out when you're ready. We'll scope what to automate first.

Book a Call