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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 归一化配置键名。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user