|
|
|
@@ -0,0 +1,125 @@
|
|
|
|
|
<div id="security-content"
|
|
|
|
|
hx-get="{{ url_for('security.security') }}"
|
|
|
|
|
hx-trigger="change from:#security-filter-form"
|
|
|
|
|
hx-include="#security-filter-form"
|
|
|
|
|
hx-target="#security-content"
|
|
|
|
|
hx-swap="outerHTML"
|
|
|
|
|
data-from="{{ from_date.isoformat() }}"
|
|
|
|
|
data-to="{{ to_date.isoformat() }}"
|
|
|
|
|
data-severity="{{ severity }}"
|
|
|
|
|
data-rule-type="{{ rule_type }}">
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-wrap items-end justify-between gap-4 mb-6">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="font-display font-bold text-xl">Suspicious Requests & IP History</h1>
|
|
|
|
|
<p class="text-sm text-muted dark:text-muted-dark">Who's poking at your site, and how hard</p>
|
|
|
|
|
</div>
|
|
|
|
|
<form id="security-filter-form" class="flex flex-wrap gap-3 items-end">
|
|
|
|
|
<label class="text-sm text-muted dark:text-muted-dark">From
|
|
|
|
|
<input type="date" name="from" value="{{ from_date.isoformat() }}" class="block border border-line dark:border-line-dark rounded-md px-2 py-1 mt-1 bg-surface dark:bg-surface-dark text-ink dark:text-ink-dark font-data text-sm">
|
|
|
|
|
</label>
|
|
|
|
|
<label class="text-sm text-muted dark:text-muted-dark">To
|
|
|
|
|
<input type="date" name="to" value="{{ to_date.isoformat() }}" class="block border border-line dark:border-line-dark rounded-md px-2 py-1 mt-1 bg-surface dark:bg-surface-dark text-ink dark:text-ink-dark font-data text-sm">
|
|
|
|
|
</label>
|
|
|
|
|
<label class="text-sm text-muted dark:text-muted-dark">Severity
|
|
|
|
|
<select name="severity" class="block border border-line dark:border-line-dark rounded-md px-2 py-1 mt-1 bg-surface dark:bg-surface-dark text-ink dark:text-ink-dark text-sm">
|
|
|
|
|
<option value="" {{ 'selected' if not severity }}>All</option>
|
|
|
|
|
<option value="low" {{ 'selected' if severity == 'low' }}>Low</option>
|
|
|
|
|
<option value="medium" {{ 'selected' if severity == 'medium' }}>Medium</option>
|
|
|
|
|
<option value="high" {{ 'selected' if severity == 'high' }}>High</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<label class="text-sm text-muted dark:text-muted-dark">Rule Type
|
|
|
|
|
<select name="rule_type" class="block border border-line dark:border-line-dark rounded-md px-2 py-1 mt-1 bg-surface dark:bg-surface-dark text-ink dark:text-ink-dark text-sm">
|
|
|
|
|
<option value="" {{ 'selected' if not rule_type }}>All</option>
|
|
|
|
|
<option value="sensitive_path" {{ 'selected' if rule_type == 'sensitive_path' }}>Sensitive Path</option>
|
|
|
|
|
<option value="injection" {{ 'selected' if rule_type == 'injection' }}>Injection</option>
|
|
|
|
|
<option value="scanner_ua" {{ 'selected' if rule_type == 'scanner_ua' }}>Scanner UA</option>
|
|
|
|
|
<option value="spoofed_bot" {{ 'selected' if rule_type == 'spoofed_bot' }}>Spoofed Bot</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-6">
|
|
|
|
|
<h3 class="text-xs font-medium uppercase tracking-wide text-muted dark:text-muted-dark mb-2">Suspicious events</h3>
|
|
|
|
|
<div id="suspicious-events-grid" data-endpoint="{{ url_for('security.api_security_events') }}"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-6">
|
|
|
|
|
<h3 class="text-xs font-medium uppercase tracking-wide text-muted dark:text-muted-dark mb-2">Sensitive-path probes</h3>
|
|
|
|
|
<div id="sensitive-paths-panel" class="border border-line dark:border-line-dark rounded-xl bg-surface dark:bg-surface-dark overflow-hidden"
|
|
|
|
|
data-endpoint="{{ url_for('security.api_sensitive_paths') }}"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="mb-6 border border-line dark:border-line-dark rounded-xl p-4 bg-surface dark:bg-surface-dark">
|
|
|
|
|
<h3 class="font-display font-semibold text-sm mb-3">Export blocklist</h3>
|
|
|
|
|
<div class="flex flex-wrap gap-2 items-center">
|
|
|
|
|
<a href="{{ url_for('security.api_export_blocklist') }}"
|
|
|
|
|
class="bg-accent dark:bg-accent-dark text-white dark:text-paper-dark rounded-md px-3 py-1.5 text-sm font-medium hover:opacity-90 transition-opacity">Download new (.txt)</a>
|
|
|
|
|
<a href="{{ url_for('security.api_export_blocklist', all='true') }}"
|
|
|
|
|
class="border border-line dark:border-line-dark rounded-md px-3 py-1.5 text-sm hover:bg-paper dark:hover:bg-paper-dark transition-colors">Re-export all</a>
|
|
|
|
|
<select id="blocklist-format" class="border border-line dark:border-line-dark rounded-md px-2 py-1.5 text-sm bg-surface dark:bg-surface-dark text-ink dark:text-ink-dark" onchange="updateBlocklistLinks(this.value)">
|
|
|
|
|
<option value="plain">Plain IP list</option>
|
|
|
|
|
<option value="htaccess">.htaccess Deny</option>
|
|
|
|
|
<option value="iptables">iptables</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div id="ip-history-modal" class="fixed inset-0 bg-ink/40 dark:bg-ink-dark/60 items-center justify-center empty:hidden flex z-50"></div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function initSecurityWidgets() {
|
|
|
|
|
const root = document.getElementById('security-content');
|
|
|
|
|
const from = root.dataset.from, to = root.dataset.to;
|
|
|
|
|
const severity = root.dataset.severity, ruleType = root.dataset.ruleType;
|
|
|
|
|
|
|
|
|
|
window.initGrid(
|
|
|
|
|
'suspicious-events-grid',
|
|
|
|
|
document.getElementById('suspicious-events-grid').dataset.endpoint,
|
|
|
|
|
[
|
|
|
|
|
{ name: 'Timestamp' },
|
|
|
|
|
{
|
|
|
|
|
name: 'IP',
|
|
|
|
|
formatter: (cell) => window.gridHtml(
|
|
|
|
|
`<button class="text-accent dark:text-accent-dark underline" hx-get="/api/security/ip/${cell}" hx-target="#ip-history-modal" hx-swap="innerHTML">${cell}</button>`
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{ name: 'Path' }, { name: 'Rule Matched' },
|
|
|
|
|
{
|
|
|
|
|
name: 'Severity',
|
|
|
|
|
formatter: (cell) => {
|
|
|
|
|
const tone = { low: 'text-muted dark:text-muted-dark', medium: 'text-warn dark:text-warn-dark', high: 'text-danger dark:text-danger-dark' }[cell] || '';
|
|
|
|
|
return window.gridHtml(`<span class="font-medium ${tone}">${cell}</span>`);
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
{ from, to, severity, rule_type: ruleType },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const pathsEl = document.getElementById('sensitive-paths-panel');
|
|
|
|
|
fetch(`${pathsEl.dataset.endpoint}?from=${from}&to=${to}`)
|
|
|
|
|
.then((r) => r.json())
|
|
|
|
|
.then(({ data }) => {
|
|
|
|
|
pathsEl.innerHTML = data.length
|
|
|
|
|
? `<table class="w-full text-sm font-data">
|
|
|
|
|
<thead><tr class="text-left text-muted dark:text-muted-dark text-xs uppercase tracking-wide bg-surface-raised dark:bg-surface-raised-dark font-sans">
|
|
|
|
|
<th class="px-3 py-2">Path</th><th class="px-3 py-2">Hits</th><th class="px-3 py-2">Distinct IPs</th>
|
|
|
|
|
</tr></thead>
|
|
|
|
|
<tbody class="divide-y divide-line dark:divide-line-dark">${
|
|
|
|
|
data.map((r) => `<tr><td class="px-3 py-2">${r.path}</td><td class="px-3 py-2">${r.hit_count}</td><td class="px-3 py-2">${r.distinct_ip_count}</td></tr>`).join('')
|
|
|
|
|
}</tbody></table>`
|
|
|
|
|
: `<p class="text-sm text-muted dark:text-muted-dark p-4">No sensitive-path probes in range.</p>`;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.updateBlocklistLinks = (fmt) => {
|
|
|
|
|
document.querySelectorAll('a[href*="export-blocklist"]').forEach((a) => {
|
|
|
|
|
const url = new URL(a.href, window.location.origin);
|
|
|
|
|
url.searchParams.set('format', fmt);
|
|
|
|
|
a.href = url.toString();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</div>
|