refactor(templates): extract sections to includes; feat(css): add reusable components; fix(tables): lock column widths & stop snippet reflow
- Move large sections into partials: - forms → templates/_include_forms.html - scripts → templates/_include_scripts.html (if applicable) - Add Tailwind component classes in assets/input.css: - .badge + variants (.badge-ok, .badge-warn, .badge-danger, .badge-muted, .badge-info, .badge-success, .badge-success-solid) - .chip - .card - Override .border-gray-800 to fixed color (no opacity var) - Stabilize table layouts: - Use table-fixed + <colgroup> with percentage widths - Forms cols: 10% / 10% / 15% / 45% / 25% - Scripts cols: 10% / 20% / 45% / 25% - Remove inner fixed-width wrapper from snippet cells; use w-full + wrapping to prevent column jitter - Update templates to use new badge/chip classes
This commit is contained in:
@@ -1,195 +0,0 @@
|
||||
{# templates/_macros_ssl_tls.html #}
|
||||
{% macro ssl_tls_card(ssl_tls) %}
|
||||
<div class="space-y-4">
|
||||
|
||||
{# -------- 1) Error branch -------- #}
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
|
||||
<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="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-base font-semibold">Live TLS Probe</h3>
|
||||
{% if probe %}
|
||||
<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="text-sm text-gray-500">No probe data.</p>
|
||||
{% else %}
|
||||
<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="flex flex-wrap items-center gap-2 mt-2">
|
||||
{% if probe.weak_protocols and probe.weak_protocols|length > 0 %}
|
||||
<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="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="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="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="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>{{ e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<hr class="border-gray-800 my-4"/>
|
||||
|
||||
{# ===================== CRT.SH ===================== #}
|
||||
{% set crtsh = ssl_tls.crtsh if ssl_tls else None %}
|
||||
<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="text-gray-400 text-sm">Parsed:</span>
|
||||
<code class="text-sm">{{ crtsh.hostname or 'n/a' }}</code>
|
||||
{% if crtsh.root_domain %}
|
||||
<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="text-sm text-gray-500">No CT data.</p>
|
||||
{% else %}
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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="space-y-1 text-xs">
|
||||
{% for c in host_certs[:10] %}
|
||||
<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="text-gray-500 text-xs mt-1">(+ {{ host_certs|length - 10 }} more)</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No active host certs found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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="space-y-1 text-xs">
|
||||
{% for c in wc[:10] %}
|
||||
<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="text-gray-500 text-xs mt-1">(+ {{ wc|length - 10 }} more)</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No wildcard/root certs found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- 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 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 %}
|
||||
58
app/templates/partials/result_enrichment.html
Normal file
58
app/templates/partials/result_enrichment.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!-- /templates/partials/result_enrichment.html -->
|
||||
<section id="enrichment" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Enrichment</h2>
|
||||
|
||||
{% if enrichment.whois %}
|
||||
<h3 class="text-base font-semibold mt-2 mb-2">WHOIS</h3>
|
||||
<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">Field</th>
|
||||
<th class="text-left py-2 pr-4">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for k, v in enrichment.whois.items() %}
|
||||
<tr class="border-b border-gray-900">
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ k.replace('_', ' ').title() }}</td>
|
||||
<td class="py-2 pr-4 break-all">{{ v }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrichment.raw_whois %}
|
||||
<h3 class="text-base font-semibold mt-4 mb-2">Raw WHOIS</h3>
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-sm">{{ enrichment.raw_whois }}</pre>
|
||||
{% endif %}
|
||||
|
||||
{% if enrichment.geoip %}
|
||||
<h3 class="text-base font-semibold mt-4 mb-2">GeoIP</h3>
|
||||
{% for ip, info in enrichment.geoip.items() %}
|
||||
<details class="border border-gray-800 rounded-lg mb-2">
|
||||
<summary class="px-3 py-2 cursor-pointer hover:bg-gray-900/50">{{ ip }}</summary>
|
||||
<div class="px-3 pb-3 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<tbody>
|
||||
{% for key, val in info.items() %}
|
||||
<tr class="border-b border-gray-900">
|
||||
<td class="py-2 pr-4 whitespace-nowrap text-gray-400">{{ key.replace('_', ' ').title() }}</td>
|
||||
<td class="py-2 pr-4 break-all">{{ val }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if not enrichment.whois and not enrichment.raw_whois and not enrichment.geoip and not enrichment.bec_words %}
|
||||
<p class="text-sm text-gray-500">No enrichment data available.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
113
app/templates/partials/result_forms.html
Normal file
113
app/templates/partials/result_forms.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!-- /templates/partials/result_forms.html -->
|
||||
<section id="forms" class="card">
|
||||
<h2 class="text-lg font-semibold mb-3">Forms</h2>
|
||||
|
||||
{% if forms and forms|length > 0 %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full table-fixed text-sm"> <!-- was: min-w-full -->
|
||||
<colgroup>
|
||||
<col class="w-[10%]"> <!-- Action -->
|
||||
<col class="w-[10%]"> <!-- Method -->
|
||||
<col class="w-[15%]"> <!-- Inputs -->
|
||||
<col class="w-[45%]"> <!-- Matches -->
|
||||
<col class="w-[25%]"> <!-- Snippet -->
|
||||
</colgroup>
|
||||
<thead class="text-gray-400 border-b border-gray-800">
|
||||
<tr>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Action</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Method</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Inputs</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Matches (Rules)</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Form Snippet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in forms %}
|
||||
<tr class="border-b border-gray-900 align-top">
|
||||
<!-- Action -->
|
||||
<td class="py-2 pr-4 break-all">
|
||||
{% if f.action %}
|
||||
{{ f.action[:80] }}{% if f.action|length > 80 %}…{% endif %}
|
||||
{% else %}
|
||||
<span class="text-gray-500">(no action)</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Method -->
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ (f.method or 'get')|upper }}</td>
|
||||
|
||||
<!-- Inputs -->
|
||||
<td class="py-2 pr-4 break-words">
|
||||
{% if f.inputs and f.inputs|length > 0 %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for inp in f.inputs %}
|
||||
<span class="chip"
|
||||
title="{{ (inp.name or '') ~ ' : ' ~ (inp.type or 'text') }}">
|
||||
{{ inp.name or '(unnamed)' }}<small class="text-gray-400"> : {{ (inp.type or 'text') }}</small>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="chip">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Matches (Rules) -->
|
||||
<td class="py-2 pr-4 break-words">
|
||||
{% if f.rules and f.rules|length > 0 %}
|
||||
<ul class="space-y-1">
|
||||
{% for r in f.rules %}
|
||||
<li title="{{ r.description or '' }}">
|
||||
{{ r.name }}
|
||||
{% if r.severity %}
|
||||
{% set sev = r.severity|lower %}
|
||||
<span class="ml-2 rounded-full px-2 py-0.5 text-xs border
|
||||
{% if sev == 'high' %} badge badge-danger
|
||||
{% elif sev == 'medium' %} badge badge-warn
|
||||
{% else %} badge badge-info {% endif %}">
|
||||
{{ r.severity|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.tags %}
|
||||
{% for t in r.tags %}
|
||||
<span class="chip" title="Tag: {{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if r.description %}
|
||||
<small class="text-gray-400"> — {{ r.description }}</small>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Form Snippet (let column width control it) -->
|
||||
<td class="py-2 pr-4 align-top">
|
||||
{% if f.content_snippet %}
|
||||
<details>
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">
|
||||
View snippet ({{ f.content_snippet|length }} chars)
|
||||
</summary>
|
||||
<pre class="mt-1 bg-[#0b0f14] border border-gray-800 rounded-lg p-3
|
||||
w-full max-w-full overflow-auto max-h-64
|
||||
whitespace-pre-wrap break-words font-mono text-xs">{{ f.content_snippet }}</pre>
|
||||
</details>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No form issues detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
120
app/templates/partials/result_scripts.html
Normal file
120
app/templates/partials/result_scripts.html
Normal file
@@ -0,0 +1,120 @@
|
||||
<!-- /templates/partials/result_scripts.html -->
|
||||
<section id="scripts" class="card">
|
||||
<h2 class="text-lg font-semibold mb-3">Suspicious Scripts</h2>
|
||||
|
||||
{% if suspicious_scripts %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full table-fixed text-sm">
|
||||
<colgroup>
|
||||
<col class="w-[10%]"> <!-- Type -->
|
||||
<col class="w-[15%]"> <!-- Source -->
|
||||
<col class="w-[45%]"> <!-- Matches -->
|
||||
<col class="w-[30%]"> <!-- Snippet -->
|
||||
</colgroup>
|
||||
<thead class="text-gray-400 border-b border-gray-800">
|
||||
<tr>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Type</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Source URL</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Matches (Rules & Heuristics)</th>
|
||||
<th class="text-left py-2 pr-4 whitespace-normal break-words">Content Snippet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in suspicious_scripts %}
|
||||
<tr class="border-b border-gray-900 align-top">
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ s.type or 'unknown' }}</td>
|
||||
|
||||
<td class="py-2 pr-4 break-all">
|
||||
{% if s.src %}
|
||||
<a href="{{ s.src }}" target="_blank" rel="noopener" class="hover:text-blue-400">
|
||||
{{ s.src[:100] }}{% if s.src|length > 100 %}…{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Matches (Rules & Heuristics) -->
|
||||
<td class="py-2 pr-4 break-words" data-role="matches-cell">
|
||||
{% set has_rules = s.rules and s.rules|length > 0 %}
|
||||
{% set has_heur = s.heuristics and s.heuristics|length > 0 %}
|
||||
|
||||
{% if has_rules %}
|
||||
<div class="mb-1"><strong>Rules</strong></div>
|
||||
<ul class="space-y-1">
|
||||
{% for r in s.rules %}
|
||||
<li title="{{ r.description or '' }}">
|
||||
{{ r.name }}
|
||||
{% if r.severity %}
|
||||
{% set sev = r.severity|lower %}
|
||||
<span class="ml-2 rounded-full px-2 py-0.5 text-xs border
|
||||
{% if sev == 'high' %} badge badge-danger
|
||||
{% elif sev == 'medium' %} badge badge-warn
|
||||
{% else %} badge badge-info {% endif %}">
|
||||
{{ r.severity|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.tags %}
|
||||
{% for t in r.tags %}
|
||||
<span class="chip" title="Tag: {{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if r.description %}
|
||||
<small class="text-gray-400"> — {{ r.description }}</small>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if has_heur %}
|
||||
<div class="mt-2 mb-1"><strong>Heuristics</strong></div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
{% for h in s.heuristics %}
|
||||
<li>{{ h }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if not has_rules and not has_heur %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Content Snippet (let column width control it) -->
|
||||
<td class="py-2 pr-4 align-top" data-role="snippet-cell">
|
||||
{% if s.content_snippet %}
|
||||
<details>
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">
|
||||
View snippet ({{ s.content_snippet|length }} chars)
|
||||
</summary>
|
||||
<pre class="mt-1 bg-[#0b0f14] border border-gray-800 rounded-lg p-3
|
||||
w-full max-w-full overflow-auto max-h-64
|
||||
whitespace-pre-wrap break-words font-mono text-xs">{{ s.content_snippet }}</pre>
|
||||
</details>
|
||||
{% else %}
|
||||
{% if s.type == 'external' and s.src %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn-analyze-snippet inline-flex items-center gap-2 rounded-lg px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs"
|
||||
data-url="{{ s.src }}"
|
||||
data-job="{{ uuid }}">
|
||||
Analyze external script
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No suspicious scripts detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
200
app/templates/partials/result_ssl_tls.html
Normal file
200
app/templates/partials/result_ssl_tls.html
Normal file
@@ -0,0 +1,200 @@
|
||||
|
||||
{# templates/result_ssl_tls.html #}
|
||||
{% macro ssl_tls_card(ssl_tls) %}
|
||||
<section id="ssl" class="card">
|
||||
<h2 class="text-lg font-semibold mb-3">TLS / Certs</h2>
|
||||
<div class="space-y-4">
|
||||
|
||||
{# -------- 1) Error branch -------- #}
|
||||
{% if ssl_tls is none or (ssl_tls.error is defined) or ('error' in ssl_tls) %}
|
||||
<span class="badge badge-danger">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 %}
|
||||
<span class="chip">Skipped</span>
|
||||
{% if ssl_tls.reason %}<span class="text-gray-400 text-sm ml-2">{{ ssl_tls.reason }}</span>{% endif %}
|
||||
|
||||
<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="space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-base font-semibold">Live TLS Probe</h3>
|
||||
{% if probe %}
|
||||
<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="text-sm text-gray-500">No probe data.</p>
|
||||
{% else %}
|
||||
<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="badge badge-success">Supported</span>
|
||||
{% else %}
|
||||
<span class="badge badge-info">Not Supported</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="py-2 pr-4">
|
||||
{% if r and r.selected_cipher %}
|
||||
<span class="badge badge-warn">{{ 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="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>
|
||||
{% for wp in probe.weak_protocols %}
|
||||
<span class="chip">{{ wp }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if probe.weak_ciphers and probe.weak_ciphers|length > 0 %}
|
||||
<span class="badge badge-warn">Weak Ciphers</span>
|
||||
{% for wc in probe.weak_ciphers %}
|
||||
<span class="chip">{{ wc }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if probe.errors and probe.errors|length > 0 %}
|
||||
<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>{{ e }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<hr class="border-gray-800 my-4"/>
|
||||
|
||||
{# ===================== CRT.SH ===================== #}
|
||||
{% set crtsh = ssl_tls.crtsh if ssl_tls else None %}
|
||||
<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="text-gray-400 text-sm">Parsed:</span>
|
||||
<code class="text-sm">{{ crtsh.hostname or 'n/a' }}</code>
|
||||
{% if crtsh.root_domain %}
|
||||
<span class="text-gray-400 text-sm">• Root:</span>
|
||||
<code class="text-sm">{{ crtsh.root_domain }}</code>
|
||||
{% if crtsh.is_root_domain %}
|
||||
<span class="badge badge-success">Root</span>
|
||||
{% else %}
|
||||
<span class="badge badge-info">Subdomain</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not crtsh %}
|
||||
<p class="text-sm text-gray-500">No CT data.</p>
|
||||
{% else %}
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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="space-y-1 text-xs">
|
||||
{% for c in host_certs[:10] %}
|
||||
<li class="font-mono">
|
||||
<span class="chip">{{ 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="text-gray-500 text-xs mt-1">(+ {{ host_certs|length - 10 }} more)</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No active host certs found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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="space-y-1 text-xs">
|
||||
{% for c in wc[:10] %}
|
||||
<li class="font-mono">
|
||||
<span class="chip">{{ 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="text-gray-500 text-xs mt-1">(+ {{ wc|length - 10 }} more)</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No wildcard/root certs found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- 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 class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</div>
|
||||
</section>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros_ssl_tls.html" import ssl_tls_card %}
|
||||
{% block title %}Scan Results{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -41,69 +40,11 @@
|
||||
</section>
|
||||
|
||||
<!-- Enrichment -->
|
||||
<section id="enrichment" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Enrichment</h2>
|
||||
|
||||
{% if enrichment.whois %}
|
||||
<h3 class="text-base font-semibold mt-2 mb-2">WHOIS</h3>
|
||||
<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">Field</th>
|
||||
<th class="text-left py-2 pr-4">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for k, v in enrichment.whois.items() %}
|
||||
<tr class="border-b border-gray-900">
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ k.replace('_', ' ').title() }}</td>
|
||||
<td class="py-2 pr-4 break-all">{{ v }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if enrichment.raw_whois %}
|
||||
<h3 class="text-base font-semibold mt-4 mb-2">Raw WHOIS</h3>
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-sm">{{ enrichment.raw_whois }}</pre>
|
||||
{% endif %}
|
||||
|
||||
{% if enrichment.geoip %}
|
||||
<h3 class="text-base font-semibold mt-4 mb-2">GeoIP</h3>
|
||||
{% for ip, info in enrichment.geoip.items() %}
|
||||
<details class="border border-gray-800 rounded-lg mb-2">
|
||||
<summary class="px-3 py-2 cursor-pointer hover:bg-gray-900/50">{{ ip }}</summary>
|
||||
<div class="px-3 pb-3 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<tbody>
|
||||
{% for key, val in info.items() %}
|
||||
<tr class="border-b border-gray-900">
|
||||
<td class="py-2 pr-4 whitespace-nowrap text-gray-400">{{ key.replace('_', ' ').title() }}</td>
|
||||
<td class="py-2 pr-4 break-all">{{ val }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{% if not enrichment.whois and not enrichment.raw_whois and not enrichment.geoip and not enrichment.bec_words %}
|
||||
<p class="text-sm text-gray-500">No enrichment data available.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
{% include "partials/result_enrichment.html" %}
|
||||
|
||||
<!-- TLS / SSL / CERTS -->
|
||||
<section id="ssl" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">TLS / Certs</h2>
|
||||
{% from "partials/result_ssl_tls.html" import ssl_tls_card %}
|
||||
{{ ssl_tls_card(enrichment.ssl_tls) }}
|
||||
</section>
|
||||
|
||||
<!-- Redirects -->
|
||||
<section id="redirects" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
@@ -136,221 +77,11 @@
|
||||
</section>
|
||||
|
||||
<!-- Forms -->
|
||||
<section id="forms" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Forms</h2>
|
||||
{% include "partials/result_forms.html" %}
|
||||
|
||||
{% if forms and forms|length > 0 %}
|
||||
<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">Action</th>
|
||||
<th class="text-left py-2 pr-4">Method</th>
|
||||
<th class="text-left py-2 pr-4">Inputs</th>
|
||||
<th class="text-left py-2 pr-4">Matches (Rules)</th>
|
||||
<th class="text-left py-2 pr-4">Form Snippet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in forms %}
|
||||
<tr class="border-b border-gray-900 align-top">
|
||||
<!-- Action -->
|
||||
<td class="py-2 pr-4 break-all">
|
||||
{% if f.action %}
|
||||
{{ f.action[:80] }}{% if f.action|length > 80 %}…{% endif %}
|
||||
{% else %}
|
||||
<span class="text-gray-500">(no action)</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Method -->
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ (f.method or 'get')|upper }}</td>
|
||||
|
||||
<!-- Inputs -->
|
||||
<td class="py-2 pr-4">
|
||||
{% if f.inputs and f.inputs|length > 0 %}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{% for inp in f.inputs %}
|
||||
<span class="rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs"
|
||||
title="{{ (inp.name or '') ~ ' : ' ~ (inp.type or 'text') }}">
|
||||
{{ inp.name or '(unnamed)' }}<small class="text-gray-400"> : {{ (inp.type or 'text') }}</small>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-gray-500">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Matches (Rules) -->
|
||||
<td class="py-2 pr-4">
|
||||
{% if f.rules and f.rules|length > 0 %}
|
||||
<ul class="space-y-1">
|
||||
{% for r in f.rules %}
|
||||
<li title="{{ r.description or '' }}">
|
||||
{{ r.name }}
|
||||
{% if r.severity %}
|
||||
{% set sev = r.severity|lower %}
|
||||
<span class="ml-2 rounded-full px-2 py-0.5 text-xs border
|
||||
{% if sev == 'high' %} bg-red-600/20 text-red-300 border-red-700
|
||||
{% elif sev == 'medium' %} bg-yellow-600/20 text-yellow-300 border-yellow-700
|
||||
{% else %} bg-blue-600/20 text-blue-300 border-blue-700 {% endif %}">
|
||||
{{ r.severity|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.tags %}
|
||||
{% for t in r.tags %}
|
||||
<span class="ml-1 rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs" title="Tag: {{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if r.description %}
|
||||
<small class="text-gray-400"> — {{ r.description }}</small>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Form Snippet -->
|
||||
<td class="py-2 pr-4">
|
||||
{% if f.content_snippet %}
|
||||
<details>
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">
|
||||
View snippet ({{ f.content_snippet|length }} chars)
|
||||
</summary>
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-xs mt-1">{{ f.content_snippet }}</pre>
|
||||
</details>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No form issues detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
|
||||
<!-- Suspicious Scripts -->
|
||||
<section id="scripts" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
<h2 class="text-lg font-semibold mb-3">Suspicious Scripts</h2>
|
||||
|
||||
{% if suspicious_scripts %}
|
||||
<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">Type</th>
|
||||
<th class="text-left py-2 pr-4">Source URL</th>
|
||||
<th class="text-left py-2 pr-4">Matches (Rules & Heuristics)</th>
|
||||
<th class="text-left py-2 pr-4">Content Snippet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in suspicious_scripts %}
|
||||
<tr class="border-b border-gray-900 align-top">
|
||||
<td class="py-2 pr-4 whitespace-nowrap">{{ s.type or 'unknown' }}</td>
|
||||
|
||||
<td class="py-2 pr-4 break-all">
|
||||
{% if s.src %}
|
||||
<a href="{{ s.src }}" target="_blank" rel="noopener" class="hover:text-blue-400">
|
||||
{{ s.src[:100] }}{% if s.src|length > 100 %}…{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Matches (Rules & Heuristics) -->
|
||||
<td class="py-2 pr-4" data-role="matches-cell">
|
||||
{% set has_rules = s.rules and s.rules|length > 0 %}
|
||||
{% set has_heur = s.heuristics and s.heuristics|length > 0 %}
|
||||
|
||||
{% if has_rules %}
|
||||
<div class="mb-1"><strong>Rules</strong></div>
|
||||
<ul class="space-y-1">
|
||||
{% for r in s.rules %}
|
||||
<li title="{{ r.description or '' }}">
|
||||
{{ r.name }}
|
||||
{% if r.severity %}
|
||||
{% set sev = r.severity|lower %}
|
||||
<span class="ml-2 rounded-full px-2 py-0.5 text-xs border
|
||||
{% if sev == 'high' %} bg-red-600/20 text-red-300 border-red-700
|
||||
{% elif sev == 'medium' %} bg-yellow-600/20 text-yellow-300 border-yellow-700
|
||||
{% else %} bg-blue-600/20 text-blue-300 border-blue-700 {% endif %}">
|
||||
{{ r.severity|title }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if r.tags %}
|
||||
{% for t in r.tags %}
|
||||
<span class="ml-1 rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs" title="Tag: {{ t }}">{{ t }}</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if r.description %}
|
||||
<small class="text-gray-400"> — {{ r.description }}</small>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if has_heur %}
|
||||
<div class="mt-2 mb-1"><strong>Heuristics</strong></div>
|
||||
<ul class="list-disc list-inside space-y-1">
|
||||
{% for h in s.heuristics %}
|
||||
<li>{{ h }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if not has_rules and not has_heur %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<!-- Content Snippet / Analyze external script -->
|
||||
<td class="py-2 pr-4" data-role="snippet-cell">
|
||||
{% if s.content_snippet %}
|
||||
<details>
|
||||
<summary class="cursor-pointer text-blue-300 hover:underline">
|
||||
View snippet ({{ s.content_snippet|length }} chars)
|
||||
</summary>
|
||||
<pre class="bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-xs mt-1">{{ s.content_snippet }}</pre>
|
||||
</details>
|
||||
{% else %}
|
||||
{% if s.type == 'external' and s.src %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn-analyze-snippet inline-flex items-center gap-2 rounded-lg px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white text-xs"
|
||||
data-url="{{ s.src }}"
|
||||
data-job="{{ uuid }}">
|
||||
Analyze external script
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-500">N/A</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-500">No suspicious scripts detected.</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="mt-2"><a href="#top-jump-list" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
||||
</section>
|
||||
{% include "partials/result_scripts.html" %}
|
||||
|
||||
<!-- Screenshot -->
|
||||
<section id="screenshot" class="bg-card border border-gray-800 rounded-xl p-4">
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
/* ---- Reusable components ---- */
|
||||
@layer components {
|
||||
/* Base badge + variants (compose in markup as: class="badge badge-ok") */
|
||||
.badge { @apply inline-flex items-center rounded-full px-2 py-0.5 text-xs border; }
|
||||
.badge-ok { @apply bg-green-600/20 text-green-300 border-green-700; }
|
||||
.badge-warn { @apply bg-yellow-600/20 text-yellow-300 border-yellow-700; }
|
||||
.badge-danger { @apply bg-red-600/20 text-red-300 border-red-700; }
|
||||
.badge-muted { @apply bg-gray-700/30 text-gray-300 border-gray-700; }
|
||||
.badge-info { @apply bg-blue-600/20 text-blue-300 border-blue-700; }
|
||||
.badge-success { @apply bg-green-600/20 text-green-300 border-green-700; }
|
||||
.badge-success-solid { @apply bg-green-600 text-white border-green-600; }
|
||||
|
||||
|
||||
/* Chips (tags/pills) */
|
||||
.chip { @apply rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs; }
|
||||
|
||||
/* Card container */
|
||||
.card { @apply bg-card border border-gray-800 rounded-xl p-4; }
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
/* Your earlier override to remove opacity var from gray-800 borders */
|
||||
@layer utilities {
|
||||
.border-gray-800 { border-color: #1f2937; } /* rgb(31,41,55) */
|
||||
}
|
||||
|
||||
/* Optional tiny custom classes */
|
||||
.card-shadow { box-shadow: 0 1px 2px rgba(0,0,0,.2); }
|
||||
|
||||
Reference in New Issue
Block a user