start project

This commit is contained in:
Hemmat
2026-08-07 21:17:17 +03:30
commit ea1e1eead6
121 changed files with 8108 additions and 0 deletions
@@ -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 &amp; 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>
@@ -0,0 +1,26 @@
<div class="bg-surface dark:bg-surface-dark rounded-xl p-6 max-w-lg w-full relative border border-line dark:border-line-dark">
<button class="absolute top-3 right-3 text-muted dark:text-muted-dark hover:text-ink dark:hover:text-ink-dark" onclick="document.getElementById('ip-history-modal').innerHTML=''">
<svg class="w-4 h-4"><use href="/static/dist/icons.svg#x"/></svg>
</button>
<h3 class="font-display font-semibold text-lg mb-3 font-data">{{ ip_data.ip }}</h3>
<dl class="text-sm grid grid-cols-2 gap-y-1.5 mb-4 font-data">
<dt class="text-muted dark:text-muted-dark font-sans">First seen</dt><dd>{{ ip_data.first_seen }}</dd>
<dt class="text-muted dark:text-muted-dark font-sans">Last seen</dt><dd>{{ ip_data.last_seen }}</dd>
<dt class="text-muted dark:text-muted-dark font-sans">Total requests</dt><dd>{{ ip_data.total_requests }}</dd>
<dt class="text-muted dark:text-muted-dark font-sans">Reputation score</dt><dd>{{ ip_data.reputation_score }}</dd>
<dt class="text-muted dark:text-muted-dark font-sans">Flagged</dt>
<dd class="{{ 'text-danger dark:text-danger-dark font-medium' if ip_data.is_flagged else '' }}">{{ 'Yes' if ip_data.is_flagged else 'No' }}</dd>
{% if ip_data.spoofed_bot_names %}
<dt class="text-muted dark:text-muted-dark font-sans">Spoofed bot claims</dt><dd class="text-danger dark:text-danger-dark">{{ ip_data.spoofed_bot_names | join(', ') }}</dd>
{% endif %}
</dl>
<p class="text-xs text-muted dark:text-muted-dark mb-3">{{ ip_data.traffic_window_note }}</p>
<h4 class="font-medium text-sm mb-1">Top paths</h4>
<ul class="text-sm mb-3 font-data text-ink dark:text-ink-dark space-y-0.5">{% for path, count in ip_data.top_paths %}<li>{{ path }} <span class="text-muted dark:text-muted-dark">— {{ count }}</span></li>{% endfor %}</ul>
<h4 class="font-medium text-sm mb-1">Status codes</h4>
<ul class="text-sm mb-3 font-data text-ink dark:text-ink-dark space-y-0.5">{% for code, count in ip_data.status_code_distribution.items() %}<li>{{ code }} <span class="text-muted dark:text-muted-dark">— {{ count }}</span></li>{% endfor %}</ul>
{% if ip_data.recent_suspicious_events %}
<h4 class="font-medium text-sm mb-1">Recent flagged events</h4>
<ul class="text-sm font-data text-ink dark:text-ink-dark space-y-0.5">{% for e in ip_data.recent_suspicious_events %}<li>{{ e.timestamp }} — {{ e.path }} <span class="text-muted dark:text-muted-dark">({{ e.rule_matched }}, {{ e.severity }})</span></li>{% endfor %}</ul>
{% endif %}
</div>
@@ -0,0 +1,4 @@
<div class="bg-surface dark:bg-surface-dark rounded-xl p-6 max-w-sm w-full border border-line dark:border-line-dark">
<p class="text-sm text-ink dark:text-ink-dark">No history found for <span class="font-data">{{ ip }}</span> — it hasn't been seen yet.</p>
<button onclick="document.getElementById('ip-history-modal').innerHTML=''" class="mt-3 text-sm text-accent dark:text-accent-dark hover:underline">Close</button>
</div>
@@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block title %}Security — Kavosh{% endblock %}
{% block content %}
{% include "security/_content.html" %}
{% endblock %}