adding changelog to pages/routes

This commit is contained in:
2025-09-03 13:17:33 -05:00
parent d5cc9df699
commit 248fce5655
7 changed files with 306 additions and 3 deletions

View File

@@ -43,7 +43,6 @@ SneakyScope fetches a page in a sandbox, enriches with WHOIS/GeoIP, and runs a u
* **Playwright** for headless page fetch/render * **Playwright** for headless page fetch/render
* **BeautifulSoup4** for parsing * **BeautifulSoup4** for parsing
* **Rules Engine** * **Rules Engine**
* YAML regex rules (`config/suspicious_rules.yaml`) * YAML regex rules (`config/suspicious_rules.yaml`)
* Function rules (`app/rules/function_rules.py`) registered on startup * Function rules (`app/rules/function_rules.py`) registered on startup
* **Artifacts**: persistent path mounted at `/data` (configurable) * **Artifacts**: persistent path mounted at `/data` (configurable)

View File

@@ -12,6 +12,7 @@ from app.app_settings import AppSettings
from app.blueprints.main import bp as main_bp # ui blueprint from app.blueprints.main import bp as main_bp # ui blueprint
from app.blueprints.api import api_bp as api_bp # api blueprint from app.blueprints.api import api_bp as api_bp # api blueprint
from app.blueprints.roadmap import bp as roadmap_bp # roadmap from app.blueprints.roadmap import bp as roadmap_bp # roadmap
from app.blueprints.changelog import bp as changelog_bp # changelog
@@ -63,6 +64,7 @@ def create_app() -> Flask:
# roadmap file # roadmap file
app.config["ROADMAP_FILE"] = str(Path(app.root_path) / "docs" / "roadmap.yaml") app.config["ROADMAP_FILE"] = str(Path(app.root_path) / "docs" / "roadmap.yaml")
app.config["CHANGELOG_FILE"] = str(Path(app.root_path) / "docs" / "changelog.yaml")
# Configure storage directory (bind-mount is still handled by sandbox.sh) # Configure storage directory (bind-mount is still handled by sandbox.sh)
sandbox_storage_default = Path("/data") sandbox_storage_default = Path("/data")
@@ -73,6 +75,7 @@ def create_app() -> Flask:
app.register_blueprint(main_bp) app.register_blueprint(main_bp)
app.register_blueprint(api_bp) app.register_blueprint(api_bp)
app.register_blueprint(roadmap_bp) app.register_blueprint(roadmap_bp)
app.register_blueprint(changelog_bp)
app_logger = get_app_logger() app_logger = get_app_logger()

View File

@@ -0,0 +1,71 @@
# app/services/changelog_loader.py
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Any, List, Optional, Dict
import yaml
from flask import Blueprint, current_app, render_template
@dataclass
class ChangeItem:
title: str
details: List[str]
@dataclass
class VersionLog:
version: str
features: List[ChangeItem]
refactors: List[ChangeItem]
fixes: List[ChangeItem]
notes: List[str]
@dataclass
class Changelog:
unreleased: Dict[str, List[ChangeItem]]
versions: List[VersionLog]
def _coerce_items(items: Optional[List[Dict[str, Any]]]) -> List[ChangeItem]:
out: List[ChangeItem] = []
for it in items or []:
title = str(it.get("title", "")).strip()
details = [str(d) for d in (it.get("details") or [])]
out.append(ChangeItem(title=title, details=details))
return out
def load_changelog(path: Path) -> Changelog:
"""
Load changelog.yaml and coerce into dataclasses.
"""
data = yaml.safe_load(path.read_text(encoding="utf-8"))
unreleased = {
"features": _coerce_items(data.get("unreleased", {}).get("features")),
"refactors": _coerce_items(data.get("unreleased", {}).get("refactors")),
"fixes": _coerce_items(data.get("unreleased", {}).get("fixes")),
}
versions: List[VersionLog] = []
for v in data.get("versions", []):
versions.append(
VersionLog(
version=str(v.get("version")),
features=_coerce_items(v.get("features")),
refactors=_coerce_items(v.get("refactors")),
fixes=_coerce_items(v.get("fixes")),
notes=[str(n) for n in (v.get("notes") or [])],
)
)
return Changelog(unreleased=unreleased, versions=versions)
bp = Blueprint("changelog", __name__)
@bp.route("/changelog")
def view_changelog():
# Configurable path with sensible default at project root
cfg_path = current_app.config.get("CHANGELOG_FILE")
path = Path(cfg_path) if cfg_path else (Path(current_app.root_path).parent / "changelog.yaml")
changelog = load_changelog(path)
return render_template("changelog.html", changelog=changelog)

80
app/docs/changelog.yaml Normal file
View File

@@ -0,0 +1,80 @@
# changelog.yaml
unreleased:
features: []
refactors: []
fixes: []
versions:
- version: "v0.2"
features:
- title: "UI Modernization"
details:
- "Migrated front-end to Tailwind CSS (compiled) with Flowbite JS components."
- "New navbar and layout system; better navigation and future expansion."
- "Docker-based CSS build for reproducible, lightweight builds."
- title: "Reusable CSS Components"
details:
- "Custom utilities: badge, badge-ok, badge-warn, badge-danger, chip, card, etc."
- "Reduces repetition and enforces consistent look."
- title: "Roadmap / Changelog (YAML-driven + in-app UI)"
details:
- "YAML-backed roadmap, in-app view at `/roadmap`."
- "Roadmap Filters: q, tag, min_priority, milestone; tag chips; Details modal that renders `details`."
- "YAML-backed Changelog, in-app view at `/changelog`."
- title: "Modal sizing & ergonomics"
details:
- "Wider modal at larger breakpoints; scrollable body for long content."
- title: "GeoIP Results Uplift"
details:
- "Cloudflare detection via GeoIP ASN; badge on results page."
- "Country/ASN notes shown beside collapsed IP next to GeoIP results."
- title: "Text Analysis Pipeline (Rules)"
details:
- "`analyse_text()` extracts visible text and evaluates `category: text` rules."
- "Captures matched phrases into deduped `content_snippet` (len capped via `settings.ui.snippet_preview_len`)."
- "Results exposed in JSON as `suspicious_text`; UI via `templates/partials/result_text.html`."
refactors:
- title: "Template Includes"
details:
- "Common UI (headers/footers/layout) extracted into Jinja includes."
- title: "Roadmap loader simplification"
details:
- "Removed cache; returns typed dataclasses and normalizes `details`."
- title: "Safer JSON in templates"
details:
- "Use `|tojson|forceescape` for embedding payloads in data attributes."
- title: "Rules Engine Regex handling"
details:
- "Honor per-rule regex flags; default IGNORECASE for `category: text` if no `i` flag."
- title: "Engine/Scanner logging"
details:
- "Dispatch-time visibility; gated by `settings.app.print_rule_dispatch`."
- title: "Code cleanup"
details:
- "Removed obsolete paths/utilities; removed duplicate `enrich_url` call."
fixes:
- title: "Table Rendering"
details:
- "Locked column widths; fixed snippet scaling to prevent reflow."
- title: "Rules Engine State"
details:
- "Fix pulling engine from app state; restores proper detections."
- title: "YAML parsing edge cases"
details:
- "Quote scalars containing `:`/`#`; use explicit `null` as needed."
- title: "/roadmap page stability"
details:
- "Return structured objects; fix `AttributeError: 'dict' object has no attribute 'roadmap'`."
- title: "Modal population"
details:
- "Pass `details` through route; DOM-ready + delegation populate reliably."
- title: "Text indicators not displayed"
details:
- "Add text analyzer; align result shape with `result_text` partial."
- version: "v0.1"
notes:
- "Initial Flask web UI for URL submission and analysis."
- "Domain & IP enrichment (WHOIS, GeoIP, ASN/ISP)."
- "First Suspicious Rules Engine for scripts/forms."
- "Basic Docker setup for sandboxed deployment."

View File

@@ -35,6 +35,11 @@
Roadmap Roadmap
</a> </a>
</li> </li>
<li>
<a href="{{ url_for('changelog.view_changelog') }}">
Changelog
</a>
</li>
</ul> </ul>
{# Mobile toggle #} {# Mobile toggle #}
@@ -62,6 +67,11 @@
Roadmap Roadmap
</a> </a>
</li> </li>
<li>
<a href="{{ url_for('changelog.view_changelog') }}">
Chnagelog
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,136 @@
{# templates/changelog.html #}
{% extends "base.html" %}
{% block title %}Changelog{% 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 Changelog</h1>
{% if updated %}
<p class="text-sm text-gray-400">Last updated: {{ updated }}</p>
{% endif %}
</div>
</div>
{# Unreleased #}
{% set ur = changelog.unreleased %}
{% if ur.features or ur.refactors or ur.fixes %}
<section class="mb-8 rounded-2xl border border-gray-700 bg-gray-900 p-5">
<div class="mb-3 flex items-center gap-3">
<h2 class="text-xl font-semibold">Unreleased</h2>
<span class="badge badge-warn">WIP</span>
</div>
<div class="grid gap-6 md:grid-cols-3">
{% for title, items, icon in [
("✨ Features", ur.features, "✨"),
("🛠️ Refactors", ur.refactors, "🛠️"),
("🐛 Fixes", ur.fixes, "🐛"),
] %}
<div class="rounded-xl border border-gray-800 bg-gray-950 p-4">
<h3 class="mb-2 text-sm font-semibold text-gray-200">{{ title }}</h3>
{% if items and items|length %}
<ul class="space-y-3">
{% for it in items %}
<li class="rounded-lg border border-gray-800 bg-gray-900 p-3">
<div class="mb-1 font-medium">{{ it.title }}</div>
{% if it.details %}
<ul class="ml-5 list-disc text-sm text-gray-300">
{% for d in it.details %}
<li>{{ d }}</li>
{% endfor %}
</ul>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-sm text-gray-400">Nothing yet — add upcoming {{ title.split(' ')[1] | lower }} here.</p>
{% endif %}
</div>
{% endfor %}
</div>
</section>
{% endif %}
{# Versions Accordion #}
<section>
<div id="changelog-accordion" data-accordion="collapse" class="divide-y rounded-2xl border border-gray-700 bg-gray-900">
{% for v in changelog.versions %}
<h2 id="acc-head-{{ loop.index }}">
<button type="button"
class="flex w-full items-center justify-between px-5 py-4 text-left hover:bg-gray-800"
data-accordion-target="#acc-body-{{ loop.index }}"
aria-expanded="{{ 'true' if loop.first else 'false' }}"
aria-controls="acc-body-{{ loop.index }}">
<span class="flex items-center gap-3">
<span class="font-semibold">{{ v.version }}</span>
{% if v.notes and not (v.features or v.refactors or v.fixes) %}
<span class="badge badge-ok">Notes only</span>
{% endif %}
</span>
<svg class="h-5 w-5 text-gray-300" aria-hidden="true" fill="none" viewBox="0 0 10 6">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5 5 1 1 5"/>
</svg>
</button>
</h2>
<div id="acc-body-{{ loop.index }}"
class="{{ '' if loop.first else 'hidden' }}"
aria-labelledby="acc-head-{{ loop.index }}">
<div class="space-y-8 px-5 pb-5 pt-1">
{% if v.notes and v.notes|length %}
<div>
<h3 class="mb-2 text-sm font-semibold text-gray-200">Notes</h3>
<ul class="ml-6 list-disc text-sm text-gray-300">
{% for n in v.notes %}
<li>{{ n }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% for section_title, items in [
("✨ Features", v.features),
("🛠️ Refactors", v.refactors),
("🐛 Fixes", v.fixes),
] %}
{% if items and items|length %}
<div>
<h3 class="mb-2 text-sm font-semibold text-gray-200">{{ section_title }}</h3>
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
{% for it in items %}
<article class="rounded-2xl border border-gray-800 bg-gray-950 p-4">
<h4 class="mb-1 font-semibold leading-snug">{{ it.title }}</h4>
{% if it.details %}
<ul class="ml-5 list-disc text-sm text-gray-300">
{% for d in it.details %}
<li>{{ d }}</li>
{% endfor %}
</ul>
{% endif %}
</article>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</section>
</div>
{% endblock %}
{% block scripts %}
{# If youre not auto-initializing Flowbite elsewhere, ensure its JS is loaded globally. #}
<script>
/* Optional: if you ever render details as HTML snippets, ensure they are trusted or sanitized server-side. */
/* No extra JS needed here if Flowbite handles [data-accordion]. */
</script>
{% endblock %}

View File

@@ -17,7 +17,7 @@ This project follows [Semantic Versioning](https://semver.org/).
- _Nothing yet — add upcoming fixes here._ - _Nothing yet — add upcoming fixes here._
--- ---
## [v0.2]
### ✨ Features ### ✨ Features
@@ -37,6 +37,10 @@ This project follows [Semantic Versioning](https://semver.org/).
* **Modal sizing & ergonomics** * **Modal sizing & ergonomics**
Increased modal width at larger breakpoints and made the body scrollable so long details dont squish other content. Increased modal width at larger breakpoints and made the body scrollable so long details dont squish other content.
* **GeoIP Results Uplift**
Added Cloudflare detection via Geoip ASN results and Cloudflare badge on results page
Added Country - ASN notes beside collapsed IP next to GeoIP results for quick viewing.
* **Text Analysis Pipeline (Rules)** * **Text Analysis Pipeline (Rules)**
Implemented `analyse_text()` to extract visible page text and evaluate `category: text` rules. Implemented `analyse_text()` to extract visible page text and evaluate `category: text` rules.
Captures matched phrases into a deduped `content_snippet` (length capped via `settings.ui.snippet_preview_len`). Captures matched phrases into a deduped `content_snippet` (length capped via `settings.ui.snippet_preview_len`).
@@ -88,7 +92,7 @@ This project follows [Semantic Versioning](https://semver.org/).
--- ---
## [v0.1] Initial Work ## [v0.1]
- Implemented initial **Flask-based web UI** for URL submission and analysis. - Implemented initial **Flask-based web UI** for URL submission and analysis.
- Added **domain & IP enrichment** (WHOIS, GeoIP, ASN/ISP lookups). - Added **domain & IP enrichment** (WHOIS, GeoIP, ASN/ISP lookups).