feat(ui): migrate to Tailwind (compiled) + Flowbite JS; new navbar/layout; Docker CSS build
- Add multi-stage CSS build that compiles Tailwind into app/static/tw.css
- Add Tailwind config with dark tokens (bg/nav/card) and purge globs
- Add assets/input.css (@tailwind base/components/utilities + small utilities)
- Replace Tailwind CDN + REMOVE Flowbite CSS (keep Flowbite JS only)
- New base_tailwind.html (top navbar, responsive container, {%- block scripts -%})
- Port pages to Tailwind look/feel with wider content column:
- index: single-column form + recent results, fullscreen spinner overlay, copy-UUID
- result: sticky jump list, Tailwind tables/badges, Suspicious Scripts/Forms sections
- viewer: Monaco-based code viewer in Tailwind card, actions (copy/wrap/raw)
- ssl_tls macro: rewritten with Tailwind (details/summary for raw JSON)
- Dockerfile: add css-builder stage and copy built tw.css into /app/app/static
- Remove Flowbite stylesheet to avoid overrides; Flowbite JS loaded with defer
BREAKING CHANGE:
Legacy CSS classes/components (.card, .badge, etc.) are replaced by Tailwind utilities.
All templates now expect tw.css to be served from /static.
This commit is contained in:
@@ -1,99 +1,108 @@
|
||||
{# templates/_macros_ssl_tls.html #}
|
||||
{% macro ssl_tls_card(ssl_tls) %}
|
||||
<div class="card" id="ssl">
|
||||
<h2 class="card-title">SSL/TLS Intelligence</h2>
|
||||
<div class="space-y-4">
|
||||
|
||||
{# -------- 1) Error branch -------- #}
|
||||
{% if ssl_tls is none or 'error' in ssl_tls %}
|
||||
<div class="badge badge-danger">Error</div>
|
||||
<p class="muted">SSL/TLS enrichment failed or is unavailable.</p>
|
||||
{% if ssl_tls and ssl_tls.error %}<pre class="prewrap">{{ ssl_tls.error }}</pre>{% endif %}
|
||||
{% if ssl_tls is none or (ssl_tls.error is defined) or ('error' in ssl_tls) %}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-red-600/20 text-red-300 border-red-700">Error</span>
|
||||
<p class="text-sm text-gray-500">SSL/TLS enrichment failed or is unavailable.</p>
|
||||
{% if ssl_tls and ssl_tls.error %}
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-xs mt-1">{{ ssl_tls.error }}</pre>
|
||||
{% endif %}
|
||||
|
||||
{# -------- 2) Skipped branch -------- #}
|
||||
{% elif ssl_tls.skipped %}
|
||||
<div class="badge badge-muted">Skipped</div>
|
||||
{% if ssl_tls.reason %}<span class="muted small">{{ ssl_tls.reason }}</span>{% endif %}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-gray-700/30 text-gray-300 border-gray-700">Skipped</span>
|
||||
{% if ssl_tls.reason %}<span class="text-gray-400 text-sm ml-2">{{ ssl_tls.reason }}</span>{% endif %}
|
||||
|
||||
<div class="section">
|
||||
<button class="badge badge-muted" data-toggle="tls-raw">Toggle raw</button>
|
||||
<pre id="tls-raw" hidden>{{ ssl_tls|tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">Raw TLS JSON</summary>
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-xs mt-1">{{ ssl_tls|tojson(indent=2) }}</pre>
|
||||
</details>
|
||||
|
||||
{# -------- 3) Normal branch (render probe + crt.sh) -------- #}
|
||||
{% else %}
|
||||
|
||||
{# ===================== LIVE PROBE ===================== #}
|
||||
{% set probe = ssl_tls.probe if ssl_tls else None %}
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h3>Live TLS Probe</h3>
|
||||
<section class="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-base font-semibold">Live TLS Probe</h3>
|
||||
{% if probe %}
|
||||
<span class="muted">Host:</span> <code>{{ probe.hostname }}:{{ probe.port }}</code>
|
||||
<span class="text-gray-400 text-sm">Host:</span>
|
||||
<code class="text-sm">{{ probe.hostname }}:{{ probe.port }}</code>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not probe %}
|
||||
<p class="muted">No probe data.</p>
|
||||
<p class="text-sm text-gray-500">No probe data.</p>
|
||||
{% else %}
|
||||
<div class="tls-matrix">
|
||||
{% 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 %}
|
||||
<div class="tls-matrix-row">
|
||||
<div class="tls-cell version">{{ v }}</div>
|
||||
|
||||
{% if r and r.supported %}
|
||||
<div class="tls-cell status"><span class="badge badge-ok">Supported</span></div>
|
||||
<div class="tls-cell cipher">
|
||||
{% if r.selected_cipher %}
|
||||
<span class="chip">{{ r.selected_cipher }}</span>
|
||||
{% else %}
|
||||
<span class="muted">cipher: n/a</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tls-cell latency">
|
||||
{% if r.handshake_seconds is not none %}
|
||||
<span class="muted">{{ '%.0f' % (r.handshake_seconds*1000) }} ms</span>
|
||||
{% else %}
|
||||
<span class="muted">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="tls-cell status"><span class="badge badge-muted">Not Supported</span></div>
|
||||
<div class="tls-cell cipher">
|
||||
{% if r and r.error %}
|
||||
<span class="muted small">({{ r.error }})</span>
|
||||
{% else %}
|
||||
<span class="muted">—</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="tls-cell latency"><span class="muted">—</span></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="text-gray-400 border-b border-gray-800">
|
||||
<tr>
|
||||
<th class="text-left py-2 pr-4">Version</th>
|
||||
<th class="text-left py-2 pr-4">Status</th>
|
||||
<th class="text-left py-2 pr-4">Selected Cipher</th>
|
||||
<th class="text-left py-2 pr-4">Latency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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 %}
|
||||
<tr class="border-b border-gray-900">
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ v }}</td>
|
||||
<td class="py-2 pr-4">
|
||||
{% if r and r.supported %}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-green-600/20 text-green-300 border-green-700">Supported</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-gray-700/30 text-gray-300 border-gray-700">Not Supported</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
{% if r and r.selected_cipher %}
|
||||
<span class="rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs">{{ r.selected_cipher }}</span>
|
||||
{% elif r and r.error %}
|
||||
<span class="text-gray-500 text-xs">({{ r.error }})</span>
|
||||
{% else %}
|
||||
<span class="text-gray-500">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-4 whitespace-nowrap">
|
||||
{% if r and r.handshake_seconds is not none %}
|
||||
<span class="text-gray-400">{{ '%.0f' % (r.handshake_seconds*1000) }} ms</span>
|
||||
{% else %}
|
||||
<span class="text-gray-500">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flag-row">
|
||||
<div class="flex flex-wrap items-center gap-2 mt-2">
|
||||
{% if probe.weak_protocols and probe.weak_protocols|length > 0 %}
|
||||
<span class="badge badge-warn">Weak Protocols</span>
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-yellow-600/20 text-yellow-300 border-yellow-700">Weak Protocols</span>
|
||||
{% for wp in probe.weak_protocols %}
|
||||
<span class="chip chip-warn">{{ wp }}</span>
|
||||
<span class="rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs">{{ wp }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if probe.weak_ciphers and probe.weak_ciphers|length > 0 %}
|
||||
<span class="badge badge-warn">Weak Ciphers</span>
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-yellow-600/20 text-yellow-300 border-yellow-700">Weak Ciphers</span>
|
||||
{% for wc in probe.weak_ciphers %}
|
||||
<span class="chip chip-warn">{{ wc }}</span>
|
||||
<span class="rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs">{{ wc }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if probe.errors and probe.errors|length > 0 %}
|
||||
<details class="details">
|
||||
<summary>Probe Notes</summary>
|
||||
<ul class="list">
|
||||
<details class="mt-2">
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">Probe Notes</summary>
|
||||
<ul class="list-disc list-inside text-sm text-gray-400 space-y-1 mt-1">
|
||||
{% for e in probe.errors %}
|
||||
<li class="muted small">{{ e }}</li>
|
||||
<li>{{ e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
@@ -101,82 +110,86 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<hr class="divider"/>
|
||||
<hr class="border-gray-800 my-4"/>
|
||||
|
||||
{# ===================== CRT.SH ===================== #}
|
||||
{% set crtsh = ssl_tls.crtsh if ssl_tls else None %}
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h3>Certificate Transparency (crt.sh)</h3>
|
||||
<section class="space-y-2">
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<h3 class="text-base font-semibold">Certificate Transparency (crt.sh)</h3>
|
||||
{% if crtsh %}
|
||||
<span class="muted">Parsed:</span>
|
||||
<code>{{ crtsh.hostname or 'n/a' }}</code>
|
||||
<span class="text-gray-400 text-sm">Parsed:</span>
|
||||
<code class="text-sm">{{ crtsh.hostname or 'n/a' }}</code>
|
||||
{% if crtsh.root_domain %}
|
||||
<span class="muted"> • Root:</span> <code>{{ crtsh.root_domain }}</code>
|
||||
{% if crtsh.is_root_domain %}<span class="badge badge-ok">Root</span>{% else %}<span class="badge badge-muted">Subdomain</span>{% endif %}
|
||||
<span class="text-gray-400 text-sm">• Root:</span>
|
||||
<code class="text-sm">{{ crtsh.root_domain }}</code>
|
||||
{% if crtsh.is_root_domain %}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-green-600/20 text-green-300 border-green-700">Root</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs border bg-gray-700/30 text-gray-300 border-gray-700">Subdomain</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not crtsh %}
|
||||
<p class="muted">No CT data.</p>
|
||||
<p class="text-sm text-gray-500">No CT data.</p>
|
||||
{% else %}
|
||||
<div class="grid two">
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h4 class="muted">Host Certificates</h4>
|
||||
<h4 class="text-gray-400 text-sm mb-2">Host Certificates</h4>
|
||||
{% set host_certs = crtsh.crtsh.host_certs if 'crtsh' in crtsh and crtsh.crtsh else None %}
|
||||
{% if host_certs and host_certs|length > 0 %}
|
||||
<ul class="list">
|
||||
<ul class="space-y-1 text-xs">
|
||||
{% for c in host_certs[:10] %}
|
||||
<li class="mono small">
|
||||
<span class="chip">{{ c.get('issuer_name','issuer n/a') }}</span>
|
||||
<span class="muted"> • </span>
|
||||
<strong>{{ c.get('name_value','(name n/a)') }}</strong>
|
||||
<span class="muted"> • not_before:</span> {{ c.get('not_before','?') }}
|
||||
<li class="font-mono">
|
||||
<span class="rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-[10px]">{{ c.get('issuer_name','issuer n/a') }}</span>
|
||||
<span class="text-gray-500"> • </span>
|
||||
<strong class="break-all">{{ c.get('name_value','(name n/a)') }}</strong>
|
||||
<span class="text-gray-500"> • not_before:</span> {{ c.get('not_before','?') }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if host_certs|length > 10 %}
|
||||
<div class="muted small">(+ {{ host_certs|length - 10 }} more)</div>
|
||||
<div class="text-gray-500 text-xs mt-1">(+ {{ host_certs|length - 10 }} more)</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="muted">No active host certs found.</p>
|
||||
<p class="text-sm text-gray-500">No active host certs found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="muted">Wildcard on Root</h4>
|
||||
<h4 class="text-gray-400 text-sm mb-2">Wildcard on Root</h4>
|
||||
{% set wc = crtsh.crtsh.wildcard_root_certs if 'crtsh' in crtsh and crtsh.crtsh else None %}
|
||||
{% if wc and wc|length > 0 %}
|
||||
<ul class="list">
|
||||
<ul class="space-y-1 text-xs">
|
||||
{% for c in wc[:10] %}
|
||||
<li class="mono small">
|
||||
<span class="chip">{{ c.get('issuer_name','issuer n/a') }}</span>
|
||||
<span class="muted"> • </span>
|
||||
<strong>{{ c.get('name_value','(name n/a)') }}</strong>
|
||||
<span class="muted"> • not_before:</span> {{ c.get('not_before','?') }}
|
||||
<li class="font-mono">
|
||||
<span class="rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-[10px]">{{ c.get('issuer_name','issuer n/a') }}</span>
|
||||
<span class="text-gray-500"> • </span>
|
||||
<strong class="break-all">{{ c.get('name_value','(name n/a)') }}</strong>
|
||||
<span class="text-gray-500"> • not_before:</span> {{ c.get('not_before','?') }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if wc|length > 10 %}
|
||||
<div class="muted small">(+ {{ wc|length - 10 }} more)</div>
|
||||
<div class="text-gray-500 text-xs mt-1">(+ {{ wc|length - 10 }} more)</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="muted">No wildcard/root certs found.</p>
|
||||
<p class="text-sm text-gray-500">No wildcard/root certs found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{# ===================== RAW JSON TOGGLE ===================== #}
|
||||
<div class="section">
|
||||
<button class="badge badge-muted" data-toggle="tls-raw">Toggle raw</button>
|
||||
<pre id="tls-raw" hidden>{{ ssl_tls|tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- Raw JSON toggle -->
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">Raw TLS JSON</summary>
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-xs mt-1">{{ ssl_tls|tojson(indent=2) }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
<p><a href="#top-jump-list">Back to top</a></p>
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
Reference in New Issue
Block a user