406 lines
17 KiB
Python
406 lines
17 KiB
Python
#!/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}")
|