feat(honeypot): 自动保留期清理 + 行数上限 + VACUUM 空间回收 + size 子命令
问题:honeypot_hits 是只追加审计表,写入完全由外部扫描流量驱动(不可预期、 不可节流),而原实现只在手动跑 CLI prune 时才清理,站点运行期表体积只增不减。 实测取证(本地 PostgreSQL 建同构表灌 10.5 万行真实数据): - 典型行 244 字节(含索引),10.5 万行 24 MB - 保留期清理对照(行数恒定 10 万,每轮删 25% 插 25%): 有 VACUUM 22→24MB 收敛;无 VACUUM 22→52MB 持续膨胀 - VACUUM 不缩小文件,只让空间可复用;二次实测(5 万行、连续 4 轮删插) 行数恒定 5 万、文件稳定 15MB 不再增长 改动: - 新增 HoneypotRetention 后台清理器(每小时一轮):时间闸 honeypot_retain_days + 数量闸 honeypot_max_rows,清理后调 Compact 回收空间 - db: 新增 Compact()(PG 执行 VACUUM (ANALYZE),走 GORM Exec 非事务路径)、 DeleteOldestBeyond()(跨方言通用)、ListSinceByIP()、Size() - 去掉 honeypot_hits.provider 无用索引(10 万行占 648kB,全代码库零查询使用), 并在启动时显式清理既有库上的旧索引(GORM 去 tag 不会自动删) - 修 CLI hits --ip 过滤 bug:原先取回最近 N 条再在 Go 侧筛,导致指定 IP 的 历史命中基本查不出来 → 改到 SQL 层过滤 - 新增 CLI honeypot size 子命令:报告行数、磁盘占用、时间跨度,并与两道闸对照 - 新增配置 honeypot_auto_clean / honeypot_max_rows
This commit is contained in:
+105
-6
@@ -21,6 +21,7 @@ import (
|
||||
//
|
||||
// TamaBox honeypot hits --since 24h 列出捕获到的破解凭据
|
||||
// TamaBox honeypot stats --days 7 统计来源/路径/凭据排行
|
||||
// TamaBox honeypot size 报告表行数与磁盘占用
|
||||
// TamaBox honeypot prune --days 30 清理过期记录(保留期由配置决定)
|
||||
//
|
||||
// 密码默认脱敏:CLI 输出会被贴到工单、聊天窗口甚至截图里,
|
||||
@@ -59,6 +60,11 @@ var Honeypot = &cli.Command{
|
||||
},
|
||||
Action: runHoneypotPrune,
|
||||
},
|
||||
{
|
||||
Name: "size",
|
||||
Usage: "Report honeypot table row count and disk usage",
|
||||
Action: runHoneypotSize,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -101,13 +107,15 @@ func runHoneypotHits(ctx *cli.Context) error {
|
||||
}
|
||||
store := db.NewHoneypotStore(database)
|
||||
since := time.Now().Add(-dur)
|
||||
filterIP := strings.TrimSpace(ctx.String("ip"))
|
||||
showPwd := ctx.Bool("show-password")
|
||||
|
||||
rows, err := store.ListSince(context.Background(), since, ctx.Int("limit"))
|
||||
// 按 IP 过滤在 SQL 层做:取回最近 N 条再在内存里筛,会导致「指定 IP 的
|
||||
// 历史命中基本查不出来」(它的记录被其他 IP 的更近记录挤出了窗口)。
|
||||
rows, err := store.ListSinceByIP(context.Background(), since, filterIP, ctx.Int("limit"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "query honeypot hits")
|
||||
}
|
||||
filterIP := strings.TrimSpace(ctx.String("ip"))
|
||||
showPwd := ctx.Bool("show-password")
|
||||
|
||||
fmt.Printf("蜜罐命中记录(近 %s,共 %d 条)\n", dur, len(rows))
|
||||
fmt.Println(strings.Repeat("-", 72))
|
||||
@@ -116,9 +124,6 @@ func runHoneypotHits(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
for _, r := range rows {
|
||||
if filterIP != "" && r.IP != filterIP {
|
||||
continue
|
||||
}
|
||||
pwd := r.Password
|
||||
if pwd == "" {
|
||||
pwd = "(空)"
|
||||
@@ -252,9 +257,103 @@ func runHoneypotPrune(ctx *cli.Context) error {
|
||||
return errors.Wrap(err, "prune honeypot records")
|
||||
}
|
||||
fmt.Printf("已删除 %d 条过期蜜罐记录。\n", n)
|
||||
// 回收物理空间:PostgreSQL 的 DELETE 只标记死元组,表文件不会变小。
|
||||
// 不回收的话「清理」只是逻辑变小,磁盘占用继续单调增长。
|
||||
if n > 0 {
|
||||
if err := store.Compact(context.Background()); err != nil {
|
||||
fmt.Printf("提示:空间回收失败(%v),数据本身已正确删除;PostgreSQL 下可手动执行 VACUUM honeypot_hits。\n", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runHoneypotSize 报告蜜罐表的行数与磁盘占用。
|
||||
//
|
||||
// 这个命令存在的意义:把「会不会把数据库撑臃肿」变成运维随时能自己查的事,
|
||||
// 而不是只能等开发一次性汇报。
|
||||
func runHoneypotSize(ctx *cli.Context) error {
|
||||
database, err := openDatabase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store := db.NewHoneypotStore(database)
|
||||
info, err := store.Size(context.Background())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "query honeypot table size")
|
||||
}
|
||||
|
||||
fmt.Println("蜜罐表体积")
|
||||
fmt.Println(strings.Repeat("-", 72))
|
||||
fmt.Printf(" 表名:honeypot_hits\n")
|
||||
fmt.Printf(" 存活记录:%d 条\n", info.Rows)
|
||||
if info.Bytes > 0 {
|
||||
fmt.Printf(" 磁盘占用:%s(含索引)\n", humanBytes(info.Bytes))
|
||||
if info.Rows > 0 {
|
||||
fmt.Printf(" 单条均摊:%d 字节\n", info.Bytes/info.Rows)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(" 磁盘占用:(当前数据库不支持元数据查询)\n")
|
||||
}
|
||||
if info.Oldest != nil && info.Newest != nil {
|
||||
fmt.Printf(" 时间跨度:%s → %s\n",
|
||||
info.Oldest.Format("2006-01-02 15:04"),
|
||||
info.Newest.Format("2006-01-02 15:04"))
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 与配置中的两道闸对照,让运维一眼看出「当前是否在界内」。
|
||||
maxRows := conf.Security.HoneypotMaxRows
|
||||
retainDays := conf.Security.HoneypotRetainDays
|
||||
if maxRows > 0 {
|
||||
fmt.Printf("行数上限:%d 条(当前 %d 条,%s)\n", maxRows, info.Rows, usageRatio(info.Rows, int64(maxRows)))
|
||||
} else {
|
||||
fmt.Printf("行数上限:未设置(当前 %d 条)\n", info.Rows)
|
||||
}
|
||||
if retainDays > 0 {
|
||||
fmt.Printf("保留期:%d 天(自动清理 %s)\n", retainDays, onOff(conf.Security.HoneypotAutoClean))
|
||||
} else {
|
||||
fmt.Printf("保留期:未设置 —— 记录只增不减,需要手动执行 TamaBox honeypot prune\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// usageRatio 给出当前用量相对上限的百分比描述。
|
||||
func usageRatio(cur int64, max int64) string {
|
||||
if max <= 0 {
|
||||
return ""
|
||||
}
|
||||
pct := float64(cur) / float64(max) * 100
|
||||
switch {
|
||||
case pct >= 100:
|
||||
return "已达上限"
|
||||
case pct >= 80:
|
||||
return "接近上限"
|
||||
default:
|
||||
return fmt.Sprintf("%.1f%%", pct)
|
||||
}
|
||||
}
|
||||
|
||||
func onOff(b bool) string {
|
||||
if b {
|
||||
return "已开启"
|
||||
}
|
||||
return "已关闭"
|
||||
}
|
||||
|
||||
// humanBytes 把字节数格式化成人类可读形式。
|
||||
func humanBytes(n int64) string {
|
||||
const unit = 1024
|
||||
if n < unit {
|
||||
return fmt.Sprintf("%d B", n)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for v := n / unit; v >= unit; v /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.2f %cB", float64(n)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// maskSecret 对密码做保长脱敏:保留首尾各 1 位,中间固定长度星号。
|
||||
// 目的是让运维能分辨「是不是同一个密码」,但不把字典直接摊在终端上。
|
||||
func maskSecret(s string) string {
|
||||
|
||||
@@ -72,6 +72,15 @@ func runWeb(ctx *cli.Context) error {
|
||||
return errors.Wrap(err, "connect to database")
|
||||
}
|
||||
|
||||
// 蜜罐命中记录保留期清理:honeypot_hits 是只追加的审计表,写入完全由
|
||||
// 外部扫描流量驱动(不可预期、不可节流)。没有自动清理的话,表体积只增不减。
|
||||
// 保留期复用 honeypot_retain_days(与 CLI prune 同一个口径),开关为 honeypot_auto_clean。
|
||||
stopRetention := make(chan struct{})
|
||||
if conf.Security.HoneypotAutoClean {
|
||||
db.NewHoneypotRetention(db.Honeypots, conf.Security.HoneypotRetainDays, conf.Security.HoneypotMaxRows, 0).Start(stopRetention)
|
||||
defer close(stopRetention)
|
||||
}
|
||||
|
||||
logrus.WithContext(ctx.Context).WithField("external_url", conf.App.ExternalURL).Info("Starting web server")
|
||||
|
||||
// 站点默认语言初始化(default_lang 配置,未配置时回退 zh-CN)
|
||||
|
||||
Reference in New Issue
Block a user