#!/usr/bin/env python3
"""Basic HTTP-header assessment using DeepSeek V4.1 Flash via Strata.

Usage: python3 deepseek_security_demo.py https://example.com
Optional: STRATA_BASE_URL (defaults to http://127.0.0.1:8080/v1)
          STRATA_API_KEY  (for authenticated endpoints)
          STRATA_MODEL    (defaults to the first ID returned by /v1/models)

Uses Python standard library only. Not an active penetration test.
"""
import datetime as dt
import getpass
import html
import json
import os
import re
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
from urllib.parse import urlsplit

API_BASE = os.environ.get('STRATA_BASE_URL', 'http://127.0.0.1:8080/v1').rstrip('/')
CHECKS = [
    ('Strict-Transport-Security', 'HSTS', 'Enforces HTTPS connections'),
    ('Content-Security-Policy', 'Content Security Policy', 'Limits what resources can execute'),
    ('X-Content-Type-Options', 'Content Type Protection', 'Prevents content-type sniffing'),
    ('Referrer-Policy', 'Referrer Policy', 'Controls referrer data disclosure'),
    ('Permissions-Policy', 'Permissions Policy', 'Restricts browser capabilities'),
]

def fetch_headers(url):
    req = urllib.request.Request(url, method='HEAD', headers={'User-Agent': 'Mozilla/5.0 (compatible; LiveHeaderAudit/1.0)'})
    try:
        with urllib.request.urlopen(req, timeout=20) as res:
            return res.status, dict(res.headers.items()), res.url
    except urllib.error.HTTPError as err:
        if err.code in (405, 501):  # Some sites do not support HEAD.
            fallback = urllib.request.Request(
                url, method='GET', headers={'User-Agent': 'Mozilla/5.0 (compatible; LiveHeaderAudit/1.0)'}
            )
            with urllib.request.urlopen(fallback, timeout=20) as res:
                return res.status, dict(res.headers.items()), res.url
        return err.code, dict(err.headers.items()), url


def authorization_headers(api_key):
    return {'Authorization': f'Bearer {api_key}'} if api_key else {}


def model_name(api_key):
    configured = os.environ.get('STRATA_MODEL')
    if configured:
        return configured
    request = urllib.request.Request(
        API_BASE + '/models', headers=authorization_headers(api_key)
    )
    with urllib.request.urlopen(request, timeout=20) as response:
        models = json.load(response).get('data', [])
    if not models:
        raise ValueError('No model returned by /v1/models')
    return models[0]['id']


def ask_model(api_key, model, status, headers, url):
    lower = {k.lower(): v for k, v in headers.items()}
    selected = '\n'.join(
        f'{key}: {lower.get(key.lower(), "MISSING")[:120]}'
        for key, _, _ in CHECKS
    )
    prompt = (
        'Analyze only HTTP response headers for a basic defensive audit. '
        'Give 3 very short lines, each SEVERITY | Finding | Fix. '
        'SEVERITY must be HIGH, MEDIUM, or INFO. '
        'Discuss only facts supported by headers. Missing header is not proof of exploitable vulnerability. '
        'Each finding and fix must be 7 words or fewer. No markdown.\n'
        f'Website: {url}\nHTTP status: {status}\nHeaders:\n{selected}'
    )
    body = json.dumps({
        'model': model,
        'messages': [{'role': 'user', 'content': prompt}],
        'reasoning_effort': 'none',
        'max_tokens': 100,
        'stream': False,
    }).encode('utf-8')
    req = urllib.request.Request(API_BASE + '/chat/completions', data=body, headers={
        'Content-Type': 'application/json', **authorization_headers(api_key)
    }, method='POST')
    with urllib.request.urlopen(req, timeout=900) as res:
        reply = json.load(res)
    return reply['choices'][0]['message'].get('content', '').strip(), reply.get('usage', {})


def parse_findings(text):
    findings = []
    for line in text.splitlines():
        line = re.sub(r'^\s*[-*0-9.)]+\s*', '', line.strip())
        parts = [p.strip(' *') for p in line.split('|')]
        if len(parts) >= 3:
            sev = parts[0].upper()
            if sev not in {'HIGH', 'MEDIUM', 'INFO'}:
                sev = 'INFO'
            findings.append((sev, parts[1][:100], ' | '.join(parts[2:])[:180]))
    if findings:
        return findings[:3]
    # If the LLM ignores line formatting, still show the actual text instead of losing the demo.
    return [('INFO', 'DeepSeek assessment', (text or 'No assessment text returned.')[:900])]


def render(url, status, headers, findings, raw_answer, elapsed, model):
    low = {k.lower(): v for k, v in headers.items()}
    results = []
    for key, title, desc in CHECKS:
        exists = key.lower() in low
        results.append((title, desc, exists))
    score = int(100 * sum(ok for _, _, ok in results) / len(results))
    safe = lambda s: html.escape(str(s), quote=True)
    cards = ''.join(
        f'<div class="check"><div class="checkhead"><span>{safe(title)}</span>'
        f'<span class="pill {"pass" if present else "missing"}">{"PRESENT" if present else "MISSING"}</span></div>'
        f'<p>{safe(desc)}</p></div>'
        for title, desc, present in results
    )
    findings_html = ''.join(
        f'<div class="finding"><span class="severity {safe(s.lower())}">{safe(s)}</span>'
        f'<div><b>{safe(title)}</b><p>{safe(fix)}</p></div></div>'
        for s, title, fix in findings
    )
    header_rows = ''.join(
        f'<tr><td>{safe(k)}</td><td>{safe("[redacted]" if k.lower() in {"set-cookie", "authorization"} else v)}</td></tr>'
        for k, v in headers.items()
    )
    grade = 'good' if score >= 80 else 'caution' if score >= 40 else 'risk'
    timestamp = dt.datetime.now().astimezone().strftime('%b %d, %Y · %I:%M %p %Z')
    return f'''<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>DeepSeek / Live Security Audit</title>
<style>
*{{box-sizing:border-box}}body{{margin:0;background:#070c16;color:#e7f3ff;font-family:Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}}
body:before{{content:"";position:fixed;inset:0;pointer-events:none;background:radial-gradient(ellipse at 80% 0%,#123e47 0%,transparent 35%),radial-gradient(ellipse at 10% 100%,#182745 0%,transparent 35%);opacity:.9}}
main{{max-width:1150px;margin:auto;padding:42px 30px 80px;position:relative}}.top{{display:flex;justify-content:space-between;align-items:center;margin-bottom:46px;gap:16px}}
.brand{{font-weight:900;letter-spacing:3px;color:#b9ffdb;font-size:13px}}.live{{border:1px solid #1e7754;border-radius:40px;color:#5cffad;padding:8px 16px;font-size:12px;letter-spacing:1px;background:#0c261d}}
.live:before{{content:"● ";animation:blink 1.3s infinite}}@keyframes blink{{50%{{opacity:.25}}}}.eyebrow{{font-size:12px;text-transform:uppercase;letter-spacing:3px;color:#66dbaf}}
h1{{font-size:clamp(32px,5.2vw,64px);letter-spacing:-2.6px;line-height:1.05;margin:12px 0 14px}}.subtitle{{color:#94a8bb;font-size:16px;margin:0 0 35px;max-width:850px;overflow-wrap:anywhere}}
.hero{{display:grid;grid-template-columns:1fr 300px;gap:18px;margin-bottom:26px}}.panel{{background:#111a2a;border:1px solid #243449;border-radius:18px;padding:27px;box-shadow:0 12px 45px #0005}}
.statrow{{display:flex;gap:24px;flex-wrap:wrap}}.stat{{flex:1;min-width:120px}}.stat small{{color:#8ea4bc;font-size:12px;letter-spacing:1px}}.stat strong{{display:block;font-size:27px;margin-top:12px}}
.score{{font-size:80px;font-weight:900;line-height:1;color:#f1c76e}}.score.good{{color:#62efad}}.score.risk{{color:#ff7474}}.scorelabel{{color:#95a9b9;font-size:12px;letter-spacing:1.5px;margin:10px 0}}
.bar{{height:8px;background:#293443;border-radius:20px;overflow:hidden;margin:22px 0 0}}.bar span{{display:block;height:100%;background:linear-gradient(90deg,#f47772,#edc66f,#62efad);width:{score}%}}
.columns{{display:grid;grid-template-columns:1fr 1fr;gap:18px}}h2{{font-size:16px;letter-spacing:1px;margin:0 0 25px;color:#dbe8f3}}h2 span{{color:#66dbaf}}
.check{{border-top:1px solid #26364b;padding:19px 0 15px}}.checkhead{{display:flex;align-items:center;justify-content:space-between;gap:10px;font-weight:700;font-size:14px}}
.pill,.severity{{display:inline-block;font-size:10px;letter-spacing:1px;border-radius:5px;padding:7px 9px;font-weight:800}}.pass,.info{{color:#79f3bb;background:#133b31}}.missing,.medium{{color:#f6c56e;background:#45351f}}.high{{background:#522b33;color:#ff8e9b}}
p{{color:#91a6bd;font-size:13px;line-height:1.6;margin:7px 0 0}}.finding{{display:flex;gap:15px;border-top:1px solid #26364b;padding:19px 0;align-items:start}}.finding b{{font-size:15px}}
.foot{{margin-top:25px;display:flex;justify-content:space-between;gap:15px;color:#768ba1;font-size:12px;flex-wrap:wrap}}details{{margin-top:20px}}summary{{cursor:pointer;color:#b9ffdb}}table{{margin-top:15px;border-collapse:collapse;width:100%;table-layout:fixed}}td{{border-bottom:1px solid #26364b;padding:12px 6px;overflow-wrap:anywhere;font-size:12px;color:#b4c5d5}}td:first-child{{width:30%;color:#65dbaa}}
.notice{{padding:15px 18px;margin-top:22px;border-left:3px solid #f1c76e;background:#1e2531;color:#bbc9d8;font-size:12px;line-height:1.6}}
@media(max-width:760px){{.hero,.columns{{grid-template-columns:1fr}}.score{{font-size:60px}}main{{padding:30px 15px}}}}
</style></head><body><main>
<div class="top"><div class="brand">◆ DEEPSEEK V4.1 FLASH <span style="color:#61839b"> / STRATA</span></div><div class="live">AUDIT COMPLETE</div></div>
<div class="eyebrow">REAL WEBSITE DATA · AI-ASSISTED REVIEW</div><h1>Security Header Audit</h1><p class="subtitle">{safe(url)}</p>
<div class="hero"><div class="panel"><h2><span>01 /</span> LIVE TARGET</h2><div class="statrow"><div class="stat"><small>HTTP STATUS</small><strong>{status}</strong></div><div class="stat"><small>HEADERS INSPECTED</small><strong>{len(headers)}</strong></div><div class="stat"><small>AI ENGINE</small><strong>{safe(model)}</strong></div></div><div class="foot"><span>{safe(timestamp)}</span><span>Inference {elapsed:.0f}s</span></div></div>
<div class="panel"><div class="score {grade}">{score}<span style="font-size:25px">/100</span></div><div class="scorelabel">HEADER PRESENCE SCORE</div><div class="bar"><span></span></div></div></div>
<div class="columns"><section class="panel"><h2><span>02 /</span> SECURITY CONTROLS</h2>{cards}</section><section class="panel"><h2><span>03 /</span> DEEPSEEK FINDINGS</h2>{findings_html}</section></div>
<div class="notice">Scope: basic HTTP response-header checks only. The score measures presence of five defensive headers, not overall website security. Missing headers do not establish an exploitable vulnerability. No active penetration test was performed.</div>
<details class="panel"><summary>Show raw HTTP response headers</summary><table>{header_rows}</table></details>
<div class="foot"><span>GENERATED FROM LIVE HEADERS + STRATA MODEL</span><span>DEMO / DEEPSEEK V4.1 FLASH</span></div>
</main></body></html>'''


def main():
    url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
    if not url.startswith(('http://', 'https://')):
        url = 'https://' + url
    if not urlsplit(url).netloc:
        sys.exit('Invalid website URL')
    if not API_BASE.startswith(('http://127.0.0.1:', 'http://localhost:', 'https://')):
        sys.exit('Remote APIs must use HTTPS or a local SSH tunnel.')
    key = os.environ.get('STRATA_API_KEY', '')
    if API_BASE.startswith('https://') and not key:
        key = getpass.getpass('Strata/gateway API key (not RunPod account key): ')
    try:
        model = model_name(key)
    except Exception as exc:
        sys.exit(f'Cannot reach model at {API_BASE}/models: {exc}')
    print(f'Using Strata model: {model}', flush=True)
    print(f'1/3 Fetching HTTP headers from {url} ...', flush=True)
    try:
        status, headers, final_url = fetch_headers(url)
    except Exception as exc:
        sys.exit(f'Website request failed: {exc}')
    print(f'    HTTP {status}; {len(headers)} headers received.', flush=True)
    print('2/3 DeepSeek is assessing headers through Strata ...', flush=True)
    begin = time.monotonic()
    try:
        response, usage = ask_model(key, model, status, headers, final_url)
    except urllib.error.HTTPError as exc:
        sys.exit(f'Model request failed (HTTP {exc.code}): {exc.read(400).decode(errors="replace")}')
    except Exception as exc:
        sys.exit(f'Model request failed: {exc}')
    elapsed = time.monotonic() - begin
    findings = parse_findings(response)
    print(f'    Received {len(findings)} finding(s) in {elapsed:.1f}s. Output tokens: {usage.get("completion_tokens", "?")}', flush=True)
    output = Path.cwd() / 'deepseek-security-audit.html'
    output.write_text(render(final_url, status, headers, findings, response, elapsed, model), encoding='utf-8')
    print(f'3/3 Report created: {output}', flush=True)
    if sys.platform == 'darwin':
        subprocess.run(['open', str(output)], check=False)
    else:
        print('Open the HTML report in your browser.')

if __name__ == '__main__':
    main()
