feat(honeypot): 蜜罐安全状况周期汇总(周报/月报 + 地图 + 邮件)

以周/月为单位汇总蜜罐命中,把来源 IP 通过公共服务查地理位置,画到地图上,
生成 HTML 报告并发邮件。同时补齐 ipinfo 无法回答「这个 IP 在哪儿」的问题。

新增 IP 地理定位层 internal/security/geoip:
- 与 internal/security/ipinfo 正交。ipinfo 走 cdncheck 离线指纹,只认云厂商/CDN/WAF,
  答的是「是不是代理」;肉鸡(家宽、企业宽带、移动网络)必然被判为
  「未识别(普通网络)」,所以「在哪儿」必须另建一套。
- ip-api.com 批量打底(100 个/请求、1.4s 节流)+ 腾讯位置服务精修国内
  (一 IP 一请求,只精修命中数最高的前 N 个,受 ip_geo_tencent_key 控制)。
- Result{Geo, Unresolved, Requests, Failures} 三态。服务明确回答「没有这个地址」
  才允许写失败缓存;请求超时/限流/5xx 一律不写 —— 否则一次抖动会把整批 IP
  钉成「查不到」三天,此后每份报告的定位覆盖率都莫名变成 0。
- addrs 按调用方给的顺序构建(不是 map 遍历):腾讯精修只取前 N 个,乱序会让
  每次运行精修到不同 IP,配额花在长尾上。

报告生成 internal/security/report:
- period:PreviousWeek / PreviousMonth,窗口一律 [start, end) 左闭右开,
  相邻两期不重叠也不漏边界整点;period_key(week:2026-W44)做幂等。
- aggregate:按 shared.LimitKey 合并(IPv4 精确 / IPv6 折 /64);国家、城市、
  路径、每日趋势;显式统计未定位命中数(覆盖率 60% 的图看起来和 100% 一样)。
- mapdraw:自绘等距圆柱投影气泡图。零海岸线零国界 —— 国界/台湾/南海诸岛绘制
  必须符合国家标准,自绘极易画错,真实底图交给腾讯。经纬网 + 度数刻度 +
  标签防重叠(同一都市圈两城市坐标差 0.01°,曾把「AU 55」和「AU 30」叠成
  看起来像 AU 305 的假数字)。
- html:自包含报告页,内嵌 SVG + 腾讯交互地图(无 key 时给申请指引,
  代码里不预置任何 key)。
- countryname:港澳台统一显示「中国香港/中国澳门/中国台湾」,常见国家用中文名。

存储与调度:
- ip_geo_cache(主键 = LimitKey,成功 TTL 90 天 / 明确无数据 TTL 3 天)
- security_report_logs(period_key 唯一索引 → 重启、错过时间窗都能正确补发,
  失败重试上限 5 次后停止)
- honeypot report --period week|month [--to] [--dir] [--no-email]
- web 启动时按配置挂 30 分钟一轮的调度器(与保留期清理共用停止信号,
  defer 移出 if 外,避免「清理关掉、报告开着」时 goroutine 泄漏)

配置([security]):ip_geo_provider=auto|ipapi|tencent|none、ip_geo_tencent_key、
honeypot_report_enabled / _weekly / _monthly / _email / _dir。

顺带修复:
- route/honeypot_test.go 的 captureStore 缺 AggregateSources/AggregatePaths/
  TimestampsBetween,整个 route 包编译不过。
- templates/render_test.go 的 mockLoggedUser 缺 TelegramChatID,3 个渲染测试失败。
This commit is contained in:
tamakyi
2026-10-30 16:40:44 +08:00
parent 08bbd17751
commit d690355cdc
33 changed files with 5198 additions and 1 deletions
+28
View File
@@ -43,6 +43,13 @@ const (
//
// 不放在数据库所在目录:这两者的权限、备份策略、留存周期都应该分开管。
DefaultHoneypotCredDir = "data/honeypot-creds"
// DefaultIPGeoProvider 默认的地理定位数据源:auto(ip-api 打底 + 腾讯精修)。
DefaultIPGeoProvider = "auto"
// DefaultHoneypotReportDir 安全汇总报告文件的默认目录。
//
// 与凭据目录分开:报告里有攻击者 IP 明细,但它是可以外发/归档的材料;
// 凭据目录里是明文账号密码,永不外发。放在一起容易误打包。
DefaultHoneypotReportDir = "data/reports"
)
// sanitizeHoneypot 归一化蜜罐相关配置,修正「未配置」与「显式关闭」的歧义。
@@ -109,4 +116,25 @@ func sanitizeHoneypot() {
if strings.TrimSpace(Security.HoneypotCredDir) == "" {
Security.HoneypotCredDir = DefaultHoneypotCredDir
}
// 地理定位数据源:未配置 → auto。
// 显式写了不认识的值时保持原样,由 geoip 层解析并告警 ——
// 在配置层悄悄改成 auto 会让人以为配置生效了。
if strings.TrimSpace(Security.IPGeoProvider) == "" {
Security.IPGeoProvider = DefaultIPGeoProvider
}
// 周期报告:三个开关都用 HasKey 判定缺省值(同 HoneypotAutoClean 的理由)。
if !sec.HasKey("honeypot_report_enabled") {
Security.HoneypotReportEnabled = true
}
if !sec.HasKey("honeypot_report_weekly") {
Security.HoneypotReportWeekly = true
}
if !sec.HasKey("honeypot_report_monthly") {
Security.HoneypotReportMonthly = true
}
if strings.TrimSpace(Security.HoneypotReportDir) == "" {
Security.HoneypotReportDir = DefaultHoneypotReportDir
}
}
+12
View File
@@ -50,6 +50,18 @@ var keyAliases = map[string]string{
"honeypot_retentionday": "honeypot_retain_days",
"honeypot_keep_days": "honeypot_retain_days",
"honeypot_retain_max_days": "honeypot_retain_days",
// 地理定位与周期报告:这几个键都是新增的,最容易被写成不同的词。
"ipgeo_provider": "ip_geo_provider",
"ip_geolocation_provider": "ip_geo_provider",
"geo_provider": "ip_geo_provider",
"ip_geo_key": "ip_geo_tencent_key",
"ip_geo_tencent_apikey": "ip_geo_tencent_key",
"tencent_map_key": "ip_geo_tencent_key",
"honeypot_report": "honeypot_report_enabled",
"honeypot_report_mail": "honeypot_report_email",
"honeypot_report_to": "honeypot_report_email",
"honeypot_reports_dir": "honeypot_report_dir",
"honeypot_report_path": "honeypot_report_dir",
}
// normalizeKeyName 归一化配置键名。
+36
View File
@@ -152,6 +152,42 @@ var (
// CSPReportURI 违规上报地址(可选)。留空时浏览器只把违规打到 console 不上报;
// 用 report 模式观察时应当配上,否则「观察一周」就只是空谈。
CSPReportURI string `ini:"csp_report_uri"`
// IPGeoProvider 蜜罐/扫描来源 IP 的地理定位数据源。
//
// ⚠️ 与 ipinfo.Lookup(cdncheck 离线指纹)是两件事:那套只认云厂商/CDN/WAF
// 的 ASN,肉鸡(家宽、企业宽带、移动网络)一律显示「未识别(普通网络)」。
// 这里回答的是「这个 IP 在哪儿」,靠的是外部 IP 库。
//
// auto(缺省):ip-api.com 批量打底(全球覆盖、无需 key、一次 100 个),
// 配了 ip_geo_tencent_key 时再用腾讯位置服务精修国内 IP
// (省市 + GCJ-02 坐标)。腾讯的 IP 定位是一 IP 一请求,
// 拿它做批量主干会把 N 个 IP 变成 N 次 HTTP,故只做精修。
// ipapi:只用 ip-api.com
// tencent:只用腾讯位置服务(要求配 key,否则降级为 ipapi)
// none:不查询,报告里只给 IP 不提地理信息(攻击者 IP 一个字节都不外发)
//
// 隐私边界:auto/ipapi 会把攻击者 IP 发给 ip-api.com,tencent 会发给腾讯。
// 本报告只处理「攻击者」地址,不涉及任何用户 IP;不接受这一点请用 none。
IPGeoProvider string `ini:"ip_geo_provider"`
// IPGeoTencentKey 腾讯位置服务 key(lbs.qq.com 申请)。留空则跳过腾讯精修。
// 仅用于服务端 HTTP 请求,不下发到前端 —— 与地图 JS 用的 key 不是一回事。
IPGeoTencentKey string `ini:"ip_geo_tencent_key"`
// HoneypotReportEnabled 是否启用周期性安全状况汇总(周报 / 月报)邮件。
// 缺省 true:报告本身是运维最需要的「有没有人在打、从哪儿打」,
// 而收件人缺失时会 WARN 跳过,不会静默。
HoneypotReportEnabled bool `ini:"honeypot_report_enabled"`
// HoneypotReportWeekly / Monthly 分别控制周报与月报,缺省都开。
HoneypotReportWeekly bool `ini:"honeypot_report_weekly"`
HoneypotReportMonthly bool `ini:"honeypot_report_monthly"`
// HoneypotReportEmail 报告收件人。留空回退 security_alert_email。
// 单独开一个键是为了能把「随时可能发生的封禁告警」与「定期汇总」
// 分投到不同邮箱(例如告警进手机推送邮箱、汇总进归档邮箱)。
HoneypotReportEmail string `ini:"honeypot_report_email"`
// HoneypotReportDir 报告文件(HTML + PNG)的存放目录,缺省 data/reports。
// 报告里有攻击者 IP 明细,目录权限按 0700 创建。
HoneypotReportDir string `ini:"honeypot_report_dir"`
}
Server struct {