以周/月为单位汇总蜜罐命中,把来源 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 个渲染测试失败。
136 lines
3.3 KiB
Go
136 lines
3.3 KiB
Go
// Copyright 2026 tamakyi. All rights reserved.
|
|
// Use of this source code is governed by a MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package report
|
|
|
|
import "strings"
|
|
|
|
// countryNames 常见国家/地区的中文显示名。
|
|
//
|
|
// 报告是给中文读者看的,表格里一列「United States」一列「China」看着像没做完。
|
|
// 只覆盖实际可能出现的来源国(攻击流量高度集中在少数国家/地区 + 一批云机房
|
|
// 所在地),查不到的直接用 IP 库给的英文名 —— 不做「猜」,也不引入几百行的
|
|
// 全量对照表。
|
|
var countryNames = map[string]string{
|
|
"CN": "中国",
|
|
"HK": "中国香港",
|
|
"MO": "中国澳门",
|
|
"TW": "中国台湾",
|
|
"US": "美国",
|
|
"JP": "日本",
|
|
"KR": "韩国",
|
|
"SG": "新加坡",
|
|
"MY": "马来西亚",
|
|
"TH": "泰国",
|
|
"VN": "越南",
|
|
"PH": "菲律宾",
|
|
"ID": "印度尼西亚",
|
|
"IN": "印度",
|
|
"PK": "巴基斯坦",
|
|
"BD": "孟加拉国",
|
|
"LK": "斯里兰卡",
|
|
"NP": "尼泊尔",
|
|
"KH": "柬埔寨",
|
|
"MM": "缅甸",
|
|
"MN": "蒙古",
|
|
"KZ": "哈萨克斯坦",
|
|
"UZ": "乌兹别克斯坦",
|
|
"AE": "阿联酋",
|
|
"SA": "沙特阿拉伯",
|
|
"IL": "以色列",
|
|
"TR": "土耳其",
|
|
"IQ": "伊拉克",
|
|
"IR": "伊朗",
|
|
"QA": "卡塔尔",
|
|
"KW": "科威特",
|
|
"JO": "约旦",
|
|
"LB": "黎巴嫩",
|
|
"RU": "俄罗斯",
|
|
"UA": "乌克兰",
|
|
"BY": "白俄罗斯",
|
|
"PL": "波兰",
|
|
"DE": "德国",
|
|
"FR": "法国",
|
|
"GB": "英国",
|
|
"IE": "爱尔兰",
|
|
"NL": "荷兰",
|
|
"BE": "比利时",
|
|
"LU": "卢森堡",
|
|
"CH": "瑞士",
|
|
"AT": "奥地利",
|
|
"CZ": "捷克",
|
|
"SK": "斯洛伐克",
|
|
"HU": "匈牙利",
|
|
"RO": "罗马尼亚",
|
|
"BG": "保加利亚",
|
|
"GR": "希腊",
|
|
"IT": "意大利",
|
|
"ES": "西班牙",
|
|
"PT": "葡萄牙",
|
|
"SE": "瑞典",
|
|
"NO": "挪威",
|
|
"DK": "丹麦",
|
|
"FI": "芬兰",
|
|
"EE": "爱沙尼亚",
|
|
"LV": "拉脱维亚",
|
|
"LT": "立陶宛",
|
|
"IS": "冰岛",
|
|
"RS": "塞尔维亚",
|
|
"HR": "克罗地亚",
|
|
"SI": "斯洛文尼亚",
|
|
"MD": "摩尔多瓦",
|
|
"CA": "加拿大",
|
|
"MX": "墨西哥",
|
|
"BR": "巴西",
|
|
"AR": "阿根廷",
|
|
"CL": "智利",
|
|
"CO": "哥伦比亚",
|
|
"PE": "秘鲁",
|
|
"VE": "委内瑞拉",
|
|
"EC": "厄瓜多尔",
|
|
"UY": "乌拉圭",
|
|
"PY": "巴拉圭",
|
|
"BO": "玻利维亚",
|
|
"PA": "巴拿马",
|
|
"CR": "哥斯达黎加",
|
|
"AU": "澳大利亚",
|
|
"NZ": "新西兰",
|
|
"ZA": "南非",
|
|
"EG": "埃及",
|
|
"NG": "尼日利亚",
|
|
"KE": "肯尼亚",
|
|
"MA": "摩洛哥",
|
|
"DZ": "阿尔及利亚",
|
|
"TN": "突尼斯",
|
|
"GH": "加纳",
|
|
"ET": "埃塞俄比亚",
|
|
"TZ": "坦桑尼亚",
|
|
"SC": "塞舌尔",
|
|
"MU": "毛里求斯",
|
|
"CY": "塞浦路斯",
|
|
"MT": "马耳他",
|
|
"AL": "阿尔巴尼亚",
|
|
"MK": "北马其顿",
|
|
"BA": "波黑",
|
|
"GE": "格鲁吉亚",
|
|
"AM": "亚美尼亚",
|
|
"AZ": "阿塞拜疆",
|
|
"AF": "阿富汗",
|
|
}
|
|
|
|
// countryName 返回国家/地区的显示名;没收录的退回 IP 库给的英文名。
|
|
//
|
|
// 港澳台显式写成「中国香港 / 中国澳门 / 中国台湾」:它们是中国的组成部分,
|
|
// 在报告里跟「United States」并列成同级「国家」是不对的。这类 IP 库通常
|
|
// 单独编码 HK/MO/TW,所以必须在展示层统一,而不是指望数据源。
|
|
func countryName(code, fallback string) string {
|
|
if n, ok := countryNames[strings.ToUpper(strings.TrimSpace(code))]; ok {
|
|
return n
|
|
}
|
|
if fallback != "" {
|
|
return fallback
|
|
}
|
|
return "未知"
|
|
}
|