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:
2025-08-22 15:05:09 -05:00
parent 9cc2f8183c
commit cd30cde946
7 changed files with 723 additions and 38 deletions

View File

@@ -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
View 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
View 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” (010 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 400500 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."

View File

@@ -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
View 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
View 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.

View File

@@ -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