From d0a71d02261aeb4104d915b5abbf95070ab1f323 Mon Sep 17 00:00:00 2001 From: m17hr1l Date: Fri, 22 May 2026 23:55:50 +0200 Subject: [PATCH] stage-24: indicator lookup page + blocklist download in cockpit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaces the stage-23 index in the UI. New /lookup page: paste any indicator (IP/domain/URL/hash/CVE) → red KNOWN-BAD banner with the matching cases/feeds/severities, or green clean banner. New /export/blocklist endpoint returns deduplicated plain-text indicator lists (all or high+ severity) for firewall/DNS/SIEM ingestion, linked from a download table on the lookup page. Lookup added to topbar nav. Verified live: lookup of a real corpus IP returns the OTX case; 8.8.8.8 returns clean; blocklist endpoint emits 26 high-severity IPs with a descriptive header line. Co-Authored-By: Claude Opus 4.7 --- src/psyc/cockpit/app.py | 30 +++++++++++- src/psyc/cockpit/static/cockpit.css | 11 +++++ src/psyc/cockpit/templates/base.html | 1 + src/psyc/cockpit/templates/lookup.html | 66 ++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/psyc/cockpit/templates/lookup.html diff --git a/src/psyc/cockpit/app.py b/src/psyc/cockpit/app.py index c2193c3..c6b955f 100644 --- a/src/psyc/cockpit/app.py +++ b/src/psyc/cockpit/app.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import List from fastapi import FastAPI, Form, HTTPException, Request -from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates @@ -14,6 +14,7 @@ from psyc import db, log from psyc.cockpit import inference, journey as journey_view from psyc.lines import courier as courier_line from psyc.lines import ledger as ledger_line +from psyc.lines import lookup as lookup_line from psyc.lines import route as route_line from psyc.lines import seal as seal_line from psyc.lines import train as train_line @@ -110,6 +111,33 @@ def inference_status() -> dict: return {"online": adapter is not None, "adapter": adapter} +@app.get("/lookup", response_class=HTMLResponse) +def lookup_view(request: Request, q: str = "") -> HTMLResponse: + query = q.strip() + matches = lookup_line.lookup(query) if query else [] + counts = {t: len(lookup_line.export_blocklist(t)) for t in lookup_line.IOC_TYPES} + return TEMPLATES.TemplateResponse( + request, + "lookup.html", + { + "query": query, + "matches": matches, + "searched": bool(query), + "total_iocs": db.ioc_count(), + "counts": counts, + }, + ) + + +@app.get("/export/blocklist", response_class=PlainTextResponse) +def export_blocklist(type: str = "ip", min_severity: str = "") -> PlainTextResponse: + if type not in lookup_line.IOC_TYPES: + raise HTTPException(status_code=400, detail=f"unknown type: {type}") + values = lookup_line.export_blocklist(type, min_severity or None) + header = f"# psyc blocklist — type={type} min_severity={min_severity or 'any'} count={len(values)}\n" + return PlainTextResponse(header + "\n".join(values) + "\n") + + @app.get("/queue", response_class=HTMLResponse) def queue_view(request: Request, status: str = "pending") -> HTMLResponse: from psyc.models import ApprovalStatus diff --git a/src/psyc/cockpit/static/cockpit.css b/src/psyc/cockpit/static/cockpit.css index a588400..df1e49d 100644 --- a/src/psyc/cockpit/static/cockpit.css +++ b/src/psyc/cockpit/static/cockpit.css @@ -314,3 +314,14 @@ tr.sev-low .sev-badge { color: var(--muted); } } .reject-reason::placeholder { color: var(--muted); } .outcome-pending_approval { background: rgba(251, 191, 36, 0.15); color: var(--amber); border: 1px solid rgba(251, 191, 36, 0.4); } + +/* ── indicator lookup ───────────────────────────────────────── */ +.lookup-form { display: flex; gap: 8px; margin: 14px 0 18px; } +.lookup-input { + flex: 1; background: var(--bg); color: var(--text); border: 1px solid var(--border); + border-radius: 4px; padding: 9px 12px; font: inherit; font-size: 14px; +} +.lookup-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); } +.verdict { padding: 12px 16px; border-radius: 6px; margin: 14px 0; font-size: 14px; } +.verdict-bad { background: rgba(248, 113, 113, 0.12); border: 1px solid var(--red); color: var(--red); } +.verdict-clean { background: rgba(74, 222, 128, 0.10); border: 1px solid var(--green); color: var(--green); } diff --git a/src/psyc/cockpit/templates/base.html b/src/psyc/cockpit/templates/base.html index c94114e..7db71ff 100644 --- a/src/psyc/cockpit/templates/base.html +++ b/src/psyc/cockpit/templates/base.html @@ -18,6 +18,7 @@