feat(roadmap): YAML-driven roadmap + Tailwind UI w/ filters & details modal
- Convert roadmap to YAML: - Add data structure: id, priority, title, goal, tags, milestone - Add `details` field (supports list or block string); populated initial content - Quote scalars and use explicit nulls to avoid YAML parse edge cases - Update `updated` date to 2025-08-22 - Flask blueprint + loader: - New /roadmap view with section switching (roadmap | backlog | open_questions) - Filters: q (search), tag (multi, AND), min_priority, milestone - Dataclasses: RoadmapData/RoadmapItem; include `details` - `_normalize_details()` to accept string or list, normalize to list[str] - Configurable path via `ROADMAP_FILE` (env or defaults) - Remove cache layer for simplicity - UI (Tailwind): - `templates/roadmap.html` with responsive cards, tag chips, and filter form - Details modal (larger max width, scrollable body) showing ID/goal/priority/tags/milestone - Safe JSON payload to modal via `|tojson|forceescape` - JS: - DOM-ready, event-delegated handler for `data-item` buttons - Populate modal fields and render multi-paragraph details - Fixes & polish: - Resolved YAML `ScannerError` by quoting strings with `:` and `#` - Ensured `details` is passed through route to template and included in button payload - Minor styling tweaks for consistency with Tailwind setup Usage: - Set `ROADMAP_FILE` if not using default path - Visit /roadmap and filter via q/tag/min_priority/milestone
This commit is contained in:
@@ -7,8 +7,9 @@ from flask import Flask
|
||||
from .utils.settings import get_settings
|
||||
from .logging_setup import wire_logging_once, get_app_logger, get_engine_logger
|
||||
|
||||
from app.blueprints import ui # ui blueprint
|
||||
from app.blueprints import api # api blueprint
|
||||
from app.blueprints.ui import bp as main_bp # ui blueprint
|
||||
from app.blueprints.api import api_bp as api_bp # api blueprint
|
||||
from app.blueprints.roadmap import bp as roadmap_bp # roadmap
|
||||
|
||||
def create_app() -> Flask:
|
||||
"""
|
||||
@@ -41,9 +42,14 @@ def create_app() -> Flask:
|
||||
app.config["APP_NAME"] = settings.app.name
|
||||
app.config["APP_VERSION"] = f"v{settings.app.version_major}.{settings.app.version_minor}"
|
||||
|
||||
# roadmap file
|
||||
app.config["ROADMAP_FILE"] = str(Path(app.root_path) / "docs" / "roadmap.yaml")
|
||||
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(ui.bp)
|
||||
app.register_blueprint(api.api_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(api_bp)
|
||||
app.register_blueprint(roadmap_bp)
|
||||
|
||||
app_logger = get_app_logger()
|
||||
|
||||
|
||||
173
app/blueprints/roadmap.py
Normal file
173
app/blueprints/roadmap.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Roadmap view: loads data/roadmap.yaml, sorts and renders with filters.
|
||||
|
||||
Query params (all optional):
|
||||
- q=... (substring search over title/goal/id)
|
||||
- tag=tag1&tag=tag2 (multi; include if item has ALL selected tags)
|
||||
- min_priority=1..9 (int; keep items with priority >= this)
|
||||
- milestone=v0.2 (string; exact match on milestone)
|
||||
- section=roadmap|backlog|open_questions (default=roadmap)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import time
|
||||
import yaml
|
||||
from flask import Blueprint, render_template, request, abort, current_app
|
||||
|
||||
from app.logging_setup import get_app_logger
|
||||
logger = get_app_logger()
|
||||
|
||||
bp = Blueprint("roadmap", __name__)
|
||||
|
||||
@dataclass
|
||||
class RoadmapItem:
|
||||
id: str
|
||||
title: str
|
||||
goal: str
|
||||
tags: List[str] = field(default_factory=list)
|
||||
priority: Optional[int] = None
|
||||
milestone: Optional[str] = None
|
||||
details: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoadmapData:
|
||||
updated: Optional[str]
|
||||
roadmap: List[RoadmapItem]
|
||||
backlog: List[RoadmapItem]
|
||||
open_questions: List[RoadmapItem]
|
||||
|
||||
def _normalize_details(val) -> List[str]:
|
||||
# Accept string (block scalar) or list of strings; normalize to list[str]
|
||||
if not val:
|
||||
return []
|
||||
if isinstance(val, str):
|
||||
# split on blank lines while preserving paragraphs
|
||||
parts = [p.strip() for p in val.strip().split("\n\n")]
|
||||
return [p for p in parts if p]
|
||||
if isinstance(val, list):
|
||||
return [str(x) for x in val if str(x).strip()]
|
||||
# Fallback: stringify unknown types
|
||||
return [str(val)]
|
||||
|
||||
def _to_items(raw: List[Dict[str, Any]]) -> List[RoadmapItem]:
|
||||
items: List[RoadmapItem] = []
|
||||
for obj in raw or []:
|
||||
items.append(
|
||||
RoadmapItem(
|
||||
id=str(obj.get("id", "")),
|
||||
title=str(obj.get("title", "")),
|
||||
goal=str(obj.get("goal", "")),
|
||||
tags=list(obj.get("tags", []) or []),
|
||||
priority=obj.get("priority"),
|
||||
milestone=obj.get("milestone"),
|
||||
details=_normalize_details(obj.get("details")),
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def load_roadmap() -> RoadmapData:
|
||||
"""Load YAML and return structured RoadmapData (no caching)."""
|
||||
path = Path(current_app.config.get("ROADMAP_FILE"))
|
||||
with path.open("r", encoding="utf-8") as f:
|
||||
raw = yaml.safe_load(f) or {}
|
||||
|
||||
return RoadmapData(
|
||||
updated=raw.get("updated"),
|
||||
roadmap=_to_items(raw.get("roadmap", [])),
|
||||
backlog=_to_items(raw.get("backlog", [])),
|
||||
open_questions=_to_items(raw.get("open_questions", [])),
|
||||
)
|
||||
|
||||
|
||||
def _apply_filters(
|
||||
items: List[RoadmapItem],
|
||||
query: str,
|
||||
tags: List[str],
|
||||
min_priority: Optional[int],
|
||||
milestone: Optional[str],
|
||||
) -> List[RoadmapItem]:
|
||||
def matches(item: RoadmapItem) -> bool:
|
||||
# text search over id/title/goal
|
||||
if query:
|
||||
hay = f"{item.id} {item.title} {item.goal}".lower()
|
||||
if query not in hay:
|
||||
return False
|
||||
|
||||
# tag filter (AND)
|
||||
if tags:
|
||||
if not set(tags).issubset(set(item.tags)):
|
||||
return False
|
||||
|
||||
# min priority
|
||||
if min_priority is not None and item.priority is not None:
|
||||
if item.priority < min_priority:
|
||||
return False
|
||||
|
||||
# milestone
|
||||
if milestone:
|
||||
if (item.milestone or "").strip() != milestone.strip():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# sort: priority asc (None last), then title
|
||||
def sort_key(i: RoadmapItem) -> Tuple[int, str]:
|
||||
pri = i.priority if i.priority is not None else 9999
|
||||
return (pri, i.title.lower())
|
||||
|
||||
return sorted([i for i in items if matches(i)], key=sort_key)
|
||||
|
||||
|
||||
def _collect_all_tags(data: RoadmapData) -> List[str]:
|
||||
seen = set()
|
||||
for col in (data.roadmap, data.backlog, data.open_questions):
|
||||
for i in col:
|
||||
for t in i.tags:
|
||||
seen.add(t)
|
||||
return sorted(seen)
|
||||
|
||||
|
||||
@bp.route("/roadmap")
|
||||
def roadmap_view():
|
||||
data = load_roadmap()
|
||||
|
||||
# which column?
|
||||
section = request.args.get("section", "roadmap")
|
||||
if section not in {"roadmap", "backlog", "open_questions"}:
|
||||
abort(400, "invalid section")
|
||||
|
||||
# filters
|
||||
q = (request.args.get("q") or "").strip().lower()
|
||||
tags = request.args.getlist("tag")
|
||||
min_priority = request.args.get("min_priority")
|
||||
milestone = request.args.get("milestone") or None
|
||||
try:
|
||||
min_priority_val = int(min_priority) if min_priority else None
|
||||
except ValueError:
|
||||
min_priority_val = None
|
||||
|
||||
# pick list + filter
|
||||
source = getattr(data, section)
|
||||
items = _apply_filters(source, q, tags, min_priority_val, milestone)
|
||||
|
||||
# tag universe for sidebar chips
|
||||
all_tags = _collect_all_tags(data)
|
||||
|
||||
return render_template(
|
||||
"roadmap.html",
|
||||
updated=data.updated,
|
||||
section=section,
|
||||
items=items,
|
||||
all_tags=all_tags,
|
||||
q=q,
|
||||
selected_tags=tags,
|
||||
min_priority=min_priority_val,
|
||||
milestone=milestone,
|
||||
)
|
||||
214
app/docs/roadmap.yaml
Normal file
214
app/docs/roadmap.yaml
Normal file
@@ -0,0 +1,214 @@
|
||||
# roadmap.yaml
|
||||
updated: "2025-08-22"
|
||||
|
||||
roadmap:
|
||||
- id: "p1-analysis-cloudflare"
|
||||
priority: 1
|
||||
title: "Cloudflare Detection"
|
||||
goal: "Detect Cloudflare usage and badge it, with explanation of dual-use (security vs. abuse)."
|
||||
tags: ["analysis"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Detection signals: DNS (CNAME to Cloudflare, AS13335), HTTP headers (cf-ray, cf-cache-status), IP ranges, and challenge pages."
|
||||
- "UI: add badge + tooltip with a short explainer about legitimate protection vs. abuse evasion."
|
||||
- "Edge cases: 'grey-clouded' DNS entries, partial proxy (only some records), and CDN in front of non-HTTP services."
|
||||
- "Acceptance: correctly identifies Cloudflare on known test hosts and avoids false positives on non-CF CDNs."
|
||||
|
||||
- id: "p1-analysis-total-score"
|
||||
priority: 1
|
||||
title: "Total Score"
|
||||
goal: "Implement a generalized site “Total Score” (0–10 scale) to give analysts a quick risk snapshot."
|
||||
tags: ["analysis"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Inputs: TLS posture, suspicious scripts/forms (severity-weighted), domain/IP reputation, server headers/misconfigs."
|
||||
- "Method: weighted components with neutral defaults when data is unavailable; avoid over-penalizing partial signals."
|
||||
- "Explainability: always show a breakdown and contribution per component; include a 'Why?' link in the UI."
|
||||
- "Calibration: start with heuristic weights, then calibrate on a test set; store weights in settings.yaml."
|
||||
|
||||
- id: "p2-ui-rules-lab"
|
||||
priority: 2
|
||||
title: "Rules Lab"
|
||||
goal: "Build a WYSIWYG Rules Lab (paste, validate, run against sample text)."
|
||||
tags: ["ui"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Features: syntax-highlighted editor, rule validation, run against sample payloads, show matches/captures, timing."
|
||||
- "Samples: ship a small library of example texts and rules; allow users to save their own samples (local storage)."
|
||||
- "Safety: no external network calls; size/time limits to prevent runaway regex; clear error messages."
|
||||
- "UX: one-click copy of rule JSON; link to docs on rule schema."
|
||||
|
||||
- id: "p2-ui-usage-page"
|
||||
priority: 2
|
||||
title: "Usage Page"
|
||||
goal: "Create a “Usage” page to explain app functionality."
|
||||
tags: ["ui","docs"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Content: quickstart, supported analyses, cache vs. re-run behavior, artifact locations."
|
||||
- "Include: screenshots/GIFs, API curl examples, link to OpenAPI docs."
|
||||
- "Notes: clarify privacy, what we store, and retention defaults."
|
||||
|
||||
- id: "move-changelog-into-app"
|
||||
priority: 2
|
||||
title: "Move Changelog into App"
|
||||
goal: "Moves Changelog into App"
|
||||
tags: ["ui","docs"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Notes:Makes it much easier for users to see what's happening"
|
||||
- "Content: changelog.md already in docs."
|
||||
|
||||
- id: "p2-ui-about-page"
|
||||
priority: 2
|
||||
title: "About Page"
|
||||
goal: "Create an “About” page with project context."
|
||||
tags: ["ui","docs"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Content: project purpose, high-level architecture diagram, technology stack."
|
||||
- "Meta: version, commit hash, build date; link to repo and roadmap."
|
||||
- "Governance: disclaimer about intended use and limitations."
|
||||
|
||||
- id: "p3-api-core-endpoints"
|
||||
priority: 3
|
||||
title: "Core Endpoints"
|
||||
goal: "Add `/screenshot`, `/source`, and `/analyse` endpoints."
|
||||
tags: ["api"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Define request/response schemas; include run_id in responses to tie artifacts together."
|
||||
- "Auth: simple token header; rate-limiting per token."
|
||||
- "Errors: standardized JSON error body; consistent HTTP codes."
|
||||
- "Docs: provide curl examples; note synchronous vs. long-running behavior."
|
||||
|
||||
- id: "p3-api-analyze-script"
|
||||
priority: 3
|
||||
title: "Analyze Script Endpoint"
|
||||
goal: "Add POST /api/analyze_script in OpenAPI and serve /api/openapi.yaml."
|
||||
tags: ["api"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Request: raw script text or URL; size cap; optional rule-set selection."
|
||||
- "Processing: run rules engine; return matched rule names, severities, and excerpts."
|
||||
- "Artifacts: store hashed script with metadata; include reference in response."
|
||||
- "Validation: reject binary content; enforce content-type and max size."
|
||||
|
||||
- id: "p3-api-docs-ui"
|
||||
priority: 3
|
||||
title: "API Docs UI"
|
||||
goal: "Provide interactive docs (Swagger UI or Redoc) at /docs."
|
||||
tags: ["api"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Serve OpenAPI from /api/openapi.yaml; auto-refresh on rebuild."
|
||||
- "Swagger UI 'try it out' toggle; disable in prod if needed."
|
||||
- "Theming to match app; link to Usage page for context."
|
||||
|
||||
- id: "p3-api-json-errors"
|
||||
priority: 3
|
||||
title: "JSON Error Consistency"
|
||||
goal: "Ensure JSON error consistency across 400–500 responses."
|
||||
tags: ["api", "nice-to-have"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Schema: {\"error\": {\"code\": int, \"message\": str, \"details\": object, \"correlation_id\": str}}."
|
||||
- "Implement Flask error handlers; return JSON for 400/403/404/405/500."
|
||||
- "Log: include correlation_id in logs; surface it in responses for support."
|
||||
|
||||
- id: "p4-ops-retention-policy"
|
||||
priority: 4
|
||||
title: "Retention Policy"
|
||||
goal: "Define retention thresholds for artifacts (age/size)."
|
||||
tags: ["ops"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Policy: max age per artifact type; total size caps per workspace."
|
||||
- "Configuration: settings.yaml-driven; per-type overrides."
|
||||
- "Safety: dry-run mode and deletion preview; minimum free space guard."
|
||||
|
||||
- id: "p4-ops-cleanup-scripts"
|
||||
priority: 4
|
||||
title: "Cleanup Scripts"
|
||||
goal: "Implement cleanup/maintenance scripts, driven by settings.yaml."
|
||||
tags: ["ops"]
|
||||
milestone: null
|
||||
details:
|
||||
- "CLI: list, simulate, prune; log summary of bytes reclaimed and items removed."
|
||||
- "Scheduling: optional cron/apscheduler task; lock to prevent concurrent runs."
|
||||
- "Observability: emit metrics (counts, durations) to logs."
|
||||
|
||||
- id: "p4-ops-results-cache"
|
||||
priority: 4
|
||||
title: "Results Cache"
|
||||
goal: "Add UX toggle: “Re-run analysis” vs. “Load from cache.”"
|
||||
tags: ["ops"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Cache key: normalized URL + analysis settings; include versioning to bust on rule changes."
|
||||
- "UI: clearly label cached vs. fresh; provide 'Invalidate cache' action."
|
||||
- "TTL: setting-driven; guard against stale security results."
|
||||
|
||||
- id: "p5-intel-domain-reputation"
|
||||
priority: 5
|
||||
title: "Domain Reputation"
|
||||
goal: "Build consolidated reputation store (URLHaus, OpenPhish)."
|
||||
tags: ["intel"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Ingestion: scheduled pulls; parse feeds; dedupe and normalize indicators."
|
||||
- "Storage: compact on-disk DB (e.g., sqlite/duckdb) keyed by domain/URL with timestamps."
|
||||
- "Use: query during analysis; add context to findings with source + first_seen/last_seen."
|
||||
|
||||
- id: "p5-intel-threat-connectors"
|
||||
priority: 5
|
||||
title: "Threat Intel Connectors"
|
||||
goal: "Add connectors for VirusTotal, ThreatFox, and future providers (via settings.yaml)."
|
||||
tags: ["intel"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Config: enable per-connector with API keys via settings.yaml or env."
|
||||
- "Runtime: rate limiting and backoff; cache responses to reduce cost/latency."
|
||||
- "Merge: normalize verdicts and confidence; avoid double-counting against Total Score."
|
||||
|
||||
backlog:
|
||||
- id: "backlog-scan-server-profile"
|
||||
title: "Server Profile Scan"
|
||||
goal: "Run lightweight nmap scan on web/alt ports, merge with headers for stack inference."
|
||||
tags: ["scan"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Scope: common ports (80,443,8000,8080,8443,22); banner grab only; conservative timing."
|
||||
- "Inference: combine banners + headers to guess stack (IIS vs. nginx/Apache)."
|
||||
- "Controls: opt-in, with time and port limits to avoid noisy scans."
|
||||
|
||||
- id: "backlog-intel-ip-reputation"
|
||||
title: "IP Reputation Expansion"
|
||||
goal: "Expand reputation checks to IP blocklists and datasets."
|
||||
tags: ["intel"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Sources: community blocklists with permissive licenses; document any commercial sources separately."
|
||||
- "Model: score IPs with decay over time; avoid permanent penalties for stale abuse."
|
||||
- "Integration: surface as context; do not overrule domain-level signals."
|
||||
|
||||
open_questions:
|
||||
- id: "design-imports-unification"
|
||||
title: "Imports Unification"
|
||||
goal: "Decide if imports/utilities (e.g., decorators) should be centralized in state.py."
|
||||
tags: ["design"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Pros: consistent imports, fewer circular references, easier testing."
|
||||
- "Cons: can become a god-module; hidden dependencies."
|
||||
- "Proposal: a small 'core/state.py' for app-wide state + 'utils/' packages for helpers."
|
||||
|
||||
- id: "design-score-calibration"
|
||||
title: "Score Calibration"
|
||||
goal: "Define and calibrate methodology for the Total Score scale."
|
||||
tags: ["design"]
|
||||
milestone: null
|
||||
details:
|
||||
- "Dataset: assemble a labeled set of benign/suspicious sites for tuning."
|
||||
- "Approach: start with manual weights, then fit via simple regression or grid search."
|
||||
- "Outcome: publish thresholds for low/medium/high along with examples."
|
||||
@@ -30,6 +30,11 @@
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('roadmap.roadmap_view') }}">
|
||||
Roadmap
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{# Mobile toggle #}
|
||||
@@ -52,6 +57,11 @@
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('roadmap.roadmap_view') }}">
|
||||
Roadmap
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
262
app/templates/roadmap.html
Normal file
262
app/templates/roadmap.html
Normal file
@@ -0,0 +1,262 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Roadmap{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto max-w-6xl px-4 py-6">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight">SneakyScope Roadmap</h1>
|
||||
{% if updated %}
|
||||
<p class="text-sm text-gray-400">Last updated: {{ updated }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Section switcher -->
|
||||
<form method="get" action="{{ url_for('roadmap.roadmap_view') }}" class="flex items-center gap-2">
|
||||
<input type="hidden" name="q" value="{{ q }}">
|
||||
{% for t in selected_tags %}
|
||||
<input type="hidden" name="tag" value="{{ t }}">
|
||||
{% endfor %}
|
||||
{% if min_priority is not none %}
|
||||
<input type="hidden" name="min_priority" value="{{ min_priority }}">
|
||||
{% endif %}
|
||||
{% if milestone %}
|
||||
<input type="hidden" name="milestone" value="{{ milestone }}">
|
||||
{% endif %}
|
||||
|
||||
<select name="section" class="rounded-xl border border-gray-700 bg-gray-900 px-3 py-2 text-sm">
|
||||
<option value="roadmap" {{ "selected" if section == "roadmap" else "" }}>Roadmap</option>
|
||||
<option value="backlog" {{ "selected" if section == "backlog" else "" }}>Backlog</option>
|
||||
<option value="open_questions" {{ "selected" if section == "open_questions" else "" }}>Open Questions</option>
|
||||
</select>
|
||||
<button class="rounded-2xl bg-gray-800 px-4 py-2 text-sm font-medium hover:bg-gray-700">Go</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form method="get" action="{{ url_for('roadmap.roadmap_view') }}" class="mb-6 grid gap-4 sm:grid-cols-12">
|
||||
<input type="hidden" name="section" value="{{ section }}">
|
||||
|
||||
<!-- search -->
|
||||
<div class="sm:col-span-5">
|
||||
<label class="mb-1 block text-sm text-gray-300">Search</label>
|
||||
<input name="q" value="{{ q }}" placeholder="Search title, goal, or ID"
|
||||
class="w-full rounded-xl border border-gray-700 bg-gray-900 px-3 py-2 text-sm placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- min priority -->
|
||||
<div class="sm:col-span-2">
|
||||
<label class="mb-1 block text-sm text-gray-300">Min Priority</label>
|
||||
<input name="min_priority" type="number" min="1" max="9" value="{{ min_priority if min_priority is not none }}"
|
||||
class="w-full rounded-xl border border-gray-700 bg-gray-900 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- milestone -->
|
||||
<div class="sm:col-span-3">
|
||||
<label class="mb-1 block text-sm text-gray-300">Milestone</label>
|
||||
<input name="milestone" value="{{ milestone or '' }}" placeholder="e.g., v0.2"
|
||||
class="w-full rounded-xl border border-gray-700 bg-gray-900 px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
|
||||
</div>
|
||||
|
||||
<!-- submit -->
|
||||
<div class="sm:col-span-2 flex items-end">
|
||||
<button class="w-full rounded-2xl bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-500">Filter</button>
|
||||
</div>
|
||||
|
||||
<!-- tags -->
|
||||
<div class="sm:col-span-12">
|
||||
<label class="mb-2 block text-sm text-gray-300">Tags</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for t in all_tags %}
|
||||
<label class="inline-flex cursor-pointer items-center gap-2 rounded-2xl border border-gray-700 bg-gray-900 px-3 py-1.5 text-sm hover:border-gray-600">
|
||||
<input type="checkbox" name="tag" value="{{ t }}" class="h-4 w-4 accent-blue-600"
|
||||
{% if t in selected_tags %}checked{% endif %}>
|
||||
<span class="text-gray-200">{{ t }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Empty state -->
|
||||
{% if not items %}
|
||||
<div class="rounded-2xl border border-gray-700 bg-gray-900 p-6 text-gray-300">
|
||||
No items match your filters.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Cards -->
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{% for it in items %}
|
||||
<article class="rounded-2xl border border-gray-700 bg-gray-900 p-4">
|
||||
<div class="mb-2 flex items-start justify-between gap-3">
|
||||
<h2 class="text-base font-semibold leading-snug">{{ it.title }}</h2>
|
||||
{% if it.priority is not none %}
|
||||
<span class="badge badge-info">
|
||||
P{{ it.priority }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="mb-3 text-sm text-gray-300">{{ it.goal }}</p>
|
||||
|
||||
<!-- chips -->
|
||||
<div class="mb-3 flex flex-wrap gap-2">
|
||||
{% for tag in it.tags %}
|
||||
<span class="chips">
|
||||
{{ tag }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if it.milestone %}
|
||||
<span class="badge badge-success">
|
||||
milestone: {{ it.milestone }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-xs text-gray-400">{{ it.id }}</code>
|
||||
<!-- Placeholder for future actions (Flowbite buttons/menus) -->
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border border-gray-700 bg-gray-800 px-3 py-1.5 text-xs text-gray-200 hover:bg-gray-700"
|
||||
data-modal-target="roadmap-modal"
|
||||
data-modal-toggle="roadmap-modal"
|
||||
data-item='{{ {
|
||||
"id": it.id,
|
||||
"title": it.title,
|
||||
"goal": it.goal,
|
||||
"priority": it.priority,
|
||||
"tags": it.tags,
|
||||
"milestone": it.milestone,
|
||||
"details": it.details
|
||||
} | tojson | forceescape }}'
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="roadmap-modal" tabindex="-1" aria-hidden="true"
|
||||
class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
|
||||
<div class="fixed inset-0 bg-black/60"></div>
|
||||
|
||||
<!-- Make SIZE + LAYOUT CHANGES HERE -->
|
||||
<div class="relative z-10 w-full sm:max-w-3xl md:max-w-4xl lg:max-w-5xl
|
||||
max-h-[85vh] overflow-hidden rounded-2xl border border-gray-700 bg-gray-900">
|
||||
|
||||
<!-- Header (sticky inside modal) -->
|
||||
<div class="flex items-center justify-between gap-2 border-b border-gray-800 px-4 py-3">
|
||||
<h3 id="rm-title" class="text-lg font-semibold text-gray-100">Item</h3>
|
||||
<button type="button" class="rounded-lg p-2 hover:bg-gray-800" data-modal-hide="roadmap-modal" aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Body (scrolls if long) -->
|
||||
<div class="px-4 py-4 overflow-y-auto">
|
||||
<div class="mb-4 space-y-2 text-sm">
|
||||
<div class="text-gray-300"><span class="font-medium">ID:</span> <code id="rm-id" class="text-gray-400"></code></div>
|
||||
<div class="text-gray-300"><span class="font-medium">Goal:</span> <span id="rm-goal" class="text-gray-200"></span></div>
|
||||
<div class="text-gray-300"><span class="font-medium">Priority:</span> <span id="rm-priority"></span></div>
|
||||
<div class="text-gray-300"><span class="font-medium">Milestone:</span> <span id="rm-milestone"></span></div>
|
||||
<div class="text-gray-300"><span class="font-medium">Tags:</span>
|
||||
<span id="rm-tags" class="inline-flex flex-wrap gap-2 align-middle"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="mb-2 text-sm font-semibold text-gray-200">Details</h4>
|
||||
<div id="rm-details" class="prose prose-invert max-w-none text-sm"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex justify-end gap-2 border-t border-gray-800 px-4 py-3">
|
||||
<button type="button" class="rounded-xl border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-200 hover:bg-gray-700" data-modal-hide="roadmap-modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function(){
|
||||
function onReady(fn){
|
||||
if(document.readyState !== 'loading') fn();
|
||||
else document.addEventListener('DOMContentLoaded', fn);
|
||||
}
|
||||
|
||||
onReady(function(){
|
||||
const modal = document.getElementById('roadmap-modal');
|
||||
|
||||
function el(id){
|
||||
return document.getElementById(id);
|
||||
}
|
||||
function pill(text){
|
||||
const span = document.createElement('span');
|
||||
span.className = "inline-flex items-center rounded-full border border-gray-700 bg-gray-800 px-2.5 py-0.5 text-xs text-gray-200";
|
||||
span.textContent = text;
|
||||
return span;
|
||||
}
|
||||
function setText(id, v){ el(id).textContent = (v ?? ""); }
|
||||
function setTags(tags){
|
||||
const holder = el("rm-tags"); holder.innerHTML = "";
|
||||
(tags || []).forEach(t => holder.appendChild(pill(t)));
|
||||
}
|
||||
function setDetails(list){
|
||||
const c = el("rm-details"); c.innerHTML = "";
|
||||
if(!list || !list.length){
|
||||
const p = document.createElement('p'); p.className = "text-gray-400"; p.textContent = "No additional details.";
|
||||
c.appendChild(p); return;
|
||||
}
|
||||
list.forEach(part => {
|
||||
const p = document.createElement('p'); p.className = "text-gray-200"; p.textContent = part;
|
||||
c.appendChild(p);
|
||||
});
|
||||
}
|
||||
function populate(data){
|
||||
setText("rm-title", data.title || "Item");
|
||||
setText("rm-id", data.id || "");
|
||||
setText("rm-goal", data.goal || "");
|
||||
setText("rm-priority", (data.priority != null) ? `P${data.priority}` : "");
|
||||
setText("rm-milestone", data.milestone || "");
|
||||
setTags(data.tags || []);
|
||||
setDetails(data.details || []);
|
||||
}
|
||||
|
||||
// Event delegation: works for all current and future buttons
|
||||
document.addEventListener('click', function(ev){
|
||||
const btn = ev.target.closest('[data-item]');
|
||||
if(!btn) return;
|
||||
try{
|
||||
const raw = btn.getAttribute('data-item') || "{}";
|
||||
const data = JSON.parse(raw);
|
||||
populate(data);
|
||||
// If not using Flowbite to open, uncomment:
|
||||
// modal.classList.remove('hidden');
|
||||
} catch(err){
|
||||
console.error("Failed to parse data-item JSON", err);
|
||||
}
|
||||
});
|
||||
|
||||
// If not using Flowbite to close, uncomment:
|
||||
// document.querySelectorAll('[data-modal-hide="roadmap-modal"]').forEach(b => {
|
||||
// b.addEventListener('click', () => modal.classList.add('hidden'));
|
||||
// });
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
54
docs/changelog.md
Normal file
54
docs/changelog.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### ✨ Features
|
||||
- _Nothing yet — add upcoming features here._
|
||||
|
||||
### 🛠️ Refactors
|
||||
- _Nothing yet — add upcoming refactors here._
|
||||
|
||||
### 🐛 Fixes
|
||||
- _Nothing yet — add upcoming fixes here._
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [v0.2] – 2025-08-22
|
||||
|
||||
### ✨ Features
|
||||
- **UI Modernization**
|
||||
Migrated the entire front-end to **Tailwind CSS (compiled)** with **Flowbite JS** components for better responsiveness, consistency, and developer productivity.
|
||||
Introduced a **new navbar and layout system**, improving navigation and making future expansion easier.
|
||||
Added **Docker-based CSS build** to keep builds reproducible and lightweight.
|
||||
|
||||
- **Reusable CSS Components**
|
||||
Added custom utility classes (`badge`, `badge-ok`, `badge-warn`, `badge-danger`, `chip`, `card`, etc.) to replace long Tailwind strings.
|
||||
This reduces repetition and ensures a consistent look across the app.
|
||||
|
||||
### 🛠️ Refactors
|
||||
- **Template Includes**
|
||||
Extracted shared UI sections (headers, footers, layout chunks) into separate **Jinja includes**, improving maintainability and readability of templates.
|
||||
|
||||
### 🐛 Fixes
|
||||
- **Table Rendering**
|
||||
Locked table column widths and fixed snippet scaling issues to prevent column misalignment and content reflow.
|
||||
This ensures analysis results (like script and form findings) remain readable and properly aligned.
|
||||
|
||||
- **Rules Engine State**
|
||||
Fixed a bug where the **rules engine** was not being pulled correctly from the application state after the previous refactor.
|
||||
This restores proper detection of suspicious scripts/forms and ensures rule definitions (with `name` and `description`) are honored.
|
||||
|
||||
---
|
||||
|
||||
## [v0.1] – Initial Work
|
||||
|
||||
- Implemented initial **Flask-based web UI** for URL submission and analysis.
|
||||
- Added **domain & IP enrichment** (WHOIS, GeoIP, ASN/ISP lookups).
|
||||
- Built first version of the **Suspicious Rules Engine** for script and form detection.
|
||||
- Basic Docker setup for sandboxed deployment.
|
||||
@@ -1,34 +0,0 @@
|
||||
# SneakyScope — Roadmap (Updated 8-21-25)
|
||||
|
||||
## Priority 1 – Core Analysis / Stability
|
||||
* if cloudflare, we notate and badge it, along with a blurp that explains how cloudflare is both used for good and evil.
|
||||
* need a generalized "total score" for the site. something that is a quick 0/10 (guessing on the number), so new analyst don't have to think on the details.
|
||||
* make a "dectorators" file to unify imports
|
||||
|
||||
## Priority 2 – UI / UX
|
||||
* Rules Lab (WYSIWYG tester): paste a rule, validate/compile, run against sample text; lightweight nav entry.
|
||||
* Build reusable util classes in tailwind and replace the long class strings. Classes to build: badge, badge-ok, badge-warn, badge-danger, chip, card.
|
||||
|
||||
## Priority 3 – API Layer
|
||||
|
||||
* API endpoints: `/screenshot`, `/source`, `/analyse`.
|
||||
* **OpenAPI**: add `POST /api/analyze_script` (request/response schemas, examples) to `openapi/openapi.yaml`; serve at `/api/openapi.yaml`.
|
||||
* Docs UI: Swagger UI or Redoc at `/docs`.
|
||||
* (Nice-to-have) API JSON error consistency: handlers for 400/403/404/405/500 that always return JSON.
|
||||
|
||||
## Priority 4 – Artifact Management & Ops
|
||||
|
||||
* Retention/cleanup policy for old artifacts (age/size thresholds).
|
||||
* Make periodic maintenance scripts for storage; cleanup options set in `settings.yaml`.
|
||||
* Results caching UX: add “Re-run analysis” vs. “Load from cache” controls in the results UI.
|
||||
|
||||
## Priority 5 – Extras / Integrations
|
||||
|
||||
* Domain reputation (local feeds): build and refresh a consolidated domain/URL reputation store from URLHaus database dump and OpenPhish community dataset (scheduled pulls with dedup/normalize).
|
||||
* Threat intel connectors (settings-driven): add `settings.yaml` entries for VirusTotal and ThreatFox API keys (plus future providers); when present, enrich lookups and merge results into the unified reputation checks during analysis.
|
||||
|
||||
## Backlog / Far-Off Plans
|
||||
|
||||
* Server profile scan: run a lightweight nmap service/banner scan on common web/alt ports (80, 443, 8000, 8080, 8443, etc.) and SSH; combine with server headers to infer stack (e.g., IIS vs. Linux/\*nix).
|
||||
|
||||
* IP Lookups 0 if we are successful on domain replutation / ip reputation
|
||||
Reference in New Issue
Block a user