Files
tamabox/internal/db/honeypot_test.go
T
tamakyi 63e851ddae 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
2026-10-21 13:16:14 +08:00

130 lines
4.1 KiB
Go
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.
// 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 db
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// TestHoneypotStore 覆盖蜜罐命中记录的写入、检索与聚合。
//
// 重点验证「只追加」的审计语义:DeleteBefore 只按时间删,不做软删除;
// 以及 TopCredentials 能把攻击者正在用的字典聚合出来。
func TestHoneypotStore(t *testing.T) {
database, cleanup := newTestDB(t)
ctx := context.Background()
t.Cleanup(func() {
if err := cleanup("honeypot_hits"); err != nil {
t.Fatal(err)
}
})
store := NewHoneypotStore(database)
now := time.Now()
rows := []*HoneypotHit{
{IP: "1.2.3.4", Method: "GET", Path: "/wp-admin", Provider: "Azure", CreatedAt: now.Add(-3 * time.Hour)},
{IP: "1.2.3.4", Method: "POST", Path: "/wp-login.php", Username: "admin", Password: "admin123", HadFields: true, CreatedAt: now.Add(-2 * time.Hour)},
{IP: "1.2.3.4", Method: "POST", Path: "/wp-login.php", Username: "admin", Password: "admin123", HadFields: true, CreatedAt: now.Add(-time.Hour)},
{IP: "5.6.7.8", Method: "POST", Path: "/.ssh/id_rsa", Username: "root", Password: "toor", HadFields: true, CreatedAt: now.Add(-30 * time.Minute)},
}
for _, r := range rows {
require.NoError(t, store.Create(ctx, r))
}
since := now.Add(-4 * time.Hour)
total, err := store.CountSince(ctx, since)
require.NoError(t, err)
require.Equal(t, int64(4), total)
list, err := store.ListSince(ctx, since, 10)
require.NoError(t, err)
require.Len(t, list, 4)
// 倒序返回
require.Equal(t, "5.6.7.8", list[0].IP)
sources, err := store.TopSources(ctx, since, 10)
require.NoError(t, err)
require.Len(t, sources, 2)
require.Equal(t, "1.2.3.4", sources[0].IP)
require.Equal(t, int64(3), sources[0].Count)
require.Equal(t, "Azure", sources[0].Provider)
paths, err := store.TopPaths(ctx, since, 10)
require.NoError(t, err)
require.Len(t, paths, 3)
require.Equal(t, "/wp-login.php", paths[0].Path)
require.Equal(t, int64(2), paths[0].Count)
creds, err := store.TopCredentials(ctx, since, 10)
require.NoError(t, err)
require.Len(t, creds, 2)
require.Equal(t, "admin", creds[0].Username)
require.Equal(t, "admin123", creds[0].Password)
require.Equal(t, int64(2), creds[0].Count)
// 保留期清理
deleted, err := store.DeleteBefore(ctx, now.Add(-90*time.Minute))
require.NoError(t, err)
require.Equal(t, int64(2), deleted) // 3 小时前、2 小时前两条被清
total, err = store.CountSince(ctx, since)
require.NoError(t, err)
require.Equal(t, int64(2), total)
}
// TestHoneypotSize 覆盖体积报告:行数、时间跨度、磁盘占用。
//
// 目的:让「蜜罐表有没有把数据库撑臃肿」成为运维随时能自己查的事。
func TestHoneypotSize(t *testing.T) {
database, cleanup := newTestDB(t)
ctx := context.Background()
t.Cleanup(func() {
if err := cleanup("honeypot_hits"); err != nil {
t.Fatal(err)
}
})
store := NewHoneypotStore(database)
now := time.Now()
for i := 0; i < 3; i++ {
require.NoError(t, store.Create(ctx, &HoneypotHit{
IP: "9.9.9.9",
Method: "POST",
Path: "/wp-login.php",
Username: "admin",
CreatedAt: now.Add(-time.Duration(i) * time.Hour),
}))
}
info, err := store.Size(ctx)
require.NoError(t, err)
require.Equal(t, int64(3), info.Rows)
require.NotNil(t, info.Oldest)
require.NotNil(t, info.Newest)
// 最早的一条是 2 小时前,最新的一条是刚刚。
require.True(t, info.Newest.After(*info.Oldest))
// 磁盘占用在 PostgreSQL 下应当可查且不为零(表刚建,至少有元数据开销)。
require.Greater(t, info.Bytes, int64(0))
// 清理后行数应随之下降,但磁盘占用不保证立刻变小
// (PostgreSQL 的 DELETE 只标记死元组,需 VACUUM 才回收)。
deleted, err := store.DeleteBefore(ctx, now.Add(-90*time.Minute))
require.NoError(t, err)
require.Equal(t, int64(1), deleted)
info2, err := store.Size(ctx)
require.NoError(t, err)
require.Equal(t, int64(2), info2.Rows)
}