clawd/scripts/shannon-pptx.py

406 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Generate Shannon Security Assessment PowerPoint for inou.com"""
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
# Colors
BG_DARK = RGBColor(0x0F, 0x11, 0x17)
BG_CARD = RGBColor(0x1A, 0x1D, 0x26)
ACCENT_RED = RGBColor(0xFF, 0x4D, 0x4D)
ACCENT_ORANGE = RGBColor(0xFF, 0xA5, 0x00)
ACCENT_GREEN = RGBColor(0x4E, 0xC9, 0xB0)
ACCENT_BLUE = RGBColor(0x56, 0x9C, 0xD6)
TEXT_WHITE = RGBColor(0xF0, 0xF0, 0xF0)
TEXT_GRAY = RGBColor(0x99, 0x99, 0x99)
TEXT_DIM = RGBColor(0x66, 0x66, 0x66)
def set_slide_bg(slide, color=BG_DARK):
bg = slide.background
fill = bg.fill
fill.solid()
fill.fore_color.rgb = color
def add_text_box(slide, left, top, width, height, text, font_size=18,
color=TEXT_WHITE, bold=False, align=PP_ALIGN.LEFT, font_name="Segoe UI"):
txBox = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(height))
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.color.rgb = color
p.font.bold = bold
p.font.name = font_name
p.alignment = align
return txBox
def add_card(slide, left, top, width, height, color=BG_CARD):
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(left), Inches(top), Inches(width), Inches(height))
shape.fill.solid()
shape.fill.fore_color.rgb = color
shape.line.fill.background()
shape.shadow.inherit = False
return shape
def add_severity_badge(slide, left, top, severity, color):
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
Inches(left), Inches(top), Inches(1.8), Inches(0.4))
shape.fill.solid()
shape.fill.fore_color.rgb = color
shape.line.fill.background()
tf = shape.text_frame
tf.word_wrap = False
p = tf.paragraphs[0]
p.text = severity
p.font.size = Pt(14)
p.font.color.rgb = TEXT_WHITE
p.font.bold = True
p.font.name = "Segoe UI"
p.alignment = PP_ALIGN.CENTER
tf.paragraphs[0].space_before = Pt(2)
def add_bullet_list(slide, left, top, width, height, items, font_size=14, color=TEXT_WHITE):
txBox = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(height))
tf = txBox.text_frame
tf.word_wrap = True
for i, item in enumerate(items):
if i == 0:
p = tf.paragraphs[0]
else:
p = tf.add_paragraph()
p.text = item
p.font.size = Pt(font_size)
p.font.color.rgb = color
p.font.name = "Segoe UI"
p.space_after = Pt(6)
return txBox
# ============================================================
# SLIDE 1: Title
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6]) # Blank
set_slide_bg(slide)
add_text_box(slide, 1, 0.8, 11, 0.6, "🔐 SHANNON", 16, TEXT_DIM, font_name="Consolas")
add_text_box(slide, 1, 1.4, 11, 1.2, "Security Assessment Report", 48, TEXT_WHITE, bold=True)
add_text_box(slide, 1, 2.6, 11, 0.6, "inou.com — Penetration Test Results", 24, ACCENT_BLUE)
add_text_box(slide, 1, 4.0, 5, 0.4, "Date: February 14, 2026", 16, TEXT_GRAY)
add_text_box(slide, 1, 4.5, 5, 0.4, "Tool: Shannon Lite v1.0.0 (Keygraph)", 16, TEXT_GRAY)
add_text_box(slide, 1, 5.0, 5, 0.4, "Model: Claude Sonnet 4.5 (Anthropic)", 16, TEXT_GRAY)
add_text_box(slide, 1, 5.5, 5, 0.4, "Runtime: ~1.5 hours", 16, TEXT_GRAY)
add_text_box(slide, 1, 6.0, 5, 0.4, "Scope: Auth, XSS, Injection, SSRF, AuthZ", 16, TEXT_GRAY)
# Decorative line
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
Inches(1), Inches(3.4), Inches(3), Pt(3))
shape.fill.solid()
shape.fill.fore_color.rgb = ACCENT_RED
shape.line.fill.background()
# ============================================================
# SLIDE 2: Executive Summary
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.6, "Executive Summary", 36, TEXT_WHITE, bold=True)
# Stats cards
for i, (num, label, color) in enumerate([
("4", "Confirmed\nVulnerabilities", ACCENT_RED),
("2", "Critical", ACCENT_RED),
("2", "High", ACCENT_ORANGE),
("3", "Out of Scope\n(Need Internal)", TEXT_GRAY),
]):
x = 0.8 + i * 3.1
add_card(slide, x, 1.3, 2.8, 1.8)
add_text_box(slide, x + 0.3, 1.4, 2.2, 0.9, num, 54, color, bold=True, align=PP_ALIGN.CENTER)
add_text_box(slide, x + 0.3, 2.2, 2.2, 0.7, label, 14, TEXT_GRAY, align=PP_ALIGN.CENTER)
# Summary table
add_card(slide, 0.8, 3.5, 11.7, 3.6)
headers = ["Category", "Findings", "Severity", "Status"]
cols = [1.0, 4.5, 8.5, 10.5]
for j, h in enumerate(headers):
add_text_box(slide, cols[j], 3.6, 2.5, 0.4, h, 13, ACCENT_BLUE, bold=True)
rows = [
("Authentication", "4 vulnerabilities", "2× CRITICAL, 2× HIGH", "⚠️ Exploited"),
("Authorization", "0 vulnerabilities", "", "✅ Passed"),
("XSS", "2 code-level issues", "Requires auth", "🔒 Out of scope"),
("Injection", "1 path traversal", "Requires server access", "🔒 Out of scope"),
("SSRF", "0 vulnerabilities", "", "✅ Passed"),
]
for i, (cat, finding, sev, status) in enumerate(rows):
y = 4.2 + i * 0.55
color = ACCENT_RED if "Exploited" in status else ACCENT_GREEN if "Passed" in status else TEXT_GRAY
add_text_box(slide, cols[0], y, 3, 0.4, cat, 13, TEXT_WHITE, bold=True)
add_text_box(slide, cols[1], y, 3.5, 0.4, finding, 13, TEXT_WHITE)
add_text_box(slide, cols[2], y, 2, 0.4, sev, 13, color)
add_text_box(slide, cols[3], y, 2, 0.4, status, 13, color)
# ============================================================
# SLIDE 3: CRITICAL — Hardcoded Backdoor
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.5, "AUTH-VULN-07", 14, TEXT_DIM, font_name="Consolas")
add_text_box(slide, 0.8, 0.8, 11, 0.7, "Universal Authentication Bypass via\nHardcoded Backdoor Code", 32, TEXT_WHITE, bold=True)
add_severity_badge(slide, 0.8, 1.7, "🔴 CRITICAL", ACCENT_RED)
add_card(slide, 0.8, 2.3, 5.5, 4.5)
add_text_box(slide, 1.1, 2.4, 5, 0.4, "What Shannon Found", 16, ACCENT_BLUE, bold=True)
add_bullet_list(slide, 1.1, 2.9, 5, 3.5, [
"• Hardcoded verification code: 250365",
"• Works for ANY email address — instant account takeover",
"• Bypasses email ownership verification completely",
"• Affects both web (/verify) and mobile API",
"• Source comment: \"TODO: Remove backdoor code 250365",
" before production\"",
"",
"• HIPAA violation: unauthorized access to PHI",
], 13, TEXT_WHITE)
add_card(slide, 6.6, 2.3, 5.9, 4.5)
add_text_box(slide, 6.9, 2.4, 5.4, 0.4, "Proof of Exploitation", 16, ACCENT_RED, bold=True)
add_text_box(slide, 6.9, 2.9, 5.4, 4,
"1. Navigate to inou.com/start\n"
"2. Enter ANY email address\n"
"3. Click \"Continue\"\n"
"4. Enter code: 250365\n"
"5. Full account access granted\n\n"
"Tested with:\n"
" pentest@example.com → ✅ authenticated\n"
" victim@example.com → ✅ authenticated\n\n"
"Code location:\n"
" lib/dbcore.go:347\n"
" if code != 250365 && ...",
13, TEXT_WHITE, font_name="Consolas")
# ============================================================
# SLIDE 4: CRITICAL — Session Hijacking
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.5, "AUTH-VULN-05", 14, TEXT_DIM, font_name="Consolas")
add_text_box(slide, 0.8, 0.8, 11, 0.7, "Session Hijacking via Missing\nServer-Side Invalidation", 32, TEXT_WHITE, bold=True)
add_severity_badge(slide, 0.8, 1.7, "🔴 CRITICAL", ACCENT_RED)
add_card(slide, 0.8, 2.3, 5.5, 4.5)
add_text_box(slide, 1.1, 2.4, 5, 0.4, "The Problem", 16, ACCENT_BLUE, bold=True)
add_bullet_list(slide, 1.1, 2.9, 5, 3.5, [
"• /logout only clears the client-side cookie",
"• Session token NOT invalidated server-side",
"• Stolen cookies remain valid INDEFINITELY",
"• No session timeout mechanism exists",
"• Mobile API has NO logout endpoint at all",
"• No audit log of active sessions",
"• No ability to revoke sessions remotely",
], 13, TEXT_WHITE)
add_card(slide, 6.6, 2.3, 5.9, 4.5)
add_text_box(slide, 6.9, 2.4, 5.4, 0.4, "Attack Flow", 16, ACCENT_RED, bold=True)
add_text_box(slide, 6.9, 2.9, 5.4, 4,
"1. Victim authenticates → cookie set\n"
" login=d74520ade621d4b8\n\n"
"2. Attacker captures cookie\n"
" (via XSS, MITM, physical access)\n\n"
"3. Victim logs out\n"
" → Cookie cleared client-side ONLY\n\n"
"4. Attacker injects stolen cookie\n"
" → Full access STILL WORKS ✅\n\n"
"5. Attacker has permanent access\n"
" → No expiration, no revocation",
13, TEXT_WHITE, font_name="Consolas")
# ============================================================
# SLIDE 5: HIGH — Session Fixation + Brute Force
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.6, "High Severity Findings", 36, TEXT_WHITE, bold=True)
# Left card: Session Fixation
add_card(slide, 0.8, 1.2, 5.8, 5.8)
add_severity_badge(slide, 1.1, 1.4, "🟠 HIGH", ACCENT_ORANGE)
add_text_box(slide, 1.1, 1.9, 5.2, 0.5, "AUTH-VULN-04: Session Fixation", 18, TEXT_WHITE, bold=True)
add_bullet_list(slide, 1.1, 2.5, 5.2, 4, [
"Session IDs (dossierID) not rotated after auth",
"",
"• Same dossierID reused across ALL logins",
"• DossierID is deterministic per email",
"• Attacker authenticates → captures ID",
"• Logs out → re-authenticates",
"• Session ID is IDENTICAL both times",
"",
"Proven: login=f4d22b2137cf536c",
"persisted across logout/re-login cycle",
"",
"Combined with missing invalidation →",
"predictable, permanent session tokens",
], 13, TEXT_WHITE)
# Right card: Brute Force
add_card(slide, 6.9, 1.2, 5.8, 5.8)
add_severity_badge(slide, 7.2, 1.4, "🟠 HIGH", ACCENT_ORANGE)
add_text_box(slide, 7.2, 1.9, 5.2, 0.5, "AUTH-VULN-01: Brute Force", 18, TEXT_WHITE, bold=True)
add_bullet_list(slide, 7.2, 2.5, 5.2, 4, [
"Zero rate limiting on /verify endpoint",
"",
"• No rate limiting (0 HTTP 429s)",
"• No account lockout mechanism",
"• No CAPTCHA after failed attempts",
"• No attempt tracking or monitoring",
"",
"20 rapid requests in 3.1 seconds",
"→ All returned HTTP 200",
"",
"Brute force time estimates:",
" Sequential: ~21 hours",
" 10 parallel: ~2.1 hours",
" 100 parallel: ~12 minutes",
], 13, TEXT_WHITE)
# ============================================================
# SLIDE 6: Out of Scope (Internal Access Required)
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.6, "Out of Scope — Require Internal Access", 36, TEXT_WHITE, bold=True)
add_text_box(slide, 0.8, 1.0, 11, 0.4, "Valid vulnerabilities in code, but exploitation requires auth or server access", 16, TEXT_GRAY)
# Card 1: Path Traversal
add_card(slide, 0.8, 1.6, 3.8, 5.2)
add_text_box(slide, 1.1, 1.7, 3.3, 0.4, "INJ-VULN-01", 12, TEXT_DIM, font_name="Consolas")
add_text_box(slide, 1.1, 2.0, 3.3, 0.5, "Path Traversal in\nFile Upload", 18, TEXT_WHITE, bold=True)
add_bullet_list(slide, 1.1, 2.7, 3.3, 3.8, [
"Sanitization on wrong variable",
"(fileName vs relPath)",
"",
"filepath.Base() applied to",
"fileName, NOT relPath",
"",
"Enables arbitrary file write:",
"• /etc/cron.d/ (persistence)",
"• Web shell placement",
"• Config overwrite",
"",
"upload.go:182-186, :451-462",
], 11, TEXT_WHITE)
# Card 2: DICOM XSS
add_card(slide, 4.9, 1.6, 3.8, 5.2)
add_text_box(slide, 5.2, 1.7, 3.3, 0.4, "XSS-VULN-01", 12, TEXT_DIM, font_name="Consolas")
add_text_box(slide, 5.2, 2.0, 3.3, 0.5, "DICOM Stored XSS\nvia SeriesDescription", 18, TEXT_WHITE, bold=True)
add_bullet_list(slide, 5.2, 2.7, 3.3, 3.8, [
"DICOM tag 0008,103E",
"stored without HTML encoding",
"",
"Rendered via innerHTML in",
"/api/series endpoint",
"",
"Attack: upload crafted DICOM",
"with XSS in SeriesDescription",
"",
"Blocked by: auth requirement",
"(needs valid login cookie)",
], 11, TEXT_WHITE)
# Card 3: LLM Prompt Injection XSS
add_card(slide, 9.0, 1.6, 3.8, 5.2)
add_text_box(slide, 9.3, 1.7, 3.3, 0.4, "XSS-VULN-02", 12, TEXT_DIM, font_name="Consolas")
add_text_box(slide, 9.3, 2.0, 3.3, 0.5, "LLM Prompt Injection\n→ Stored XSS", 18, TEXT_WHITE, bold=True)
add_bullet_list(slide, 9.3, 2.7, 3.3, 3.8, [
"Freeform tracker input passed",
"to Google Gemini LLM",
"",
"LLM output stored unsanitized",
"Rendered via insertAdjacentHTML",
"",
"Attack: prompt injection to",
"make LLM generate XSS payload",
"in tracker question field",
"",
"Blocked by: auth requirement",
"(needs valid login cookie)",
], 11, TEXT_WHITE)
# ============================================================
# SLIDE 7: What Passed
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.6, "What Passed ✅", 36, ACCENT_GREEN, bold=True)
for i, (title, detail) in enumerate([
("Authorization Controls",
"10 authorization candidates tested — ALL passed. RBAC correctly prevents cross-user access to dossiers and medical data. API layer properly enforces ownership checks."),
("SQL Injection",
"All database queries use parameterized statements. No injection vectors found in any endpoint. SQLite with AES-256-GCM encryption at rest."),
("Command Injection",
"No command injection vectors found. External binary calls properly sanitized."),
("SSRF",
"Initial finding reclassified as Open Redirect (HTTP 303 client-side redirect, not server-side fetch). No true SSRF present."),
("Network Hardening",
"Internal services (API on 8082, DICOM on 8765) properly bound to localhost only. Single public port (8443). No subdomains exposed."),
]):
y = 1.3 + i * 1.15
add_card(slide, 0.8, y, 11.7, 1.0)
add_text_box(slide, 1.1, y + 0.05, 2.8, 0.4, title, 16, ACCENT_GREEN, bold=True)
add_text_box(slide, 1.1, y + 0.45, 11, 0.5, detail, 12, TEXT_GRAY)
# ============================================================
# SLIDE 8: Recommendations
# ============================================================
slide = prs.slides.add_slide(prs.slide_layouts[6])
set_slide_bg(slide)
add_text_box(slide, 0.8, 0.4, 11, 0.6, "Remediation Priorities", 36, TEXT_WHITE, bold=True)
recs = [
("1", "IMMEDIATE", "Remove backdoor code 250365",
"lib/dbcore.go:347 — Delete the hardcoded bypass. Deploy today.", ACCENT_RED),
("2", "IMMEDIATE", "Implement server-side session invalidation",
"Add session revocation to /logout. Add session expiration (e.g., 24h). Add session management UI.", ACCENT_RED),
("3", "THIS WEEK", "Add rate limiting to /verify",
"Max 5 attempts per email per 15 minutes. Add CAPTCHA after 3 failures. Log failed attempts.", ACCENT_ORANGE),
("4", "THIS WEEK", "Rotate session IDs on authentication",
"Generate new dossierID or session token after each successful /verify. Invalidate old tokens.", ACCENT_ORANGE),
("5", "NEXT SPRINT", "Sanitize DICOM metadata + LLM output",
"HTML-encode SeriesDescription before rendering. Sanitize all LLM output before insertAdjacentHTML.", ACCENT_BLUE),
("6", "NEXT SPRINT", "Fix path traversal in upload.go",
"Apply filepath.Base() to relPath (not just fileName). Validate paths stay within upload directory.", ACCENT_BLUE),
("7", "HARDENING", "Add HSTS headers + restrict CORS",
"Strict-Transport-Security on all responses. Replace wildcard CORS with specific origins.", TEXT_GRAY),
]
for i, (num, timeline, title, detail, color) in enumerate(recs):
y = 1.2 + i * 0.85
add_card(slide, 0.8, y, 11.7, 0.75)
add_text_box(slide, 1.0, y + 0.05, 0.3, 0.35, num, 18, color, bold=True, font_name="Consolas")
add_text_box(slide, 1.5, y + 0.02, 1.6, 0.35, timeline, 11, color, bold=True)
add_text_box(slide, 3.2, y + 0.02, 4, 0.35, title, 14, TEXT_WHITE, bold=True)
add_text_box(slide, 3.2, y + 0.38, 9, 0.35, detail, 11, TEXT_GRAY)
# ============================================================
# Save
# ============================================================
out = "/home/johan/clawd/memory/shannon-scan-2026-02-14/inou-security-assessment-2026-02-14.pptx"
prs.save(out)
print(f"Saved: {out}")