4a1d949e41
完整 Web 管理平台,涵盖: P0 生产加固: - 会话超时(空闲+绝对) + CSRF 防护 + 登录失败限流 - HTTPS/TLS 证书管理(自签生成/上传/删除/过期提醒) - SSL Bump (HTTPS 透明代理) 可视化配置 - 配置文件 diff 预览 + 二次确认才写入 - 服务控制二次确认 + 操作期间按钮锁定 P1 运维能力: - 日志轮转管理(logrotate) + 日志状态监控 - 告警规则(5xx/命中率/磁盘/客户端流量) - 多 Squid 实例管理 + 一键切换 - squidclient mgr 实时性能指标 - 流量异常检测(突增/大文件/高频小包/新客户端) P2 日志分析增强: - 日志持久化到 SQLite + 历史时间范围查询 - 导出 CSV/JSON(日志/KPI/异常/审计) - GeoIP 地图(Leaflet + ip-api.com 离线可选 GeoLite2) - 每客户端 24h 趋势图 - 自定义 Squid logformat 解析器 P3 体验锦上添花: - 暗/亮主题切换(cookie 持久化) - WebSSH 终端(xterm.js + paramiko) - 完整审计日志 + 用户管理 技术栈: Flask 3.0 + SQLAlchemy 2.0 + SQLite + Chart.js + Leaflet 总代码量: ~16500 行 (15 Python 模块 + 34 模板 + CSS) 路由数: 73
277 lines
10 KiB
HTML
277 lines
10 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}流量异常 - Squid Manager{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="page-header">
|
||
<h1>⚠️ 流量异常检测</h1>
|
||
<div class="page-actions">
|
||
<span class="meta-text">
|
||
{% if summary.header_stats %}
|
||
当前窗口: {{ summary.header_stats.current_window_count }} 条 ·
|
||
{{ summary.header_stats.current_window_clients }} 个客户端
|
||
{% else %}
|
||
暂无日志数据
|
||
{% endif %}
|
||
</span>
|
||
<span class="btn-group" style="display:inline-flex; gap:4px;">
|
||
<a href="{{ url_for('export_anomalies_csv') }}"
|
||
class="btn btn-secondary btn-small" title="导出异常列表为 CSV">⬇️ CSV</a>
|
||
<a href="{{ url_for('export_anomalies_json') }}"
|
||
class="btn btn-secondary btn-small" title="导出异常列表为 JSON">⬇️ JSON</a>
|
||
</span>
|
||
<button onclick="refreshAnomalies()" class="btn btn-small btn-secondary">🔄 刷新</button>
|
||
</div>
|
||
</div>
|
||
|
||
{% if entries_count == 0 %}
|
||
<div class="card">
|
||
<div class="card-empty">
|
||
<div class="empty-icon">🕵️</div>
|
||
<h2>暂无日志数据</h2>
|
||
<p>请先到 <a href="{{ url_for('logs_import') }}">日志导入</a> 或
|
||
<a href="{{ url_for('config_paths') }}">路径设置</a> 配置 access.log。</p>
|
||
</div>
|
||
</div>
|
||
{% else %}
|
||
|
||
<!-- Thresholds (collapsible) -->
|
||
<div class="card">
|
||
<details>
|
||
<summary class="card-title" style="cursor:pointer; margin:0;">
|
||
⚙️ 阈值设置 (显示用, 修改后请回到此页面点击刷新)
|
||
</summary>
|
||
<form method="get" action="{{ url_for('anomalies_view') }}" class="filter-form"
|
||
style="margin-top:12px; display:flex; gap:12px; flex-wrap:wrap; align-items:flex-end;">
|
||
<div>
|
||
<label class="form-label">突增比例 (ratio > X 标记异常)</label>
|
||
<input type="number" name="spike_ratio" min="1" step="0.5"
|
||
value="{{ thresholds.spike_warn_ratio }}" class="form-input" style="width:100px;">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">大文件阈值 (MB)</label>
|
||
<input type="number" name="large_mb" min="1" step="1"
|
||
value="{{ thresholds.large_size_mb }}" class="form-input" style="width:100px;">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">小包阈值 (KB)</label>
|
||
<input type="number" name="small_kb" min="0.1" step="0.1"
|
||
value="{{ thresholds.small_size_kb }}" class="form-input" style="width:100px;">
|
||
</div>
|
||
<div>
|
||
<label class="form-label">高频小包最小请求数</label>
|
||
<input type="number" name="min_small_req" min="1" step="1"
|
||
value="{{ thresholds.min_small_requests }}" class="form-input" style="width:120px;">
|
||
</div>
|
||
<div>
|
||
<button type="submit" class="btn btn-primary btn-small">应用</button>
|
||
</div>
|
||
<div class="meta-text" style="width:100%;">
|
||
当前 critical 阈值 = {{ thresholds.spike_crit_ratio }}x (固定) ·
|
||
基线窗口 = 前 80%, 当前窗口 = 后 20%
|
||
</div>
|
||
</form>
|
||
</details>
|
||
</div>
|
||
|
||
<!-- Summary KPI -->
|
||
<div class="kpi-grid">
|
||
<div class="kpi-card kpi-red">
|
||
<div class="kpi-label">突增客户端</div>
|
||
<div class="kpi-value">{{ summary.anomalous_clients | length }}</div>
|
||
<div class="kpi-meta">请求速率突增 > {{ thresholds.spike_warn_ratio }}x</div>
|
||
</div>
|
||
<div class="kpi-card kpi-purple">
|
||
<div class="kpi-label">大文件请求</div>
|
||
<div class="kpi-value">{{ summary.large_requests | length }}</div>
|
||
<div class="kpi-meta">单次响应 > {{ thresholds.large_size_mb }} MB</div>
|
||
</div>
|
||
<div class="kpi-card kpi-orange">
|
||
<div class="kpi-label">高频小包客户端</div>
|
||
<div class="kpi-value">{{ summary.high_freq_small | length }}</div>
|
||
<div class="kpi-meta">响应 < {{ thresholds.small_size_kb }} KB 且请求数 ≥ {{ thresholds.min_small_requests }}</div>
|
||
</div>
|
||
<div class="kpi-card kpi-blue">
|
||
<div class="kpi-label">首次出现客户端</div>
|
||
<div class="kpi-value">{{ summary.new_clients | length }}</div>
|
||
<div class="kpi-meta">本日志切片内首次见到的客户端</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 1. Spikes -->
|
||
<div class="card">
|
||
<h3 class="card-title">🔥 突增客户端
|
||
<span class="meta-text">({{ summary.anomalous_clients | length }} 条)</span>
|
||
</h3>
|
||
{% if summary.anomalous_clients %}
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>客户端</th>
|
||
<th>基线请求/分</th>
|
||
<th>当前请求/分</th>
|
||
<th>基线总数</th>
|
||
<th>当前总数</th>
|
||
<th>倍数</th>
|
||
<th>严重程度</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for a in summary.anomalous_clients %}
|
||
<tr>
|
||
<td><a href="{{ url_for('logs_view', client=a.client) }}">{{ a.client }}</a></td>
|
||
<td>{{ '%.2f' % a.baseline_rpm }}</td>
|
||
<td>{{ '%.2f' % a.current_rpm }}</td>
|
||
<td>{{ a.baseline_count }}</td>
|
||
<td>{{ a.current_count }}</td>
|
||
<td>
|
||
{% if a.ratio == 'inf' or a.ratio == 'Infinity' or (a.ratio is number and a.ratio > 1e15) %}
|
||
∞ (基线为 0)
|
||
{% else %}
|
||
{{ '%.2f' % a.ratio }}x
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<span class="badge badge-{% if a.severity == 'critical' %}red{% else %}yellow{% endif %}">
|
||
{{ a.severity }}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="meta-text">未检测到异常突增客户端。</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 2. Large requests -->
|
||
<div class="card">
|
||
<h3 class="card-title">💾 异常大文件请求
|
||
<span class="meta-text">({{ summary.large_requests | length }} 条)</span>
|
||
</h3>
|
||
{% if summary.large_requests %}
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>时间</th>
|
||
<th>客户端</th>
|
||
<th>大小</th>
|
||
<th>方法</th>
|
||
<th>状态</th>
|
||
<th>URL</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in summary.large_requests %}
|
||
<tr>
|
||
<td><code>{{ r.time }}</code></td>
|
||
<td><a href="{{ url_for('logs_view', client=r.client) }}">{{ r.client }}</a></td>
|
||
<td>
|
||
<strong>{{ '%.2f' % r.size_mb }} MB</strong>
|
||
<span class="meta-text">({{ r.size_bytes | thousands }} B)</span>
|
||
</td>
|
||
<td><code>{{ r.method }}</code></td>
|
||
<td><code>{{ r.result }}</code></td>
|
||
<td class="url-cell" title="{{ r.url }}">
|
||
<a href="{{ url_for('logs_view', url=r.url) }}">{{ r.url }}</a>
|
||
{% if r.host %}<span class="meta-text">[{{ r.host }}]</span>{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="meta-text">未检测到 > {{ thresholds.large_size_mb }} MB 的请求。</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 3. High-frequency small packets -->
|
||
<div class="card">
|
||
<h3 class="card-title">📊 高频小包客户端
|
||
<span class="meta-text">({{ summary.high_freq_small | length }} 个)</span>
|
||
</h3>
|
||
{% if summary.high_freq_small %}
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>客户端</th>
|
||
<th>请求数</th>
|
||
<th>总流量</th>
|
||
<th>平均大小</th>
|
||
<th>示例主机</th>
|
||
<th>示例 URL</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for c in summary.high_freq_small %}
|
||
<tr>
|
||
<td><a href="{{ url_for('logs_view', client=c.client) }}">{{ c.client }}</a></td>
|
||
<td><strong>{{ c.request_count | thousands }}</strong></td>
|
||
<td>{{ c.total_bytes | thousands }} B</td>
|
||
<td>{{ '%.0f' % c.avg_size }} B</td>
|
||
<td>{{ c.sample_host or '-' }}</td>
|
||
<td class="url-cell" title="{{ c.sample_url }}">
|
||
<a href="{{ url_for('logs_view', url=c.sample_url) }}">{{ c.sample_url }}</a>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="meta-text">
|
||
未检测到符合条件 (< {{ thresholds.small_size_kb }} KB × ≥ {{ thresholds.min_small_requests }} 次) 的客户端。
|
||
</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- 4. New clients -->
|
||
<div class="card">
|
||
<h3 class="card-title">👋 首次出现客户端
|
||
<span class="meta-text">({{ summary.new_clients | length }} 个)</span>
|
||
</h3>
|
||
{% if summary.new_clients %}
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th>客户端</th>
|
||
<th>首次出现时间</th>
|
||
<th>本日志段请求数</th>
|
||
<th>示例 URL</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for c in summary.new_clients %}
|
||
<tr>
|
||
<td><a href="{{ url_for('logs_view', client=c.client) }}">{{ c.client }}</a></td>
|
||
<td><code>{{ c.first_seen }}</code></td>
|
||
<td>{{ c.request_count | thousands }}</td>
|
||
<td class="url-cell" title="{{ c.sample_url }}">
|
||
<a href="{{ url_for('logs_view', url=c.sample_url) }}">{{ c.sample_url }}</a>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<p class="meta-text">未检测到新出现的客户端。</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% endif %}
|
||
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
function refreshAnomalies() {
|
||
fetch('{{ url_for("anomalies_refresh") }}', { method: 'POST' })
|
||
.then(r => r.json())
|
||
.then(d => {
|
||
if (d.ok) location.reload();
|
||
else alert('刷新失败: ' + (d.message || ''));
|
||
})
|
||
.catch(e => alert('网络错误: ' + e));
|
||
}
|
||
</script>
|
||
{% endblock %}
|