moved core app config (name, version) out of settings and into app/app_settings.py added ability to brand SneakyScope to any name added caching of cert information from crt.sh (cache enable and lenght is configurable in settings.yaml) streamlined header/footer loading to be more correct
257 lines
10 KiB
HTML
257 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Scan Results{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-xl md:max-w-3xl lg:max-w-5xl xl:max-w-6xl 2xl:max-w-7xl mx-auto space-y-4">
|
|
|
|
<!-- Top Jump List (sticky) -->
|
|
<nav id="top-jump-list"
|
|
class="sticky top-0 z-20 bg-bg/80 backdrop-blur border-b border-gray-800 py-2 px-3 rounded-b xl:rounded-b-none">
|
|
<div class="flex flex-wrap gap-2 text-sm">
|
|
<a href="{{ url_for('main.index') }}" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Analyse Another Page</a>
|
|
<a href="#url-overview" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">URL Overview</a>
|
|
<a href="#enrichment" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Enrichment</a>
|
|
<a href="#ssl" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">TLS / Certs</a>
|
|
<a href="#redirects" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Redirects</a>
|
|
<a href="#forms" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Forms</a>
|
|
<a href="#scripts" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Suspicious Scripts</a>
|
|
<a href="#sus_text" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Suspicious Text</a>
|
|
<a href="#screenshot" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Screenshot</a>
|
|
<a href="#source" class="px-2 py-1 rounded border border-gray-700 hover:bg-gray-800">Source</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- URL Overview -->
|
|
<section id="url-overview" class="bg-card border border-gray-800 rounded-xl p-4">
|
|
<h2 class="text-lg font-semibold mb-3">URL Overview</h2>
|
|
<div class="space-y-2 text-sm">
|
|
<p><span class="text-gray-400">Submitted URL:</span> <span class="break-all">{{ submitted_url }}</span></p>
|
|
<p>
|
|
<span class="text-gray-400">Final URL:</span>
|
|
<span class="break-all">{{ final_url }}</span>
|
|
</p>
|
|
<p>
|
|
<span class="text-gray-400">Permalink:</span>
|
|
<a href="{{ url_for('main.view_result', run_uuid=uuid, _external=True) }}" class="break-all hover:text-blue-400">
|
|
Permalink for {{ uuid }}
|
|
</a>
|
|
</p>
|
|
<p>
|
|
<span class="text-gray-400">Full Results File:</span>
|
|
<a href="{{ url_for('main.view_artifact', run_uuid=uuid, filename='results.json') }}"
|
|
target="_blank" rel="noopener"
|
|
class="break-all hover:text-blue-400">
|
|
Results File
|
|
</a>
|
|
</p>
|
|
|
|
<p><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Enrichment -->
|
|
{% include "partials/result_enrichment.html" %}
|
|
|
|
<!-- TLS / SSL / CERTS -->
|
|
{% from "partials/result_ssl_tls.html" import ssl_tls_card %}
|
|
{{ ssl_tls_card(enrichment.ssl_tls) }}
|
|
|
|
<!-- Redirects -->
|
|
<section id="redirects" class="bg-card border border-gray-800 rounded-xl p-4">
|
|
<h2 class="text-lg font-semibold mb-3">Redirects</h2>
|
|
{% if redirects %}
|
|
<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">Status</th>
|
|
<th class="text-left py-2 pr-4">URL</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in redirects %}
|
|
<tr class="border-b border-gray-900">
|
|
<td class="py-2 pr-4 whitespace-nowrap">{{ r.status }}</td>
|
|
<td class="py-2 pr-4 break-all">
|
|
<a href="{{ r.url }}" target="_blank" rel="noopener" class="hover:text-blue-400">{{ r.url }}</a>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-sm text-gray-500">No redirects detected.</p>
|
|
{% endif %}
|
|
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
|
</section>
|
|
|
|
<!-- Forms -->
|
|
{% include "partials/result_forms.html" %}
|
|
|
|
<!-- Suspicious Scripts -->
|
|
{% include "partials/result_scripts.html" %}
|
|
|
|
<!-- Suspicious Text -->
|
|
{% include "partials/result_text.html" with context %}
|
|
|
|
<!-- Screenshot -->
|
|
<section id="screenshot" class="bg-card border border-gray-800 rounded-xl p-4">
|
|
<h2 class="text-lg font-semibold mb-3">Screenshot</h2>
|
|
<img src="{{ url_for('main.artifacts', run_uuid=uuid, filename='screenshot.png') }}"
|
|
alt="Screenshot"
|
|
class="w-full rounded-lg border border-gray-800">
|
|
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
|
</section>
|
|
|
|
<!-- Source -->
|
|
<section id="source" class="bg-card border border-gray-800 rounded-xl p-4">
|
|
<h2 class="text-lg font-semibold mb-3">Source</h2>
|
|
<p>
|
|
<a href="{{ url_for('main.view_artifact', run_uuid=uuid, filename='source.html') }}"
|
|
target="_blank" rel="noopener"
|
|
class="inline-flex items-center gap-2 rounded-lg px-3 py-1.5 bg-gray-800 hover:bg-gray-700 border border-gray-700">
|
|
View Source
|
|
</a>
|
|
</p>
|
|
<p class="mt-2"><a href="#url-overview" class="text-sm text-gray-400 hover:text-blue-400">Back to top</a></p>
|
|
</section>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
/**
|
|
* Helpers to parse artifact path and build viewer URL
|
|
*/
|
|
function parseArtifactPath(artifactPath) {
|
|
if (!artifactPath) return { uuid: null, rel: null };
|
|
const norm = String(artifactPath).replace(/\\/g, '/'); // windows -> posix
|
|
const m = norm.match(/\/([0-9a-fA-F-]{36})\/(.+)$/);
|
|
if (!m) return { uuid: null, rel: null };
|
|
return { uuid: m[1], rel: m[2] };
|
|
}
|
|
function buildViewerUrlFromAbsPath(artifactPath) {
|
|
const { uuid, rel } = parseArtifactPath(artifactPath);
|
|
if (!uuid || !rel) return '#';
|
|
const encodedRel = rel.split('/').map(encodeURIComponent).join('/');
|
|
return `/view/artifact/${encodeURIComponent(uuid)}/${encodedRel}`;
|
|
}
|
|
|
|
/**
|
|
* Map severities to Tailwind badge classes
|
|
*/
|
|
function severityClass(sev) {
|
|
switch ((sev || '').toString().toLowerCase()) {
|
|
case 'high': return 'ml-2 rounded-full px-2 py-0.5 text-xs border bg-red-600/20 text-red-300 border-red-700';
|
|
case 'medium': return 'ml-2 rounded-full px-2 py-0.5 text-xs border bg-yellow-600/20 text-yellow-300 border-yellow-700';
|
|
default: return 'ml-2 rounded-full px-2 py-0.5 text-xs border bg-blue-600/20 text-blue-300 border-blue-700';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle "Analyze external script" buttons
|
|
*/
|
|
document.addEventListener('click', async (e) => {
|
|
const btn = e.target.closest('.btn-analyze-snippet');
|
|
if (!btn) return;
|
|
|
|
const row = btn.closest('tr');
|
|
const snippetCell = btn.closest('[data-role="snippet-cell"]') || btn.parentElement;
|
|
const matchesCell = row ? row.querySelector('[data-role="matches-cell"]') : null;
|
|
|
|
const url = btn.dataset.url;
|
|
const job = btn.dataset.job;
|
|
|
|
// Replace button with lightweight loading text
|
|
const loading = document.createElement('span');
|
|
loading.className = 'text-gray-400';
|
|
loading.textContent = 'Analyzing…';
|
|
btn.replaceWith(loading);
|
|
|
|
try {
|
|
const r = await fetch('/api/analyze_script', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ job_id: job, url: url })
|
|
});
|
|
const data = await r.json();
|
|
if (!data.ok) {
|
|
loading.textContent = 'Error: ' + (data.error || 'Unknown');
|
|
return;
|
|
}
|
|
|
|
// Build details with snippet
|
|
const details = document.createElement('details');
|
|
const summary = document.createElement('summary');
|
|
summary.className = 'cursor-pointer text-blue-300 hover:underline';
|
|
summary.textContent = 'View snippet (' + (data.snippet_len ?? (data.snippet || '').length) +
|
|
' chars' + (data.truncated ? ', truncated' : '') +
|
|
', ' + (data.bytes ?? '') + ' bytes)';
|
|
|
|
const pre = document.createElement('pre');
|
|
pre.className = 'bg-[#0b0f14] border border-gray-800 rounded-lg p-3 overflow-x-auto text-xs mt-1';
|
|
pre.textContent = data.snippet || '';
|
|
|
|
const viewerUrl = buildViewerUrlFromAbsPath(data.artifact_path || '');
|
|
const open = document.createElement('a');
|
|
open.href = viewerUrl; open.target = '_blank'; open.rel = 'noopener';
|
|
open.className = 'ml-2 text-sm text-gray-300 hover:text-blue-400';
|
|
open.textContent = 'open in viewer';
|
|
|
|
summary.appendChild(document.createTextNode(' '));
|
|
summary.appendChild(open);
|
|
details.appendChild(summary);
|
|
details.appendChild(pre);
|
|
|
|
loading.replaceWith(details);
|
|
|
|
// Update Matches cell with rule findings
|
|
if (matchesCell) {
|
|
if (Array.isArray(data.findings) && data.findings.length) {
|
|
const frag = document.createDocumentFragment();
|
|
const strong = document.createElement('div');
|
|
strong.className = 'mb-1';
|
|
strong.innerHTML = '<strong>Rules</strong>';
|
|
const ul = document.createElement('ul');
|
|
ul.className = 'space-y-1';
|
|
|
|
data.findings.forEach((f) => {
|
|
const li = document.createElement('li');
|
|
li.textContent = (f.name || 'Rule') + (f.description ? ' — ' + f.description : '');
|
|
if (f.severity) {
|
|
const badge = document.createElement('span');
|
|
badge.className = severityClass(f.severity);
|
|
badge.textContent = String(f.severity).charAt(0).toUpperCase() + String(f.severity).slice(1);
|
|
li.appendChild(document.createTextNode(' '));
|
|
li.appendChild(badge);
|
|
}
|
|
if (Array.isArray(f.tags)) {
|
|
f.tags.forEach((t) => {
|
|
const chip = document.createElement('span');
|
|
chip.className = 'ml-1 rounded-full bg-gray-800 border border-gray-700 text-gray-300 px-2 py-0.5 text-xs';
|
|
chip.title = 'Tag: ' + t;
|
|
chip.textContent = t;
|
|
li.appendChild(document.createTextNode(' '));
|
|
li.appendChild(chip);
|
|
});
|
|
}
|
|
ul.appendChild(li);
|
|
});
|
|
|
|
frag.appendChild(strong);
|
|
frag.appendChild(ul);
|
|
matchesCell.innerHTML = '';
|
|
matchesCell.appendChild(frag);
|
|
} else {
|
|
matchesCell.innerHTML = '<span class="text-gray-500">No rule matches.</span>';
|
|
}
|
|
}
|
|
} catch (err) {
|
|
loading.textContent = 'Request failed: ' + err;
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|