diff --git a/Dockerfile b/Dockerfile index adc9235..94ca760 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,24 @@ +# --- Stage 1: CSS builder (no npm) --- +FROM alpine:3.20 AS css-builder +WORKDIR /css +RUN apk add --no-cache curl + +# Download Tailwind standalone CLI +# (Update version if desired; linux-x64 works on Alpine) +RUN curl -sL https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.10/tailwindcss-linux-x64 \ + -o /usr/local/bin/tailwindcss && chmod +x /usr/local/bin/tailwindcss + +# Config + sources +COPY tailwind/tailwind.config.js ./ +COPY assets ./assets +COPY app/templates ./app/templates +COPY app/static ./app/static + +# Build Tailwind CSS +RUN tailwindcss -i ./assets/input.css -o ./tw.css --minify + +# --- Stage 2: Playwright python image with requirements. + # Use the official Playwright image with browsers preinstalled FROM mcr.microsoft.com/playwright/python:v1.45.0-jammy @@ -19,6 +40,9 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code (the double app is needed because the app folder needs to be inside the app folder) COPY app/ /app/app/ +# Bring in the compiled CSS from Stage 1 +COPY --from=css-builder /css/tw.css /app/app/static/tw.css + COPY entrypoint.sh ./entrypoint.sh RUN chmod +x /app/entrypoint.sh diff --git a/app/blueprints/ui.py b/app/blueprints/ui.py index 58a56f6..e861ade 100644 --- a/app/blueprints/ui.py +++ b/app/blueprints/ui.py @@ -69,7 +69,8 @@ def inject_app_info(): """Inject app name and version into all templates.""" return { "app_name": app_name, - "app_version": app_version + "app_version": app_version, + "current_year": datetime.strftime(datetime.now(),"%Y") } @bp.route("/", methods=["GET"]) diff --git a/app/static/style.css b/app/static/style.css index b5f9d56..e69de29 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -1,472 +0,0 @@ -/* ========================================================================== - SneakyScope Stylesheet - Consolidated + Commented - ========================================================================== */ - -/* ========================================================================== - 0) Theme Variables - -------------------------------------------------------------------------- */ -:root { - /* Typography */ - --font-sans: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; - - /* Colors (derived from your current palette) */ - --bg: #0b0f14; - --text: #e6edf3; - --header-bg: #0f1720; - --card-bg: #111826; - --border: #1f2a36; /* darker border */ - --border-2: #243041; /* lighter border used on inputs/tables */ - --input-bg: #0b1220; - - --link: #7dd3fc; - --link-hover: #38bdf8; - - /* Accents */ - --accent-pill-bg: rgba(59,130,246,.18); - --accent-pill-bd: rgba(59,130,246,.45); - - /* Radius & Shadows */ - --radius: 12px; -} - -/* ========================================================================== - 1) Base / Reset - -------------------------------------------------------------------------- */ -html { scroll-behavior: smooth; } - -:root { font-family: var(--font-sans); } - -body { - margin: 0; - background: var(--bg); - color: var(--text); -} - -a { - color: var(--link); - text-decoration: underline; -} -a:hover { color: var(--link-hover); } - -img { - max-width: 100%; - height: auto; - border-radius: 8px; - border: 1px solid var(--border-2); -} - -/* ========================================================================== - 2) Layout (header/footer/main/cards) - -------------------------------------------------------------------------- */ -header, footer { - padding: 1rem 1.25rem; - background: var(--header-bg); - border-bottom: 1px solid var(--border); -} - -main { - /* full-width layout */ - padding: 1.5rem 2rem; - max-width: 100%; - width: 100%; - margin: 0; - box-sizing: border-box; -} - -.card { - background: var(--card-bg); - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - margin-bottom: 1rem; - /* anchors don't hide under sticky nav */ - scroll-margin-top: 72px; -} - -/* ========================================================================== - 3) Form Controls & Buttons - -------------------------------------------------------------------------- */ -label { - display: block; - margin-bottom: 0.5rem; -} - -input[type="url"] { - width: 100%; - padding: 0.7rem; - border-radius: 8px; - border: 1px solid var(--border-2); - background: var(--input-bg); - color: var(--text); -} - -button, .button { - display: inline-block; - margin-top: 0.75rem; - padding: 0.6rem 1rem; - border-radius: 8px; - border: 1px solid var(--border-2); - background: #1a2535; - color: var(--text); - text-decoration: none; - cursor: pointer; -} -button:hover, .button:hover { filter: brightness(1.05); } - -/* Flash messages */ -.flash { list-style: none; padding: 0.5rem 1rem; } -.flash .error { color: #ff6b6b; } - -/* Simple grid utility */ -.grid { - display: grid; - grid-template-columns: 150px 1fr; - gap: 0.5rem 1rem; -} - -/* ========================================================================== - 4) Code Blocks & Details/Accordion - -------------------------------------------------------------------------- */ -pre.code { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - font-size: 0.9rem; - white-space: pre-wrap; /* wrap long lines */ - word-break: break-all; - background: var(--input-bg); - padding: 0.75rem; - border-radius: 8px; - border: 1px solid var(--border-2); -} - -details summary { - cursor: pointer; - padding: 0.5rem; - font-weight: bold; - border-radius: 8px; - background: var(--card-bg); - border: 1px solid var(--border); - margin-bottom: 0.5rem; - transition: background 0.3s ease; -} -details[open] summary { background: #1a2535; } - -/* inner spacing when expanded */ -details > ul, -details > table { padding-left: 1rem; margin: 0.5rem 0; } - -/* flagged state */ -details.flagged summary { border-left: 4px solid #ff6b6b; } - -/* gentle transitions */ -details ul, details p { transition: all 0.3s ease; } - -/* readable expanded code without blowing layout */ -details pre.code { - white-space: pre-wrap; - word-break: break-word; - max-height: 18rem; - overflow: auto; -} - -/* ========================================================================== - 5) Tables — Enrichment (generic) - -------------------------------------------------------------------------- */ -.enrichment-table { - width: 100%; - border-collapse: collapse; - margin-bottom: 1rem; -} -.enrichment-table th, -.enrichment-table td { - border: 1px solid var(--border-2); - padding: 0.5rem; - vertical-align: top; -} -.enrichment-table th { - background: var(--card-bg); - text-align: left; -} -.enrichment-table td { - width: auto; - word-break: break-word; -} -.enrichment-table tbody tr:hover { background: #1f2a36; } -.enrichment-table thead th { border-bottom: 2px solid var(--border-2); } -/* ensure nested tables don't overflow cards */ -.card table { table-layout: auto; word-break: break-word; } - -/* ========================================================================== - 6) Tables — Shared Rules (Scripts & Forms) - -------------------------------------------------------------------------- */ -.scripts-table, -.forms-table { - table-layout: fixed; - width: 100%; -} -.scripts-table td ul, -.forms-table td ul { - margin: 0.25rem 0 0.25rem 1rem; - padding-left: 1rem; -} -.scripts-table td small, -.forms-table td small { opacity: 0.85; } -/* ellipsize by default */ -.scripts-table td, .scripts-table th, -.forms-table td, .forms-table th { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -/* allow wrapping inside expanded blocks */ -.scripts-table details, -.forms-table details { white-space: normal; } -.scripts-table details > pre.code, -.forms-table details > pre.code { - white-space: pre-wrap; - max-height: 28rem; - overflow: auto; -} - -/* ========================================================================== - 7) Scripts Table (columns & tweaks) - -------------------------------------------------------------------------- */ -/* compact inline snippet */ -.scripts-table pre.code { margin: 0; padding: 0.25rem; font-size: 0.9rem; } - -/* columns: Type | Source URL | Snippet | Matches */ -.scripts-table th:nth-child(1) { width: 8rem; } -.scripts-table th:nth-child(2) { width: 32rem; } -.scripts-table th:nth-child(3) { width: 24rem; } -.scripts-table th:nth-child(4) { width: auto; } - -/* ========================================================================== - 8) Forms Table (columns & helpers) - -------------------------------------------------------------------------- */ -/* columns: Action | Method | Inputs | Matches | Form Snippet */ -.forms-table th:nth-child(1) { width: 15rem; } /* Action */ -.forms-table th:nth-child(2) { width: 5rem; } /* Method */ -.forms-table th:nth-child(3) { width: 15rem; } /* Inputs */ -.forms-table th:nth-child(5) { width: 24rem; } /* Snippet */ -.forms-table th:nth-child(4) { width: auto; } /* Matches grows */ - -/* input chips layout inside cells */ -.forms-table .chips { - display: flex; - gap: 0.25rem; - flex-wrap: wrap; - white-space: normal; -} - -/* ========================================================================== - 9) Results Table (Recent runs list) - -------------------------------------------------------------------------- */ -.results-table { - width: 100%; - border-collapse: collapse; - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: var(--radius); - overflow: hidden; - table-layout: auto; -} -.results-table thead th { - padding: 0.6rem 0.75rem; - background: var(--header-bg); - border-bottom: 1px solid var(--border); - text-align: left; - font-weight: 600; - white-space: nowrap; -} -.results-table tbody td { - padding: 0.6rem 0.75rem; - border-top: 1px solid var(--border); - vertical-align: top; - text-align: left; -} -.results-table tbody tr:nth-child(odd) { background: #0d1522; } -.results-table a { text-decoration: underline; } - -/* column-specific helpers */ -.results-table td.url, -.results-table td.url a { - overflow-wrap: anywhere; - word-break: break-word; -} -.results-table td.uuid { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - word-break: break-all; - max-width: 28ch; -} -.results-table td.timestamp { - text-align: right; - white-space: nowrap; -} -.results-table tbody tr:first-child { box-shadow: inset 0 0 0 1px var(--border-2); } -.results-table .copy-btn { - margin-left: 0.4rem; - padding: 0.2rem 0.45rem; - border-radius: 6px; - border: 1px solid var(--border-2); - background: #1a2535; - color: var(--text); - cursor: pointer; - line-height: 1; - font-size: 0.9rem; -} -.results-table .copy-btn:hover { filter: brightness(1.1); } - -/* ========================================================================== - 10) Utilities (chips, badges, helpers) - -------------------------------------------------------------------------- */ -.breakable { white-space: normal; overflow-wrap: anywhere; word-break: break-word; } - -/* Generic badge + severities */ -.badge { - display: inline-block; - padding: 0.1rem 0.4rem; - margin-left: 0.35rem; - border-radius: 0.4rem; - font-size: 0.75rem; - line-height: 1; - vertical-align: middle; - user-select: none; - border: 1px solid transparent; /* individual severities add their borders */ -} -.sev-high { background: #fdecea; color: #b71c1c; border-color: #f5c6c4; } -.sev-medium { background: #fff8e1; color: #8a6d3b; border-color: #ffe0a3; } -.sev-low { background: #e8f5e9; color: #1b5e20; border-color: #b9e6be; } - -/* Tag chips */ -.chips { display: flex; gap: 0.25rem; flex-wrap: wrap; } -.chip { - display: inline-block; - padding: 0.1rem 0.35rem; - border-radius: 999px; - font-size: 0.7rem; - line-height: 1; - background: #eef2f7; - color: #425466; - border: 1px solid #d9e2ec; -} - -.checkbox-row { - display: flex; align-items: center; gap: .5rem; - margin: .5rem 0 1rem; -} - -/* ========================================================================== - 11) Sticky Top Jump Navigation - -------------------------------------------------------------------------- */ -.top-jump-nav { - position: sticky; - top: 0; - z-index: 50; - display: flex; - align-items: center; - gap: .5rem .75rem; - padding: .5rem 1rem; - margin: 0 0 1rem 0; - - background: var(--card-bg); - border: 1px solid rgba(255,255,255,.08); - box-shadow: 0 4px 14px rgba(0,0,0,.25); - border-radius: 10px; - - overflow-x: auto; - white-space: nowrap; - -webkit-overflow-scrolling: touch; -} -.top-jump-nav a { - display: inline-block; - padding: .4rem .75rem; - border: 1px solid rgba(255,255,255,.12); - border-radius: 999px; - text-decoration: none; - font-size: .95rem; - line-height: 1; - color: inherit; - opacity: .95; -} -.top-jump-nav a:hover, -.top-jump-nav a:focus { - opacity: 1; - background: rgba(255,255,255,.06); - outline: none; -} -.top-jump-nav a.active { - background: var(--accent-pill-bg); - border-color: var(--accent-pill-bd); - box-shadow: inset 0 0 0 1px rgba(59,130,246,.25); -} - -/* --- Titles and structure --- */ -.card-title { margin: 0 0 .5rem; font-size: 1.1rem; } -.section { margin-top: 1rem; } -.section-header { display: flex; gap: .5rem; align-items: baseline; flex-wrap: wrap; } - -/* --- Divider --- */ -.divider { border: 0; border-top: 1px solid #1f2a36; margin: 1rem 0; } - -/* --- Badges / Chips --- */ -.badge { display: inline-block; padding: .15rem .5rem; border-radius: 999px; font-size: .75rem; border: 1px solid transparent; } -.badge-ok { background: #0e3820; border-color: #2c6e49; color: #bff3cf; } -.badge-warn { background: #3d290e; border-color: #9a6b18; color: #ffe2a8; } -.badge-danger { background: #401012; border-color: #a33a42; color: #ffc1c5; } -.badge-muted { background: #111826; border-color: #273447; color: #9fb0c3; } - -.chip { display: inline-block; padding: .1rem .4rem; border: 1px solid #273447; border-radius: 8px; font-size: .75rem; margin-right: .25rem; } -.chip-warn { border-color: #9a6b18; } - -/* --- Text helpers --- */ -.muted { color: #9fb0c3; } -.small { font-size: .8rem; } -.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; } -.prewrap { white-space: pre-wrap; } - -/* --- Lists / details --- */ -.list { margin: .5rem 0; padding-left: 1.1rem; } -.details summary { cursor: pointer; } - -/* --- Grid --- */ -.grid.two { display: grid; grid-template-columns: 1fr; gap: 1rem; } -@media (min-width: 900px) { - .grid.two { grid-template-columns: 1fr 1fr; } -} - -/* --- TLS Matrix --- */ -.tls-matrix { border: 1px solid #1f2a36; border-radius: 10px; overflow: hidden; } -.tls-matrix-row { display: grid; grid-template-columns: 120px 140px 1fr 100px; gap: .5rem; align-items: center; - padding: .5rem .75rem; border-bottom: 1px solid #1f2a36; } -.tls-matrix-row:last-child { border-bottom: none; } - -.tls-cell.version { font-weight: 600; } -.tls-cell.status {} -.tls-cell.cipher {} -.tls-cell.latency { text-align: right; } - - -/* ========================================================================== - 12) Responsive Tweaks - -------------------------------------------------------------------------- */ -@media (max-width: 1200px) { - .forms-table th:nth-child(1) { width: 22rem; } - .forms-table th:nth-child(3) { width: 16rem; } - .forms-table th:nth-child(5) { width: 18rem; } -} - -@media (max-width: 768px) { - main { padding: 1rem; } - - .enrichment-table, - .results-table { - display: block; - overflow-x: auto; - white-space: nowrap; - } -} - -@media (max-width: 640px) { - .top-jump-nav { padding: .4rem .6rem; gap: .4rem .5rem; } - .top-jump-nav a { padding: .35rem .6rem; font-size: .9rem; } -} diff --git a/app/templates/_macros_ssl_tls.html b/app/templates/_macros_ssl_tls.html index 8acf474..12689ce 100644 --- a/app/templates/_macros_ssl_tls.html +++ b/app/templates/_macros_ssl_tls.html @@ -1,99 +1,108 @@ {# templates/_macros_ssl_tls.html #} {% macro ssl_tls_card(ssl_tls) %} -
-

SSL/TLS Intelligence

+
{# -------- 1) Error branch -------- #} - {% if ssl_tls is none or 'error' in ssl_tls %} -
Error
-

SSL/TLS enrichment failed or is unavailable.

- {% if ssl_tls and ssl_tls.error %}
{{ ssl_tls.error }}
{% endif %} + {% if ssl_tls is none or (ssl_tls.error is defined) or ('error' in ssl_tls) %} + Error +

SSL/TLS enrichment failed or is unavailable.

+ {% if ssl_tls and ssl_tls.error %} +
{{ ssl_tls.error }}
+ {% endif %} {# -------- 2) Skipped branch -------- #} {% elif ssl_tls.skipped %} -
Skipped
- {% if ssl_tls.reason %}{{ ssl_tls.reason }}{% endif %} + Skipped + {% if ssl_tls.reason %}{{ ssl_tls.reason }}{% endif %} -
- - -
+
+ Raw TLS JSON +
{{ ssl_tls|tojson(indent=2) }}
+
{# -------- 3) Normal branch (render probe + crt.sh) -------- #} {% else %} {# ===================== LIVE PROBE ===================== #} {% set probe = ssl_tls.probe if ssl_tls else None %} -
-
-

Live TLS Probe

+
+
+

Live TLS Probe

{% if probe %} - Host: {{ probe.hostname }}:{{ probe.port }} + Host: + {{ probe.hostname }}:{{ probe.port }} {% endif %}
{% if not probe %} -

No probe data.

+

No probe data.

{% else %} -
- {% set versions = ['TLS1.0','TLS1.1','TLS1.2','TLS1.3'] %} - {% for v in versions %} - {% set r = probe.results_by_version.get(v) if probe.results_by_version else None %} -
-
{{ v }}
- - {% if r and r.supported %} -
Supported
-
- {% if r.selected_cipher %} - {{ r.selected_cipher }} - {% else %} - cipher: n/a - {% endif %} -
-
- {% if r.handshake_seconds is not none %} - {{ '%.0f' % (r.handshake_seconds*1000) }} ms - {% else %} - - {% endif %} -
- {% else %} -
Not Supported
-
- {% if r and r.error %} - ({{ r.error }}) - {% else %} - - {% endif %} -
-
- {% endif %} -
- {% endfor %} +
+ + + + + + + + + + + {% set versions = ['TLS1.0','TLS1.1','TLS1.2','TLS1.3'] %} + {% for v in versions %} + {% set r = probe.results_by_version.get(v) if probe.results_by_version else None %} + + + + + + + {% endfor %} + +
VersionStatusSelected CipherLatency
{{ v }} + {% if r and r.supported %} + Supported + {% else %} + Not Supported + {% endif %} + + {% if r and r.selected_cipher %} + {{ r.selected_cipher }} + {% elif r and r.error %} + ({{ r.error }}) + {% else %} + + {% endif %} + + {% if r and r.handshake_seconds is not none %} + {{ '%.0f' % (r.handshake_seconds*1000) }} ms + {% else %} + + {% endif %} +
-
+
{% if probe.weak_protocols and probe.weak_protocols|length > 0 %} - Weak Protocols + Weak Protocols {% for wp in probe.weak_protocols %} - {{ wp }} + {{ wp }} {% endfor %} {% endif %} {% if probe.weak_ciphers and probe.weak_ciphers|length > 0 %} - Weak Ciphers + Weak Ciphers {% for wc in probe.weak_ciphers %} - {{ wc }} + {{ wc }} {% endfor %} {% endif %}
{% if probe.errors and probe.errors|length > 0 %} -
- Probe Notes -
    +
    + Probe Notes +
      {% for e in probe.errors %} -
    • {{ e }}
    • +
    • {{ e }}
    • {% endfor %}
    @@ -101,82 +110,86 @@ {% endif %}
-
+
{# ===================== CRT.SH ===================== #} {% set crtsh = ssl_tls.crtsh if ssl_tls else None %} -
-
-

Certificate Transparency (crt.sh)

+
+
+

Certificate Transparency (crt.sh)

{% if crtsh %} - Parsed: - {{ crtsh.hostname or 'n/a' }} + Parsed: + {{ crtsh.hostname or 'n/a' }} {% if crtsh.root_domain %} - • Root: {{ crtsh.root_domain }} - {% if crtsh.is_root_domain %}Root{% else %}Subdomain{% endif %} + • Root: + {{ crtsh.root_domain }} + {% if crtsh.is_root_domain %} + Root + {% else %} + Subdomain + {% endif %} {% endif %} {% endif %}
{% if not crtsh %} -

No CT data.

+

No CT data.

{% else %} -
+
-

Host Certificates

+

Host Certificates

{% set host_certs = crtsh.crtsh.host_certs if 'crtsh' in crtsh and crtsh.crtsh else None %} {% if host_certs and host_certs|length > 0 %} -
    +
      {% for c in host_certs[:10] %} -
    • - {{ c.get('issuer_name','issuer n/a') }} - - {{ c.get('name_value','(name n/a)') }} - • not_before: {{ c.get('not_before','?') }} +
    • + {{ c.get('issuer_name','issuer n/a') }} + + {{ c.get('name_value','(name n/a)') }} + • not_before: {{ c.get('not_before','?') }}
    • {% endfor %}
    {% if host_certs|length > 10 %} -
    (+ {{ host_certs|length - 10 }} more)
    +
    (+ {{ host_certs|length - 10 }} more)
    {% endif %} {% else %} -

    No active host certs found.

    +

    No active host certs found.

    {% endif %}
-

Wildcard on Root

+

Wildcard on Root

{% set wc = crtsh.crtsh.wildcard_root_certs if 'crtsh' in crtsh and crtsh.crtsh else None %} {% if wc and wc|length > 0 %} -
    +
      {% for c in wc[:10] %} -
    • - {{ c.get('issuer_name','issuer n/a') }} - - {{ c.get('name_value','(name n/a)') }} - • not_before: {{ c.get('not_before','?') }} +
    • + {{ c.get('issuer_name','issuer n/a') }} + + {{ c.get('name_value','(name n/a)') }} + • not_before: {{ c.get('not_before','?') }}
    • {% endfor %}
    {% if wc|length > 10 %} -
    (+ {{ wc|length - 10 }} more)
    +
    (+ {{ wc|length - 10 }} more)
    {% endif %} {% else %} -

    No wildcard/root certs found.

    +

    No wildcard/root certs found.

    {% endif %}
{% endif %}
- {# ===================== RAW JSON TOGGLE ===================== #} -
- - -
- + +
+ Raw TLS JSON +
{{ ssl_tls|tojson(indent=2) }}
+
{% endif %} -

Back to top

+

Back to top

{% endmacro %} diff --git a/app/templates/base.html b/app/templates/base.html index faa5870..2d760a9 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,36 +1,76 @@ - +{# Base layout using Tailwind + Flowbite, non-destructive #} + - - - - {{ app_name }} {{ app_version }} - - - - -
-

{{ app_name }} {{ app_version }}

-
+ + + {% block title %}{% endblock %} {{ app_name }} + - {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} -
    - {% for category, message in messages %} -
  • {{ message }}
  • - {% endfor %} + + + + {# Your existing CSS stays; we’ll keep only custom tweaks there. #} + + {% block head %}{% endblock %} + + + + {# Top Navbar (Flowbite collapse) #} + + + {# Page content wrapper #} +
    +
    {% block content %}{% endblock %} -
    +
+ -
- {{ app_name }} - A self-hosted URL analysis sandbox - {{ app_version }} -
- + {# Footer #} +
+

© {{ current_year }} SneakyScope {{ app_name }} {{ app_version }} - A selfhosted URL sandbox

+
+ + {# Flowbite JS (enables collapse) #} + + {% block scripts %}{% endblock %} + - -{% block page_js %} -{% endblock %} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index dd6e0c0..ab6c9f7 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,159 +1,201 @@ -{% extends 'base.html' %} +{% extends "base.html" %} +{% block title %}Home{% endblock %} + {% block content %} + +
- -
-

Analyze a URL

- - + +
+

Start a New Analysis

- - + +
+ + +
- - - - -{% if recent_results %} -
-

Recent Results

- - - - - - - - - - {% for r in recent_results %} - - - - - - {% endfor %} - -
TimestampURLUUID
- {% if r.timestamp %} - {{ r.timestamp }} - {% else %} - N/A - {% endif %} - - - {{ r.final_url or r.submitted_url }} - - - {{ r.uuid }} - -
-
-{% endif %} + + +
+ +
- -
-
-
- Analyzing website… + + {% if recent_results %} +
+

Recent Results

+
+ + + + + + + + + + {% for r in recent_results %} + + + + + + {% endfor %} + +
TimestampURLUUID
+ {% if r.timestamp %}{{ r.timestamp }}{% else %}N/A{% endif %} + + + {{ r.final_url or r.submitted_url }} + + +
+ {{ r.uuid }} + +
+
+
+
+ {% else %} +
+

Recent Results

+

No recent scans.

+
+ {% endif %}
-
- + + {% endblock %} -{% block page_js %} - +{% block scripts %} + // Hide spinner overlay if arriving from bfcache/back + window.addEventListener('pageshow', () => { + hideSpinner(); + if (submitBtn) submitBtn.disabled = false; + if (btnSpinner) btnSpinner.classList.add('hidden'); + }); - - {% endblock %} diff --git a/app/templates/result.html b/app/templates/result.html index 3c07977..d063f08 100644 --- a/app/templates/result.html +++ b/app/templates/result.html @@ -1,352 +1,394 @@ {% extends "base.html" %} {% from "_macros_ssl_tls.html" import ssl_tls_card %} +{% block title %}Scan Results{% endblock %} + {% block content %} +
- + + - - - - - - + - -
-

Enrichment

+ +
+

Enrichment

- {% if enrichment.whois %} -

WHOIS

- - - - - - +

WHOIS

+
+
FieldValue
+ + + + + - {% for k, v in enrichment.whois.items() %} - - - - - {% endfor %} + {% for k, v in enrichment.whois.items() %} + + + + + {% endfor %} -
FieldValue
{{ k.replace('_', ' ').title() }}{{ v }}
{{ k.replace('_', ' ').title() }}{{ v }}
+ +
{% endif %} {% if enrichment.raw_whois %} -

Raw WHOIS

-
{{ enrichment.raw_whois }}
+

Raw WHOIS

+
{{ enrichment.raw_whois }}
{% endif %} - {% if enrichment.geoip %} -

GeoIP

- {% for ip, info in enrichment.geoip.items() %} -
- {{ ip }} - +

GeoIP

+ {% for ip, info in enrichment.geoip.items() %} +
+ {{ ip }} +
+
- {% for key, val in info.items() %} - - - - - {% endfor %} + {% for key, val in info.items() %} + + + + + {% endfor %} -
{{ key.replace('_', ' ').title() }}{{ val }}
{{ key.replace('_', ' ').title() }}{{ val }}
-
- {% endfor %} + +
+ + {% endfor %} {% endif %} {% if not enrichment.whois and not enrichment.raw_whois and not enrichment.geoip and not enrichment.bec_words %} -

No enrichment data available.

+

No enrichment data available.

{% endif %} -

Back to top

-
+

Back to top

+ - -{{ ssl_tls_card(enrichment.ssl_tls) }} + +
+

TLS / Certs

+ {{ ssl_tls_card(enrichment.ssl_tls) }} +
- -
-

Redirects

+ +
+

Redirects

{% if redirects %} - - - - - - +
+
StatusURL
+ + + + + - {% for r in redirects %} - - - - - {% endfor %} + {% for r in redirects %} + + + + + {% endfor %} -
StatusURL
{{ r.status }}{{ r.url }}
{{ r.status }} + {{ r.url }} +
+ +
{% else %} -

No redirects detected.

+

No redirects detected.

{% endif %} -

Back to top

-
+

Back to top

+ - -
-

Forms

+ +
+

Forms

- {% if forms and forms|length > 0 %} - - - - - - - - - - - - {% for f in forms %} - - - + {% if forms and forms|length > 0 %} +
+
ActionMethodInputsMatches (Rules)Form Snippet
- {% if f.action %} - {{ f.action[:25] }}{% if f.action|length > 25 %}…{% endif %} - {% else %} - (no action) - {% endif %} -
+ + + + + + + + + + + {% for f in forms %} + + + - - + + - - + {% else %} + None + {% endif %} + - - + {% else %} + N/A + {% endif %} + - - - - {% endfor %} - -
ActionMethodInputsMatches (Rules)Form Snippet
+ {% if f.action %} + {{ f.action[:80] }}{% if f.action|length > 80 %}…{% endif %} + {% else %} + (no action) + {% endif %} + {{ (f.method or 'get')|upper }}{{ (f.method or 'get')|upper }} - {% if f.inputs and f.inputs|length > 0 %} -
+ +
+ {% if f.inputs and f.inputs|length > 0 %} +
{% for inp in f.inputs %} - - {{ inp.name or '(unnamed)' }} : {{ (inp.type or 'text') }} + + {{ inp.name or '(unnamed)' }} : {{ (inp.type or 'text') }} {% endfor %}
- {% else %} - None - {% endif %} -
- {% if f.rules and f.rules|length > 0 %} -
    + +
+ {% if f.rules and f.rules|length > 0 %} +
    {% for r in f.rules %} -
  • - {{ r.name }} - {% if r.severity %} - {{ r.severity|title }} - {% endif %} - {% if r.tags %} - {% for t in r.tags %} - {{ t }} - {% endfor %} - {% endif %} - {% if r.description %} - — {{ r.description }} - {% endif %} -
  • +
  • + {{ r.name }} + {% if r.severity %} + {% set sev = r.severity|lower %} + + {{ r.severity|title }} + + {% endif %} + {% if r.tags %} + {% for t in r.tags %} + {{ t }} + {% endfor %} + {% endif %} + {% if r.description %} + — {{ r.description }} + {% endif %} +
  • {% endfor %}
- {% else %} - N/A - {% endif %} -
- {% if f.content_snippet %} -
- View snippet ({{ f.content_snippet|length }} chars) -
{{ f.content_snippet }}
-
- {% else %} - N/A - {% endif %} -
- {% else %} -

No form issues detected.

- {% endif %} + + + {% if f.content_snippet %} +
+ + View snippet ({{ f.content_snippet|length }} chars) + +
{{ f.content_snippet }}
+
+ {% else %} + N/A + {% endif %} + + + {% endfor %} + + +
+ {% else %} +

No form issues detected.

+ {% endif %} -

Back to top

- +

Back to top

+ + +
+

Suspicious Scripts

- -
-

Suspicious Scripts

+ {% if suspicious_scripts %} +
+ + + + + + + + + + + {% for s in suspicious_scripts %} + + - {% if suspicious_scripts %} -
TypeSource URLMatches (Rules & Heuristics)Content Snippet
{{ s.type or 'unknown' }}
- - - - - - - - - - {% for s in suspicious_scripts %} - - - + - - + + + + + + + {% endfor %} + +
TypeSource URLMatches (Rules & Heuristics)Content Snippet
{{ s.type or 'unknown' }} + {% if s.src %} + + {{ s.src[:100] }}{% if s.src|length > 100 %}…{% endif %} + + {% else %} + N/A + {% endif %} + - {% if s.src %} - {{ s.src[:50] }} - {% else %} N/A {% endif %} - + {% set has_rules = s.rules and s.rules|length > 0 %} + {% set has_heur = s.heuristics and s.heuristics|length > 0 %} - - - {% set has_rules = s.rules and s.rules|length > 0 %} - {% set has_heur = s.heuristics and s.heuristics|length > 0 %} + {% if has_rules %} +
Rules
+
    + {% for r in s.rules %} +
  • + {{ r.name }} + {% if r.severity %} + {% set sev = r.severity|lower %} + + {{ r.severity|title }} + + {% endif %} + {% if r.tags %} + {% for t in r.tags %} + {{ t }} + {% endfor %} + {% endif %} + {% if r.description %} + — {{ r.description }} + {% endif %} +
  • + {% endfor %} +
+ {% endif %} - {% if has_rules %} - Rules -
    - {% for r in s.rules %} -
  • - {{ r.name }} - {% if r.severity %} - {{ r.severity|title }} + {% if has_heur %} +
    Heuristics
    +
      + {% for h in s.heuristics %} +
    • {{ h }}
    • + {% endfor %} +
    + {% endif %} + + {% if not has_rules and not has_heur %} + N/A + {% endif %} +
+ {% if s.content_snippet %} +
+ + View snippet ({{ s.content_snippet|length }} chars) + +
{{ s.content_snippet }}
+
+ {% else %} + {% if s.type == 'external' and s.src %} + + {% else %} + N/A {% endif %} - {% if r.tags %} - {% for t in r.tags %} - {{ t }} - {% endfor %} - {% endif %} - {% if r.description %} - — {{ r.description }} - {% endif %} - - {% endfor %} - - {% endif %} + {% endif %} +
+
- {% if has_heur %} - Heuristics - - {% endif %} + {% else %} +

No suspicious scripts detected.

+ {% endif %} - {% if not has_rules and not has_heur %} - N/A - {% endif %} - +

Back to top

+
- - - {% if s.content_snippet %} -
- View snippet ({{ s.content_snippet|length }} chars) -
{{ s.content_snippet }}
-
- {% else %} - {% if s.type == 'external' and s.src %} - - {% else %} - N/A - {% endif %} - {% endif %} - - - {% endfor %} - - + +
+

Screenshot

+ Screenshot +

Back to top

+
+ +
+

Source

+

+ + View Source + +

+

Back to top

+
- {% else %} -

No suspicious scripts detected.

- {% endif %} - -

Back to top

- - - -
-

Screenshot

- Screenshot -

Back to top

-
- - -
-

Source

-

View Source

-

Back to top

-
- {% endblock %} -{% block page_js %} +{% block scripts %} - - - - - -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/app/templates/viewer.html b/app/templates/viewer.html index a18f3b2..39a8a47 100644 --- a/app/templates/viewer.html +++ b/app/templates/viewer.html @@ -1,25 +1,44 @@ {% extends "base.html" %} +{% block title %}Source Viewer{% endblock %} + {% block content %} -
-
-
-

Code Viewer

-
- File: {{ filename }} +
+
+
+
+

Source Viewer

+
+ File: {{ filename }} +
-
-
- - - Open raw - Download -
-
+
+ + + + Open raw + + + Download + +
+ -
-
+
+ +
+
+{% endblock %} +{% block scripts %} @@ -36,13 +55,18 @@ if (!name) return 'plaintext'; const m = name.toLowerCase().match(/\.([a-z0-9]+)$/); const ext = m ? m[1] : ''; - const map = {js:'javascript',mjs:'javascript',cjs:'javascript',ts:'typescript',json:'json', - html:'html',htm:'html',css:'css',py:'python',sh:'shell',bash:'shell', - yml:'yaml',yaml:'yaml',md:'markdown',txt:'plaintext',log:'plaintext'}; + const map = { + js:'javascript', mjs:'javascript', cjs:'javascript', + ts:'typescript', json:'json', + html:'html', htm:'html', css:'css', + py:'python', sh:'shell', bash:'shell', + yml:'yaml', yaml:'yaml', + md:'markdown', txt:'plaintext', log:'plaintext' + }; return map[ext] || 'plaintext'; } - // Wait until the AMD loader has defined window.require + // Wait until the AMD loader defines window.require function waitForRequire(msLeft = 5000) { return new Promise((resolve, reject) => { const t0 = performance.now(); @@ -90,15 +114,23 @@ // Buttons document.getElementById('copyBtn')?.addEventListener('click', async () => { - try { await navigator.clipboard.writeText(editor.getValue()); statusEl.textContent = 'Copied.'; } - catch (e) { statusEl.textContent = 'Copy failed: ' + e; } + try { + await navigator.clipboard.writeText(editor.getValue()); + statusEl.textContent = 'Copied.'; + } catch (e) { + statusEl.textContent = 'Copy failed: ' + e; + } }); + document.getElementById('wrapBtn')?.addEventListener('click', () => { const opts = editor.getRawOptions(); editor.updateOptions({ wordWrap: opts.wordWrap === 'on' ? 'off' : 'on' }); }); - statusEl.textContent = (resp.ok ? '' : `Warning: HTTP ${resp.status}`) + (text.length ? '' : ' (empty file)'); + // Status + const warn = resp.ok ? '' : `Warning: HTTP ${resp.status}`; + const empty = text.length ? '' : (warn ? ' · empty file' : 'Empty file'); + statusEl.textContent = warn + (warn && empty ? '' : '') + (empty || ''); }); } catch (err) { statusEl.textContent = 'Viewer error: ' + err.message; diff --git a/assets/input.css b/assets/input.css new file mode 100644 index 0000000..e84ecb0 --- /dev/null +++ b/assets/input.css @@ -0,0 +1,6 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Optional tiny custom classes */ +.card-shadow { box-shadow: 0 1px 2px rgba(0,0,0,.2); } diff --git a/docs/roadmap.md b/docs/roadmap.md index 36554cc..88d015f 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,20 +1,21 @@ # SneakyScope — Roadmap (Updated 8-21-25) ## Priority 1 – Core Analysis / Stability +* why are the rules not finding anything now? +* 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. -*(no open items currently tracked in this bucket)* +## 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 2 – API Layer +## 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 3 – UI / UX - -* Rules Lab (WYSIWYG tester): paste a rule, validate/compile, run against sample text; lightweight nav entry. - ## Priority 4 – Artifact Management & Ops * Retention/cleanup policy for old artifacts (age/size thresholds). @@ -23,11 +24,11 @@ ## Priority 5 – Extras / Integrations -* Bulk URL analysis (batch/queue). -* Optional: analyst verdict tags and export (CSV/JSON). * 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 \ No newline at end of file diff --git a/tailwind/package.json b/tailwind/package.json new file mode 100644 index 0000000..cd2b39a --- /dev/null +++ b/tailwind/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "devDependencies": { + "tailwindcss": "^3.4.10" + } +} diff --git a/tailwind/tailwind.config.js b/tailwind/tailwind.config.js new file mode 100644 index 0000000..6c152ec --- /dev/null +++ b/tailwind/tailwind.config.js @@ -0,0 +1,16 @@ +module.exports = { + content: [ + "./app/templates/**/*.html", + "./app/static/**/*.js", + ], + theme: { + extend: { + colors: { + bg: "#0b0f14", + nav: "#0f1720", + card: "#111826", + }, + }, + }, + plugins: [], +};