Files
squid-manager/templates/anomalies.html
T
Hermes Agent 4a1d949e41 feat: 完整 Squid Web Manager 平台 - P0-P3 全功能
完整 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
2026-07-15 10:19:21 +08:00

277 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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">响应 &lt; {{ thresholds.small_size_kb }} KB 且请求数 &ge; {{ 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">未检测到 &gt; {{ 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">
未检测到符合条件 (&lt; {{ thresholds.small_size_kb }} KB × &ge; {{ 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 %}