diff --git a/README.md b/README.md index a5fa2a9..644cc9a 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,21 @@ Four browser-based apps served from a central hub. Launch everything with one co OpenSearch Web UI -**Mantis** — ticket browser with threat modelling dashboard, disposition scoring, and known malicious IP tracking. +**Threat Model** — threat modelling dashboard with disposition scoring and known malicious IP tracking. -Mantis Web UI +Threat Model Web UI — overview +Threat Model Web UI — detail + +**Dashboard** — aggregated analytics dashboard. + +Dashboard Web UI | App | What it's for | |---|---| | **OpenSearch** | Cross-protocol IP activity matrix, per-protocol drill-down, inline enrichment | -| **Mantis** | Ticket browser with disposition scoring and known malicious IP tracking | +| **Threat Model** | Threat modelling dashboard with disposition scoring and known malicious IP tracking | +| **Dashboard** | Aggregated analytics dashboard | +| **Mantis Explorer** | Ticket browser and search across the PISCES ticket history | --- @@ -46,7 +53,7 @@ Four browser-based apps served from a central hub. Launch everything with one co **Setup** -| | | +| Guide | Description | |---|---| | [VM Setup](docs/vm-setup.md) | Create an Ubuntu VM and connect to the cyber range network | | [Getting Started](docs/getting-started.md) | Install, configure, and launch the toolkit on Ubuntu | @@ -54,7 +61,7 @@ Four browser-based apps served from a central hub. Launch everything with one co **Using the toolkit** -| | | +| Guide | Description | |---|---| | [Web UI Workflow](docs/workflow.md) | End-to-end triage walkthrough using the browser-based UI | | [CLI Workflow](docs/cli-workflow.md) | Terminal-based querier walkthrough — alerts, enrichment, filters, tickets | @@ -64,7 +71,7 @@ Four browser-based apps served from a central hub. Launch everything with one co **Reference** -| | | +| Guide | Description | |---|---| | [Advanced Usage](docs/advanced-usage.md) | Full CLI flag reference for all tools | | [MCP Server Reference](docs/mcp-servers.md) | Full tool reference for all three MCP servers | diff --git a/apps/dashboard_web/__init__.py b/apps/dashboard_web/__init__.py index e69de29..1a14eb6 100644 --- a/apps/dashboard_web/__init__.py +++ b/apps/dashboard_web/__init__.py @@ -0,0 +1,18 @@ +"""PISCES Dashboard web application package.""" + +from datetime import date + + +def safe_date_param(value: str) -> str: + """Validate a date query parameter, returning '' for invalid values. + + Prevents user-supplied strings from reaching exception messages + (reflected XSS via error rendering). + """ + v = value.strip() + if not v: + return "" + try: + return date.fromisoformat(v).isoformat() + except ValueError: + return "" diff --git a/apps/dashboard_web/app.py b/apps/dashboard_web/app.py index 23ec604..8b77813 100644 --- a/apps/dashboard_web/app.py +++ b/apps/dashboard_web/app.py @@ -41,11 +41,35 @@ def api_cache_clear(): dcache.invalidate() return "", 204 + @app.route("/api/dashboard/sensors") + def api_sensor_summary(): + """Sensor browser modal content — terms agg on host.name.""" + from apps.dashboard_web.opensearch.aggregations import agg_opensearch_sensors + + time_range = request.args.get("time_range", "now-24h") + data = agg_opensearch_sensors(time_range) + # Build bucket-like dicts matching the sensor_summary.html template + buckets = [ + {"key": label, "doc_count": count} + for label, count in zip(data["labels"], data["counts"]) + ] + current = [ + s.strip() + for s in request.args.get("sensor", "").split(",") + if s.strip() and s.strip().lower() != "all" + ] + return render_template("sensor_summary.html", buckets=buckets, current_sensors=current) + from apps.dashboard_web.mantis import bp as mantis_bp from apps.dashboard_web.opensearch import bp as opensearch_bp from apps.dashboard_web.overview import bp as overview_bp + from apps.dashboard_web.tickets import bp as tickets_bp - for bp in [overview_bp, opensearch_bp, mantis_bp]: + for bp in [overview_bp, opensearch_bp, mantis_bp, tickets_bp]: app.register_blueprint(bp) + from apps.shared.blueprints import make_shared_static_blueprint + + app.register_blueprint(make_shared_static_blueprint()) + return app diff --git a/apps/dashboard_web/mantis/__init__.py b/apps/dashboard_web/mantis/__init__.py index 27b8b40..fc229d8 100644 --- a/apps/dashboard_web/mantis/__init__.py +++ b/apps/dashboard_web/mantis/__init__.py @@ -1,9 +1,9 @@ from flask import Blueprint, render_template, request from apps.dashboard_web import cache as dcache +from apps.dashboard_web import safe_date_param from apps.dashboard_web.mantis.aggregations import ( agg_mantis_attack_types, - agg_mantis_blocklists, agg_mantis_infra_count, agg_mantis_timeline, agg_mantis_top_ips, @@ -14,20 +14,21 @@ @bp.route("/api/dashboard/mantis") def section(): - time_range = request.args.get("time_range", "now-24h") - cached = dcache.get("mantis", {"time_range": time_range}) + since = safe_date_param(request.args.get("since", "")) + until = safe_date_param(request.args.get("until", "")) + cache_key = {"since": since, "until": until} + cached = dcache.get("mantis", cache_key) if cached is not None: return cached try: data = { - "attack_types": agg_mantis_attack_types(), - "timeline": agg_mantis_timeline(), - "blocklists": agg_mantis_blocklists(), - "top_ips": agg_mantis_top_ips(), + "attack_types": agg_mantis_attack_types(since, until), + "timeline": agg_mantis_timeline(since, until), + "top_ips": agg_mantis_top_ips(since, until), "infra_count": agg_mantis_infra_count(), } except Exception as exc: data = {"error": str(exc)} - html = render_template("mantis/section.html", data=data, time_range=time_range) - dcache.put("mantis", {"time_range": time_range}, html) + html = render_template("mantis/section.html", data=data) + dcache.put("mantis", cache_key, html) return html diff --git a/apps/dashboard_web/mantis/aggregations.py b/apps/dashboard_web/mantis/aggregations.py index 022c45d..985d3e9 100644 --- a/apps/dashboard_web/mantis/aggregations.py +++ b/apps/dashboard_web/mantis/aggregations.py @@ -2,22 +2,42 @@ import collections -from apps.mantis_web.data import INFRA_ROWS, MALICIOUS_ROWS, _raw_tickets +from apps.threat_model.data import INFRA_ROWS, MALICIOUS_ROWS, _raw_tickets -def agg_mantis_attack_types() -> list: +def _filter_tickets(since: str, until: str) -> list: + """Filter _raw_tickets by created_at date range.""" + tickets = _raw_tickets + if since: + tickets = [t for t in tickets if (t.get("created_at", "") or "") >= since] + if until: + tickets = [t for t in tickets if (t.get("created_at", "") or "") <= until + "T23:59:59"] + return tickets + + +def _filter_malicious(since: str, until: str) -> list: + """Filter MALICIOUS_ROWS by last_seen date range.""" + rows = MALICIOUS_ROWS + if since: + rows = [r for r in rows if (r.get("last_seen", "") or "") >= since] + if until: + rows = [r for r in rows if (r.get("first_seen", "") or "") <= until] + return rows + + +def agg_mantis_attack_types(since: str = "", until: str = "") -> list: """Counter over attack_types in MALICIOUS_ROWS.""" counter = collections.Counter() - for row in MALICIOUS_ROWS: + for row in _filter_malicious(since, until): for at in row.get("attack_types", []): counter[at] += 1 return [{"name": k.replace("_", " ").title(), "value": v} for k, v in counter.most_common()] -def agg_mantis_timeline() -> dict: +def agg_mantis_timeline(since: str = "", until: str = "") -> dict: """Monthly ticket volume from _raw_tickets.""" counter = collections.Counter() - for t in _raw_tickets: + for t in _filter_tickets(since, until): created = t.get("created_at", "") if created and len(created) >= 7: counter[created[:7]] += 1 @@ -25,22 +45,9 @@ def agg_mantis_timeline() -> dict: return {"months": months, "counts": [counter[m] for m in months]} -def agg_mantis_blocklists() -> dict: - """Counter over blocklists in MALICIOUS_ROWS.""" - counter = collections.Counter() - for row in MALICIOUS_ROWS: - for bl in row.get("blocklists", []): - counter[bl] += 1 - items = counter.most_common() - return { - "labels": [k for k, _ in items], - "counts": [v for _, v in items], - } - - -def agg_mantis_top_ips(n: int = 10) -> list: +def agg_mantis_top_ips(since: str = "", until: str = "", n: int = 10) -> list: """Top N malicious IPs by ticket count.""" - return sorted(MALICIOUS_ROWS, key=lambda r: -r.get("tickets", 0))[:n] + return sorted(_filter_malicious(since, until), key=lambda r: -r.get("tickets", 0))[:n] def agg_mantis_infra_count() -> int: diff --git a/apps/dashboard_web/mantis/templates/mantis/section.html b/apps/dashboard_web/mantis/templates/mantis/section.html index 89f1e5e..7cdf13c 100644 --- a/apps/dashboard_web/mantis/templates/mantis/section.html +++ b/apps/dashboard_web/mantis/templates/mantis/section.html @@ -3,9 +3,9 @@ {% else %}
-
+
Infrastructure IPs
-
{{ data.infra_count }}
+
{{ data.infra_count | intcomma }}
@@ -21,20 +21,11 @@
-
Blocklist Sources
- {% if not data.blocklists.labels %} -
No blocklist data
- {% else %} -
- {% endif %} -
- -
Ticket Volume Timeline (monthly)
{% if not data.timeline.months %}
No ticket data
{% else %} -
+
{% endif %}
@@ -79,17 +70,17 @@ + + + +
{% block content %}{% endblock content %}
+ + + + +{% endif %} diff --git a/apps/hub/app.py b/apps/hub/app.py index b3dcf59..cf4eac4 100644 --- a/apps/hub/app.py +++ b/apps/hub/app.py @@ -1,13 +1,123 @@ """Flask application factory for the PISCES Hub portal.""" +import json +import os +import subprocess +import time + from flask import Flask, render_template +def _read_version() -> str: + """Read version from pyproject.toml without requiring package installation.""" + toml = os.path.join(os.path.dirname(__file__), "..", "..", "pyproject.toml") + try: + with open(toml) as f: + for line in f: + if line.startswith("version"): + return line.split('"')[1] + except OSError: + pass + return "unknown" + + +def _git_update_info() -> dict: + """Fetch origin and return commits-behind count for the current branch. + + Only compares HEAD against origin/ so the count is + always meaningful. Runs once at startup. + """ + repo = os.path.join(os.path.dirname(__file__), "..", "..") + info: dict = {"branch": "unknown", "behind": None} + try: + subprocess.run( + ["git", "fetch", "origin"], + cwd=repo, + capture_output=True, + timeout=10, + ) + branch = subprocess.check_output( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + cwd=repo, + text=True, + timeout=5, + ).strip() + info["branch"] = branch + result = subprocess.check_output( + ["git", "rev-list", f"HEAD..origin/{branch}", "--count"], + cwd=repo, + text=True, + timeout=5, + ).strip() + info["behind"] = int(result) + except (subprocess.SubprocessError, OSError, ValueError): + pass + return info + + +_VERSION = _read_version() +_GIT_INFO = _git_update_info() + +_DATA = os.path.join(os.path.dirname(__file__), "..", "..", "data", "tickets") +_INDEX = os.path.join(_DATA, "indexed", "tickets_index.json") +_ENRICHED = os.path.join(_DATA, "enriched") + +_ENRICHED_FILES = { + "malicious": "malicious_ips.json", + "false_positive": "false_positive_ips.json", + "infrastructure": "known_infra_ips.json", + "undetermined": "undetermined_ips.json", +} + + +def _count_json(path: str) -> int: + """Return len() of a JSON array file, or 0 if missing.""" + try: + with open(path) as f: + return len(json.load(f)) + except (OSError, json.JSONDecodeError): + return 0 + + +def _age_str(path: str) -> str: + """Return human-readable age of a file, e.g. '2h ago' or 'N/A'.""" + try: + delta = time.time() - os.path.getmtime(path) + except OSError: + return "N/A" + if delta < 60: + return "just now" + if delta < 3600: + return f"{int(delta // 60)}m ago" + if delta < 86400: + return f"{int(delta // 3600)}h ago" + return f"{int(delta // 86400)}d ago" + + +def _gather_stats() -> dict: + """Read local data files for hub stats. No API calls.""" + counts = {k: _count_json(os.path.join(_ENRICHED, v)) for k, v in _ENRICHED_FILES.items()} + counts["tickets"] = _count_json(_INDEX) + return { + "counts": counts, + "index_age": _age_str(_INDEX), + "model_age": _age_str(os.path.join(_ENRICHED, "malicious_ips.json")), + } + + def create_app() -> Flask: app = Flask(__name__, static_folder="static", template_folder="templates") + from apps.shared.blueprints import make_shared_static_blueprint + + app.register_blueprint(make_shared_static_blueprint()) + @app.route("/") def index(): - return render_template("index.html") + return render_template("index.html", stats=_gather_stats(), version=_VERSION, git=_GIT_INFO) + + @app.route("/settings") + def settings(): + return render_template("settings.html") return app diff --git a/apps/hub/static/pisces-logo.ico b/apps/hub/static/pisces-logo.ico deleted file mode 100644 index dd66c23..0000000 Binary files a/apps/hub/static/pisces-logo.ico and /dev/null differ diff --git a/apps/hub/static/pisces-logo.png b/apps/hub/static/pisces-logo.png deleted file mode 100644 index 8980d6a..0000000 Binary files a/apps/hub/static/pisces-logo.png and /dev/null differ diff --git a/apps/hub/static/pisces.css b/apps/hub/static/pisces.css index a03c15f..c0b215c 100644 --- a/apps/hub/static/pisces.css +++ b/apps/hub/static/pisces.css @@ -1,167 +1,17 @@ /* ================================================================ - PISCES Web UI — Material Design 3 Dual-Theme SOC Analyst Console + PISCES Hub & Dashboard — App-specific styles + Requires: /shared/static/tokens.css, /shared/static/base.css ================================================================ */ -/* ── 1. Design Tokens ─────────────────────────────────── */ - -/* Dark theme (default) */ -[data-theme="dark"] { - /* MD3 surfaces (tonal elevation via colour, not shadow) */ - --surface: #0d0f14; - --surface-container-low: #161a24; - --surface-container: #1a1f2e; - --surface-container-high: #1e2433; - --surface-container-highest:#232840; - - /* Outline */ - --outline: #2a3045; - --outline-dim: #1e2233; - - /* Text */ - --on-surface: #c8ccd8; - --on-surface-dim: #5a6278; - - /* Primary */ - --primary: #4f8ef7; - --primary-dim: #3a7ae8; - - /* Secondary / Tertiary */ - --secondary: #7ec8e3; - --tertiary: #bc8cff; - - /* Semantic */ - --green: #3fb950; --yellow: #d29922; - --red: #f85149; --orange: #e3763c; --purple: #bc8cff; - - /* State layers */ - --state-hover: rgba(79,142,247,0.08); - --state-press: rgba(79,142,247,0.12); - - /* Badges (theme-aware) */ - --badge-green-bg: rgba(63,185,80,0.2); - --badge-red-bg: rgba(248,81,73,0.2); - --badge-yellow-bg: rgba(210,153,34,0.2); - --badge-blue-bg: rgba(79,142,247,0.2); - --badge-gray-bg: rgba(90,98,120,0.3); - - /* Focus ring */ - --focus-ring: rgba(79,142,247,0.25); - - /* Detail panel shadow */ - --panel-shadow: rgba(0,0,0,0.45); - --modal-backdrop: rgba(0,0,0,0.55); -} - -/* Light theme */ -[data-theme="light"] { - --surface: #f8f9fc; - --surface-container-low: #f0f1f5; - --surface-container: #e8eaef; - --surface-container-high: #e1e3e9; - --surface-container-highest:#d8dbe2; - - --outline: #c4c8d4; - --outline-dim: #dcdfe6; - - --on-surface: #1a1c24; - --on-surface-dim: #5c6070; - - --primary: #2563eb; - --primary-dim: #1d4fd8; - - --secondary: #0d7490; - --tertiary: #7c3aed; - - --green: #16a34a; --yellow: #ca8a04; - --red: #dc2626; --orange: #ea580c; --purple: #7c3aed; - - --state-hover: rgba(37,99,235,0.06); - --state-press: rgba(37,99,235,0.10); - - --badge-green-bg: rgba(22,163,74,0.12); - --badge-red-bg: rgba(220,38,38,0.12); - --badge-yellow-bg: rgba(202,138,4,0.12); - --badge-blue-bg: rgba(37,99,235,0.10); - --badge-gray-bg: rgba(92,96,112,0.12); - - --focus-ring: rgba(37,99,235,0.25); - - --panel-shadow: rgba(0,0,0,0.15); - --modal-backdrop: rgba(0,0,0,0.35); -} - -/* Legacy aliases — zero-breakage bridge from old var names */ -:root { - --bg: var(--surface); - --bg2: var(--surface-container-low); - --bg3: var(--surface-container-high); - --surface-1: var(--surface-container-low); - --surface-2: var(--surface-container-high); - --surface-3: var(--surface-container-highest); - --text: var(--on-surface); - --text-dim: var(--on-surface-dim); - --border: var(--outline); - --accent: var(--primary); - --accent2: var(--secondary); - - /* Shape */ - --radius-xs: 4px; - --radius-sm: 8px; - --radius-md: 12px; - --radius-pill: 28px; - - /* Typography */ - --font: "JetBrains Mono","Fira Mono","Courier New",monospace; - --sans: system-ui,-apple-system,sans-serif; -} - - -/* ── 2. Reset & Base ──────────────────────────────────── */ - -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -html { font-size: 14px; } +/* ── Layout ───────────────────────────────────────────── */ body { - background: var(--surface); - color: var(--on-surface); - font-family: var(--font); height: 100vh; overflow: hidden; display: flex; flex-direction: column; - line-height: 1.5; -} - -h1 { - font-family: var(--sans); - font-size: 1.2rem; - font-weight: 600; - color: var(--secondary); - margin-bottom: 0.3rem; } -h2 { - font-family: var(--sans); - font-size: 1rem; - font-weight: 600; - color: var(--on-surface); - margin-bottom: 0.5rem; -} - -.subtitle { - font-size: 0.8rem; - color: var(--on-surface-dim); - margin-bottom: 1rem; -} - -a { color: var(--primary); text-decoration: none; } -a:hover { text-decoration: underline; } -.ip-link { color: var(--secondary); font-weight: 600; } - - -/* ── 3. Layout ────────────────────────────────────────── */ - /* Nav */ nav { flex-shrink: 0; @@ -283,7 +133,6 @@ main { padding: 1.5rem; } -/* Table-fill mode: header/subtitle stay pinned, only the table scrolls */ main.table-fill { overflow-y: hidden; display: flex; @@ -296,125 +145,7 @@ main.table-fill > .table-wrap { } -/* ── 4. Buttons ───────────────────────────────────────── */ - -.btn { - position: relative; - font-family: var(--sans); - background: var(--surface-container-high); - border: 1px solid var(--outline); - color: var(--on-surface); - padding: 0 14px; - border-radius: var(--radius-pill); - cursor: pointer; - font-size: 0.82rem; - height: 32px; - text-decoration: none; - display: inline-flex; - align-items: center; - gap: 6px; - overflow: hidden; - transition: border-color 0.15s, color 0.15s; -} - -.btn::before { - content: ''; - position: absolute; - inset: 0; - background: transparent; - transition: background 0.15s; - pointer-events: none; -} - -.btn:hover::before { background: var(--state-hover); } -.btn:active::before { background: var(--state-press); } -.btn:hover { border-color: var(--primary); color: var(--primary); } - -.btn:disabled { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; -} - -.btn-primary { - background: var(--primary); - border-color: var(--primary); - color: #fff; -} -.btn-primary:hover { background: var(--primary-dim); border-color: var(--primary-dim); color: #fff; } -.btn-primary:hover::before { background: transparent; } - -.btn-sm { padding: 0 10px; font-size: 0.78rem; height: 26px; } - -/* Icon-only circle button */ -.btn-icon { - position: relative; - background: transparent; - border: none; - color: var(--on-surface-dim); - width: 28px; - height: 28px; - border-radius: 50%; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - overflow: hidden; - transition: color 0.15s; -} - -.btn-icon::before { - content: ''; - position: absolute; - inset: 0; - border-radius: 50%; - background: transparent; - transition: background 0.15s; - pointer-events: none; -} - -.btn-icon:hover::before { background: var(--state-hover); } -.btn-icon:active::before { background: var(--state-press); } -.btn-icon:hover { color: var(--primary); } - -/* Chevron rotates when expand button is active */ -.btn-icon.active { color: var(--primary); } -.btn-icon .fa-chevron-down { transition: transform 0.2s ease; display: block; } -.btn-icon.active .fa-chevron-down { transform: rotate(180deg); } - -/* Theme toggle button */ -.btn-theme { - position: relative; - background: transparent; - border: none; - color: var(--on-surface-dim); - width: 36px; - height: 36px; - border-radius: 50%; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - overflow: hidden; - transition: color 0.15s; - font-size: 1rem; -} -.btn-theme::before { - content: ''; - position: absolute; - inset: 0; - border-radius: 50%; - background: transparent; - transition: background 0.15s; - pointer-events: none; -} -.btn-theme:hover::before { background: var(--state-hover); } -.btn-theme:hover { color: var(--primary); } - - -/* ── 5. Tables ────────────────────────────────────────── */ +/* ── Tables ───────────────────────────────────────────── */ .table-wrap { overflow-x: auto; @@ -432,7 +163,6 @@ table { thead th:last-child, tbody td:last-child { border-right: 1px solid var(--outline); } -/* Column width classes — min-width hints, auto layout sizes to content */ .col-num { min-width: 42px; } .col-time { min-width: 120px; } .col-sensor { min-width: 80px; } @@ -444,7 +174,6 @@ tbody td:last-child { border-right: 1px solid var(--outline); } .col-freq { min-width: 52px; } .col-detail { min-width: 44px; } -/* Risk bar visual (3-block tier indicator) */ .risk-bar { display: inline-flex; gap: 2px; align-items: center; } .rb { width: 8px; height: 12px; border-radius: 2px; } .rb-low { background: var(--green); } @@ -467,7 +196,6 @@ thead th { user-select: none; } -/* Column resize handle */ .resize-handle { position: absolute; right: 0; @@ -524,7 +252,7 @@ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; } td.zero { color: var(--on-surface-dim); } -/* ── 6. Detail Panel ──────────────────────────────────── */ +/* ── Detail Panel ─────────────────────────────────────── */ tr.detail-row td { background: var(--surface-container-low); @@ -543,7 +271,6 @@ tr.detail-row td { .detail-grid .dk { color: var(--on-surface-dim); } .detail-grid .dv { color: var(--on-surface); word-break: break-all; } -/* Slide-in detail panel */ .detail-panel-slide { position: fixed; top: 0; right: 0; @@ -601,7 +328,6 @@ tr.detail-row td { gap: 12px; } -/* Propagate flex layout through the HTMX swap target div */ #detail-panel-content { display: flex; flex-direction: column; @@ -617,12 +343,10 @@ tr.detail-row td { margin-bottom: 12px; } -/* Stack enrich-cols vertically inside the narrow panel */ .detail-panel-slide .enrich-cols { grid-template-columns: 1fr; } -/* Ensure enrich columns stretch full width inside panel */ .detail-panel-slide .enrich-col { display: flex; flex-direction: column; @@ -633,9 +357,8 @@ tr.detail-row td { } -/* ── 7. Enrichment ────────────────────────────────────── */ +/* ── Enrichment ───────────────────────────────────────── */ -/* Side-by-side enrichment grid */ .enrich-cols { display: grid; grid-template-columns: 1fr 1fr; @@ -657,10 +380,8 @@ tr.detail-row td { gap: 6px; } -/* Legacy enrich triggers */ .enrich-triggers { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.6rem; } -/* Enrich card */ .enrich-card { border: 2px solid var(--outline); border-radius: var(--radius-md); @@ -722,22 +443,8 @@ tr.detail-row td { .cve-list { color: var(--red); } -.badge { - display: inline-block; - padding: 1px 7px; - border-radius: 10px; - font-family: var(--sans); - font-size: 0.75rem; - font-weight: 600; -} -.badge-green { background: var(--badge-green-bg); color: var(--green); } -.badge-red { background: var(--badge-red-bg); color: var(--red); } -.badge-yellow { background: var(--badge-yellow-bg); color: var(--yellow); } -.badge-blue { background: var(--badge-blue-bg); color: var(--primary); } -.badge-gray { background: var(--badge-gray-bg); color: var(--on-surface-dim); } - -/* ── 7b. Mantis Ticket Card ───────────────────────────── */ +/* ── Mantis Ticket Card ───────────────────────────────── */ .mantis-lookup-btns { display: flex; @@ -823,7 +530,7 @@ tr.detail-row td { } -/* ── 8. Filter Forms ──────────────────────────────────── */ +/* ── Filter Forms ─────────────────────────────────────── */ .filter-form { background: var(--surface-container-low); @@ -860,7 +567,6 @@ tr.detail-row td { box-shadow: 0 0 0 2px var(--focus-ring); } -/* Filter creation form (detail panel) */ .filter-action-row { padding-top: 8px; margin-top: 6px; @@ -911,13 +617,12 @@ tr.detail-row td { margin-top: 4px; } -/* Filter zone fills panel width */ [id^="filter-zone-"] { width: 100%; } -/* ── 9. Pagination ────────────────────────────────────── */ +/* ── Pagination ───────────────────────────────────────── */ .pagination-bar { display: flex; @@ -948,7 +653,7 @@ tr.detail-row td { } -/* ── 10. Modals ───────────────────────────────────────── */ +/* ── Modals ───────────────────────────────────────────── */ .modal-backdrop { display: none; @@ -1001,7 +706,6 @@ tr.detail-row td { padding: 6px 0; } -/* Notice summary rows */ .ns-meta { font-size: 0.75rem; color: var(--on-surface-dim); @@ -1055,7 +759,7 @@ tr.detail-row td { .ns-row:hover .ns-bar-fill { opacity: 1; } -/* ── 10b. Sensor Summary Modal ────────────────────────── */ +/* ── Sensor Summary Modal ─────────────────────────────── */ .ss-meta { font-family: var(--sans); font-size: 0.78rem; @@ -1098,17 +802,8 @@ tr.detail-row td { } -/* ── 11. HTMX Indicators ─────────────────────────────── */ +/* ── Spinner ──────────────────────────────────────────── */ -.htmx-indicator { - display: none; - font-size: 0.8rem; - color: var(--on-surface-dim); - padding: 4px 8px; -} -.htmx-request .htmx-indicator { display: inline; } - -/* Spinner in table body */ .loading-row td { text-align: center; color: var(--on-surface-dim); @@ -1116,40 +811,8 @@ tr.detail-row td { font-style: italic; } -/* Global HTMX progress bar */ -#htmx-bar { - position: fixed; - top: 0; left: 0; right: 0; - height: 3px; - z-index: 9999; - pointer-events: none; - overflow: hidden; -} -#htmx-bar::after { - content: ''; - display: block; - height: 100%; - width: 45%; - background: var(--primary); - border-radius: 0 2px 2px 0; - transform: translateX(-120%); -} -#htmx-bar.htmx-bar-active::after { - animation: htmx-sweep 1.4s ease-in-out infinite; -} -#htmx-bar.htmx-bar-done::after { - animation: none; - transform: translateX(280%); - transition: transform 0.25s ease, opacity 0.25s ease; - opacity: 0; -} -@keyframes htmx-sweep { - 0% { transform: translateX(-120%); } - 100% { transform: translateX(280%); } -} - -/* ── 12. IP Pivot ─────────────────────────────────────── */ +/* ── IP Pivot ─────────────────────────────────────────── */ .pivot-section { margin-bottom: 2rem; @@ -1175,10 +838,8 @@ tr.detail-row td { letter-spacing: 0.05em; } -.empty-note { color: var(--on-surface-dim); font-size: 0.82rem; font-style: italic; } - -/* ── 13. Utilities ────────────────────────────────────── */ +/* ── Utilities ────────────────────────────────────────── */ .page-header { display: flex; @@ -1193,7 +854,6 @@ tr.detail-row td { color: var(--on-surface-dim); } -/* Checkbox */ .check-label { display: flex; align-items: center; @@ -1202,21 +862,3 @@ tr.detail-row td { cursor: pointer; color: var(--on-surface); } - -/* Org identity icons */ -.org-icon { - font-size: 0.72rem; - margin-right: 3px; - opacity: 0.85; - vertical-align: middle; -} -.org-icon.org-cdn { color: var(--primary); } -.org-icon.org-cloud { color: var(--secondary); } -.org-icon.org-scanner { color: var(--yellow); } -.org-icon.org-research { color: var(--purple); } -.org-icon.org-private { color: var(--on-surface-dim); } - -/* Links */ -.mt-1 { margin-top: 0.5rem; } -.mt-2 { margin-top: 1rem; } -.mb-1 { margin-bottom: 0.5rem; } diff --git a/apps/hub/templates/index.html b/apps/hub/templates/index.html index cebb391..baf2924 100644 --- a/apps/hub/templates/index.html +++ b/apps/hub/templates/index.html @@ -1,130 +1,197 @@ - + PISCES Hub - - + + + +
-

PISCES Security Dashboard

+

PISCES Toolkit

Select an application to get started

-
- -
-
OpenSearch
-
Malcolm/Zeek log analysis across 10 protocols. Cross-protocol IP activity matrix, drill-downs, and enrichment.
+
+
+
Tickets Indexed
+
{{ stats.index_age }}
+
+
+
Threat Model Updated
+
{{ stats.model_age }}
+
+
+ +
- +
+ PISCES Toolkit v{{ version }} + {% if git.behind is not none %} + · + {% if git.behind == 0 %} + {{ git.branch }} up to date + {% else %} + {{ git.behind }} commit{{ 's' if git.behind != 1 }} behind {{ git.branch }} + {% endif %} + {% endif %} +
+ Made by Liam Dale +
+ diff --git a/apps/hub/templates/settings.html b/apps/hub/templates/settings.html new file mode 100644 index 0000000..47ddcc1 --- /dev/null +++ b/apps/hub/templates/settings.html @@ -0,0 +1,155 @@ + + + + + + PISCES · Settings + + + + + + + + + + + +
+

Settings

+

Preferences are saved locally and apply across all PISCES apps

+
+ +
+
+
+
+
Theme
+
Controls the colour scheme for all apps
+
+ +
+
+ + + + + diff --git a/apps/mantis_explorer/app.py b/apps/mantis_explorer/app.py index 8887d46..c0bfa42 100644 --- a/apps/mantis_explorer/app.py +++ b/apps/mantis_explorer/app.py @@ -21,6 +21,10 @@ def create_app() -> Flask: """Create and configure the Mantis Explorer Flask app.""" app = Flask(__name__, static_folder="static", template_folder="templates") + from apps.shared.blueprints import make_shared_static_blueprint + + app.register_blueprint(make_shared_static_blueprint()) + @app.context_processor def inject_globals() -> dict: return {"script_name": request.environ.get("SCRIPT_NAME", "")} diff --git a/apps/mantis_explorer/data.py b/apps/mantis_explorer/data.py index 2071498..982f9ed 100644 --- a/apps/mantis_explorer/data.py +++ b/apps/mantis_explorer/data.py @@ -10,12 +10,12 @@ _REPO = os.path.dirname(os.path.dirname(_HERE)) # --------------------------------------------------------------------------- -# Raw tickets — reuse mantis_web's already-parsed list in hub mode so we +# Raw tickets — reuse threat_model's already-parsed list in hub mode so we # don't parse the JSON twice. Falls back to loading independently when -# running standalone (mantis_web.data will load itself in that process). +# running standalone (threat_model.data will load itself in that process). # --------------------------------------------------------------------------- -from apps.mantis_web.data import TICKETS_BY_ID # noqa: E402, F401 -from apps.mantis_web.data import _raw_tickets as RAW_TICKETS # noqa: E402 +from apps.threat_model.data import TICKETS_BY_ID # noqa: E402, F401 +from apps.threat_model.data import _raw_tickets as RAW_TICKETS # noqa: E402 from src.mantis.activity_report import ( # noqa: E402 StudentStats, _filter_by_date_range, diff --git a/apps/mantis_explorer/static/me.css b/apps/mantis_explorer/static/me.css index 6efa9a7..79dcd6a 100644 --- a/apps/mantis_explorer/static/me.css +++ b/apps/mantis_explorer/static/me.css @@ -1,93 +1,16 @@ /* ================================================================ - Mantis Explorer — Material Design 3 Dark/Light Theme - Self-contained; does not depend on pisces.css + Mantis Explorer — App-specific styles + Requires: /shared/static/tokens.css, /shared/static/base.css ================================================================ */ -/* ── 1. Design Tokens ─────────────────────────────────── */ - -[data-theme="dark"] { - --surface: #0d0f14; - --surface-container-low: #161a24; - --surface-container: #1a1f2e; - --surface-container-high: #1e2433; - --surface-container-highest:#232840; - --outline: #2a3045; - --outline-dim: #1e2233; - --on-surface: #c8ccd8; - --on-surface-dim: #5a6278; - --primary: #4f8ef7; - --primary-dim: #3a7ae8; - --secondary: #7ec8e3; - --tertiary: #bc8cff; - --green: #3fb950; --yellow: #d29922; - --red: #f85149; --orange: #e3763c; --purple: #bc8cff; - --state-hover: rgba(79,142,247,0.08); - --badge-green-bg: rgba(63,185,80,0.2); - --badge-red-bg: rgba(248,81,73,0.2); - --badge-yellow-bg: rgba(210,153,34,0.2); - --badge-blue-bg: rgba(79,142,247,0.2); - --badge-gray-bg: rgba(90,98,120,0.3); - --focus-ring: rgba(79,142,247,0.25); - --panel-shadow: rgba(0,0,0,0.45); -} - -[data-theme="light"] { - --surface: #f8f9fc; - --surface-container-low: #f0f1f5; - --surface-container: #e8eaef; - --surface-container-high: #e1e3e9; - --surface-container-highest:#d8dbe2; - --outline: #c4c8d4; - --outline-dim: #dcdfe6; - --on-surface: #1a1c24; - --on-surface-dim: #5c6070; - --primary: #2563eb; - --primary-dim: #1d4fd8; - --secondary: #0d7490; - --tertiary: #7c3aed; - --green: #16a34a; --yellow: #ca8a04; - --red: #dc2626; --orange: #ea580c; --purple: #7c3aed; - --state-hover: rgba(37,99,235,0.06); - --badge-green-bg: rgba(22,163,74,0.12); - --badge-red-bg: rgba(220,38,38,0.12); - --badge-yellow-bg: rgba(202,138,4,0.12); - --badge-blue-bg: rgba(37,99,235,0.10); - --badge-gray-bg: rgba(92,96,112,0.12); - --focus-ring: rgba(37,99,235,0.25); - --panel-shadow: rgba(0,0,0,0.15); -} - -:root { - --radius-xs: 4px; - --radius-sm: 8px; - --radius-md: 12px; - --radius-pill: 28px; - - --font: "JetBrains Mono","Fira Mono","Courier New",monospace; - --sans: system-ui,-apple-system,sans-serif; -} - - -/* ── 2. Reset & Base ──────────────────────────────────── */ - -*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } - -html { font-size: 14px; } +/* ── Body layout ──────────────────────────────────────── */ body { - background: var(--surface); - color: var(--on-surface); - font-family: var(--font); min-height: 100vh; display: flex; flex-direction: column; - line-height: 1.5; } -a { color: var(--primary); text-decoration: none; } -a:hover { text-decoration: underline; } - - /* ── 3. Nav ───────────────────────────────────────────── */ nav { @@ -183,6 +106,28 @@ nav .spacer { flex: 1; } } .btn-icon:hover { color: var(--primary); text-decoration: none; } +.btn-notice { + background: transparent; + border: 1px solid var(--orange); + color: var(--orange); + font-family: var(--sans); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + padding: 4px 10px; + border-radius: 6px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 6px; + transition: background 0.15s, color 0.15s; +} +.btn-notice:hover { + background: var(--orange); + color: var(--surface); +} + /* ── 4. Main & Page Wrapper ───────────────────────────── */ @@ -261,8 +206,14 @@ main { transition: border-color 0.15s, box-shadow 0.15s; color-scheme: dark; } -[data-theme="light"] .me-filter-bar input[type="date"], -[data-theme="light"] .me-filter-bar input[type="text"] { +[data-theme="pisces-light"] .me-filter-bar input[type="date"], +[data-theme="pisces-light"] .me-filter-bar input[type="text"], +[data-theme="gruvbox-light"] .me-filter-bar input[type="date"], +[data-theme="gruvbox-light"] .me-filter-bar input[type="text"], +[data-theme="tokyonight-light"] .me-filter-bar input[type="date"], +[data-theme="tokyonight-light"] .me-filter-bar input[type="text"], +[data-theme="catppuccin-latte"] .me-filter-bar input[type="date"], +[data-theme="catppuccin-latte"] .me-filter-bar input[type="text"] { color-scheme: light; } @@ -534,29 +485,6 @@ td.name-cell { max-width: 240px; } } -/* ── 12. Buttons ──────────────────────────────────────── */ - -.btn { - font-family: var(--sans); - background: var(--surface-container-high); - border: 1px solid var(--outline); - color: var(--on-surface); - padding: 0 14px; - border-radius: var(--radius-pill); - cursor: pointer; - font-size: 0.82rem; - height: 30px; - display: inline-flex; - align-items: center; - gap: 6px; - text-decoration: none; - transition: border-color 0.15s, color 0.15s; - white-space: nowrap; -} -.btn:hover { border-color: var(--primary); color: var(--primary); text-decoration: none; } -.btn-sm { padding: 0 10px; font-size: 0.78rem; height: 26px; } - - /* ── 13. Student Slide Panel ──────────────────────────── */ .student-panel-backdrop { @@ -1060,42 +988,15 @@ tr.sp-ticket-row-active { box-shadow: inset 3px 0 0 var(--primary); } -/* ── 16. HTMX Loading Bar ─────────────────────────────── */ -#htmx-bar { - position: fixed; - top: 0; left: 0; right: 0; - height: 3px; - z-index: 9999; - pointer-events: none; - overflow: hidden; -} -#htmx-bar::after { - content: ''; - display: block; - height: 100%; - width: 45%; - background: var(--primary); - border-radius: 0 2px 2px 0; - transform: translateX(-120%); -} -#htmx-bar.htmx-bar-active::after { - animation: htmx-sweep 1.4s ease-in-out infinite; -} -#htmx-bar.htmx-bar-done::after { - animation: none; - transform: translateX(280%); - transition: transform 0.25s ease, opacity 0.25s ease; - opacity: 0; -} -@keyframes htmx-sweep { - 0% { transform: translateX(-120%); } - 100% { transform: translateX(280%); } -} - -.htmx-indicator { display: none; } -.htmx-request .htmx-indicator { display: inline; } +/* ── Spinner ──────────────────────────────────────────── */ +.loading-row td { + text-align: center; + color: var(--on-surface-dim); + padding: 24px; + font-style: italic; +} /* ── 17. Utilities ────────────────────────────────────── */ @@ -1106,3 +1007,80 @@ tr.sp-ticket-row-active { padding: 1.5rem; text-align: center; } + +/* ── 18. Notice Modal ─────────────────────────────────── */ + +.modal-backdrop { + position: fixed; + top: 0; left: 0; + width: 100vw; height: 100vh; + background: rgba(0,0,0,0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; + visibility: hidden; + opacity: 0; + transition: opacity 0.2s ease-in-out; + backdrop-filter: blur(4px); +} + +.modal-backdrop.show { + visibility: visible; + opacity: 1; +} + +.modal-content { + background: var(--surface-container-low); + border: 1px solid var(--outline); + border-top: 3px solid var(--orange); + border-radius: var(--radius-md); + padding: 1.75rem; + max-width: 550px; + width: 90%; + box-shadow: 0 24px 48px rgba(0,0,0,0.8); + display: flex; + flex-direction: column; + gap: 1.25rem; + position: relative; +} + +.modal-header { + font-family: var(--sans); + font-weight: 700; + font-size: 1.1rem; + color: var(--orange); + display: flex; + align-items: center; + gap: 12px; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.modal-body { + font-size: 0.9rem; + line-height: 1.6; + color: var(--on-surface); +} + +.modal-body p { margin-bottom: 0.75rem; } + +.modal-code-box { + background: var(--surface); + padding: 0.75rem 1rem; + border-radius: var(--radius-sm); + font-family: var(--font); + font-size: 0.78rem; + border: 1px solid var(--outline); + margin: 0.5rem 0; + color: var(--secondary); + white-space: nowrap; + overflow-x: auto; +} +.modal-code-box code { color: inherit; display: inline; } + +.modal-footer { + display: flex; + justify-content: flex-end; + margin-top: 0.5rem; +} diff --git a/apps/mantis_explorer/static/pisces-logo.ico b/apps/mantis_explorer/static/pisces-logo.ico deleted file mode 100644 index dd66c23..0000000 Binary files a/apps/mantis_explorer/static/pisces-logo.ico and /dev/null differ diff --git a/apps/mantis_explorer/static/pisces-logo.png b/apps/mantis_explorer/static/pisces-logo.png deleted file mode 100644 index 8980d6a..0000000 Binary files a/apps/mantis_explorer/static/pisces-logo.png and /dev/null differ diff --git a/apps/mantis_explorer/templates/base.html b/apps/mantis_explorer/templates/base.html index 2ca56bc..e0ec6c9 100644 --- a/apps/mantis_explorer/templates/base.html +++ b/apps/mantis_explorer/templates/base.html @@ -1,16 +1,13 @@ - + {% block title %}Mantis Explorer{% endblock title %} - - + + + +
@@ -80,25 +75,44 @@ +{# ── Notice modal ──────────────────────────────── #} + + + + + + - + PISCES OpenSearch @@ -63,10 +59,6 @@