fix(honeypot): 补齐封禁装配与诱捕语义,凭据外置为文件不再入库
一、封禁链路整条没接上(P0,已实测复现)
route.New 的 autoban.New 漏传 HoneypotEnabled/Threshold/Duration,而
RecordHoneypot 首行就是 `if !b.honeypotEnabled { return false }`
→ 蜜罐命中从不封禁、落库 banned 恒 false、IsBanned 永不为真。
同一批改动又把中间件里蜜罐路径的常规计数也跳过了,净效果是蜜罐路径
从「10 次封禁」退化为零计数零封禁 —— 是 regression 不是修复。
处理:抽 newBanner() 作为唯一装配入口并补齐参数;新增 banner_config_test
四个用例(全部经由 newBanner,不再自建 Banner)。变异验证:分别把
HoneypotEnabled 置 false、HoneypotThreshold 置 0,对应用例按预期失败。
二、诱捕语义(审计时漏掉,比上面更影响功能价值)
原实现对**所有方法**计数且阈值 1 → 扫描器 GET 一次假登录页就吃 403,
永远走不到「POST 提交账号密码」那一步,蜜罐的核心价值(拿到字典)归零。
处理:提交凭据 → RecordHoneypot(不受 enable_auto_ban 约束,立即封);
纯探测 → 常规 Record(阈值 10)。新增 TestHoneypotProbeDoesNotBanImmediately,
变异验证:把 GET 也塞进 RecordHoneypot,测试按预期失败。
三、落库边界(实测取证,非推断)
- Path 原样落库而列宽 512 → PG `SQLSTATE 22001 值太长了(512)`,整条证据丢失;
实测 600 字符失败、500 字符成功;
- 三处按字节截断 → `SQLSTATE 22021 无效的 "UTF8" 编码字节顺序`(中文密码/URL
可稳定触发)。统一改 rune 截断并按列宽逐项对齐。
四、资源与噪音
- alerted 无界 map → 带 TTL 的 deduper(上限 2 万,过期清理 + 超限硬裁),
同时承担告警 24h 冷却与纯探测落库采样(每 IP 每分钟一条)——
GET 洪泛不再变成等量 INSERT;
- credentialKeys 去掉 id/name,普通 JSON 探测不再被误判为凭据提交并触发假告警;
- canary 从 meta generator 移到 HTML 注释,generator 伪装成 WordPress,
不再同时向扫描器自曝「这是蜜罐」和「这站是 TamaBox」;
- serveLoginFailure 复用 WriteLoginPage,不再手抄响应头(已开始漂移);
- Describer 死代码 → Prefixes(),接到新命令 honeypot paths;
- honeypot_paths 解析挪到 honeypot.SplitPaths(CLI 与 route 共用),
补前导斜杠、拒绝 "/"(会让所有 404 变假登录页)。
五、凭据外置:数据库不再存用户名/密码
明文账号密码一旦入库就会跟着进备份、主从复制与任何能连库的报表工具,
事后无法彻底清除。改为:
- 新增 honeypot.CredRecord/CredStore:按天文件 creds-YYYY-MM-DD.jsonl,
目录 0700、文件 0600、坏行跳过、PurgeBefore 只删整天过期的文件;
- db.HoneypotHit 删掉 Username/Password/Fields/RawBody,只留 HadFields 元数据;
- HoneypotStore 删掉 TopCredentials/ListSinceByIP(CLI 改读文件后零调用);
- 新增 LegacyCredentialColumns/PurgeLegacyCredentialColumns:AutoMigrate 不删
既有列,老库升级后历史明文仍在 → 启动 WARN + `honeypot purge-creds` 显式 DROP;
- 清理器 HoneypotRetention 增加 CredPurgeable 参数,与 DB 记录共用保留期;
honeypot prune 也一并删过期凭据文件;
- CLI:hits 不再连库(DB 挂了也能查),stats 的凭据排行读文件;
- 配置 honeypot_cred_dir(缺省 data/honeypot-creds),.gitignore 加 /data/。
验证:go build ./... / go vet ./internal/... / go test ./...(含 PostgreSQL)全绿;
新增 credstore_test(追加/倒序/IP 过滤/坏行/Top/Purge/权限)、
TestHoneypotLegacyCredentialColumns(ADD COLUMN 模拟老库 → 检测 → DROP)、
TestHoneypotWritesCredentialsToFile;CLI 端到端手动验证 hits/--ip/--show-password。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -127,6 +128,15 @@ func Init(typ, dsn string) (*gorm.DB, error) {
|
||||
Reports = NewReportsStore(db)
|
||||
Honeypots = NewHoneypotStore(db)
|
||||
|
||||
// 旧版本把消息/密码明文存在 honeypot_hits 上;结构体已删掉这些字段,
|
||||
// 但 AutoMigrate 不会删除既有列 —— 老库升级上来后历史明文仍在。
|
||||
// 这里只告警不自动删:DROP COLUMN 不可逆,必须由运维确认后执行
|
||||
// `TamaBox honeypot purge-creds`。
|
||||
if cols, err := Honeypots.LegacyCredentialColumns(context.Background()); err == nil && len(cols) > 0 {
|
||||
logrus.WithField("columns", cols).
|
||||
Warn("honeypot_hits 上仍残留旧版明文凭据列,请执行 TamaBox honeypot purge-creds 清除")
|
||||
}
|
||||
|
||||
if err := db.Use(otelgorm.NewPlugin(
|
||||
otelgorm.WithDBName(conf.Database.Name),
|
||||
)); err != nil {
|
||||
|
||||
+47
-54
@@ -28,15 +28,14 @@ type HoneypotHit struct {
|
||||
// Query 保存原始查询串(截断)。
|
||||
Query string `gorm:"size:1024" json:"query"`
|
||||
|
||||
// Username / Password 为提取出的破解凭据。Password 落库前做可逆加密还是脱敏,
|
||||
// 由调用方决定:这里只存明文,CLI 展示时默认脱敏。
|
||||
Username string `gorm:"size:256" json:"username"`
|
||||
Password string `gorm:"size:256" json:"password"`
|
||||
// Fields 保存其余表单/JSON 字段(JSON 字符串),便于还原破解工具特征。
|
||||
Fields string `gorm:"type:text" json:"fields"`
|
||||
// RawBody 保存截断后的原始 body。
|
||||
RawBody string `gorm:"type:text" json:"raw_body"`
|
||||
// HadFields 标记本次提交是否真的带了凭据字段(区分「探测 GET」与「尝试破解 POST」)。
|
||||
//
|
||||
// ⚠️ 这里只存「有没有」这个事实,**不存凭据本身**。
|
||||
// 用户名/密码/原始 body 全部落在 honeypot.CredStore(默认
|
||||
// data/honeypot-creds/creds-YYYY-MM-DD.jsonl,权限 0600),原因见
|
||||
// internal/security/honeypot/credstore.go 的 CredRecord 注释:
|
||||
// 明文账号密码不能进数据库 —— 一旦入库就会跟着进备份、主从复制、
|
||||
// 以及任何有只读权限的报表工具,事后无法彻底清除。
|
||||
HadFields bool `json:"had_fields"`
|
||||
|
||||
// UserAgent / Referer / ContentType 用于扫描器画像。
|
||||
@@ -69,19 +68,12 @@ type HoneypotStore interface {
|
||||
Create(ctx context.Context, hit *HoneypotHit) error
|
||||
// ListSince 返回 since 之后的命中记录,按时间倒序,最多 limit 条。
|
||||
ListSince(ctx context.Context, since time.Time, limit int) ([]*HoneypotHit, error)
|
||||
// ListSinceByIP 返回 since 之后、来自指定 IP 的命中记录,按时间倒序。
|
||||
//
|
||||
// 刻意在 SQL 层过滤而不是取回后在 Go 侧过滤:后者只能看到「最近 limit 条」
|
||||
// 里恰好落在窗口内的该 IP 记录,指定 IP 的历史命中基本查不出来。
|
||||
ListSinceByIP(ctx context.Context, since time.Time, ip string, limit int) ([]*HoneypotHit, error)
|
||||
// CountSince 统计 since 之后的命中总数。
|
||||
CountSince(ctx context.Context, since time.Time) (int64, error)
|
||||
// TopSources 返回 since 之后命中次数最多的来源 IP。
|
||||
TopSources(ctx context.Context, since time.Time, limit int) ([]*HoneypotSourceStat, error)
|
||||
// TopPaths 返回 since 之后被命中次数最多的路径。
|
||||
TopPaths(ctx context.Context, since time.Time, limit int) ([]*HoneypotPathStat, error)
|
||||
// TopCredentials 返回 since 之后最常被尝试的用户名/密码组合。
|
||||
TopCredentials(ctx context.Context, since time.Time, limit int) ([]*HoneypotCredentialStat, error)
|
||||
// DeleteBefore 清理 before 之前的记录(保留期到期),返回删除行数。
|
||||
DeleteBefore(ctx context.Context, before time.Time) (int64, error)
|
||||
// DeleteOldestBeyond 保留最近 keepRows 行,删除更早的记录,返回删除行数。
|
||||
@@ -101,6 +93,10 @@ type HoneypotStore interface {
|
||||
// 而不是只能靠开发一次性汇报。Bytes 取整个关系的总大小(含索引),
|
||||
// 因为磁盘压力来自索引和 TOAST,不只是堆表。
|
||||
Size(ctx context.Context) (HoneypotSize, error)
|
||||
// LegacyCredentialColumns 返回仍残留的旧版凭据列(空表示已清理)。
|
||||
LegacyCredentialColumns(ctx context.Context) ([]string, error)
|
||||
// PurgeLegacyCredentialColumns 删除旧版遗留的凭据列(不可逆)。
|
||||
PurgeLegacyCredentialColumns(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// HoneypotSize 蜜罐表的体积报告。
|
||||
@@ -127,11 +123,42 @@ type HoneypotPathStat struct {
|
||||
Count int64
|
||||
}
|
||||
|
||||
// HoneypotCredentialStat 凭据组合聚合行。
|
||||
type HoneypotCredentialStat struct {
|
||||
Username string
|
||||
Password string
|
||||
Count int64
|
||||
// LegacyHoneypotCredentialColumns 是早期版本在 honeypot_hits 上留下的凭据列。
|
||||
//
|
||||
// 结构体已经删掉这些字段,但 GORM 的 AutoMigrate **不会删除既有列** ——
|
||||
// 老库升级上来后列还在、历史明文也还在。启动时检测到就告警,由运维执行
|
||||
// `TamaBox honeypot purge-creds` 显式清除(DROP COLUMN 不可逆,不自动做)。
|
||||
var LegacyHoneypotCredentialColumns = []string{"username", "password", "fields", "raw_body"}
|
||||
|
||||
// LegacyCredentialColumns 返回当前库里仍然存在的旧凭据列(空表示已清理干净)。
|
||||
func (h *honeypots) LegacyCredentialColumns(ctx context.Context) ([]string, error) {
|
||||
var present []string
|
||||
for _, col := range LegacyHoneypotCredentialColumns {
|
||||
if h.db.WithContext(ctx).Migrator().HasColumn(&HoneypotHit{}, col) {
|
||||
present = append(present, col)
|
||||
}
|
||||
}
|
||||
return present, nil
|
||||
}
|
||||
|
||||
// PurgeLegacyCredentialColumns 删除旧版遗留的凭据列(不可逆)。
|
||||
//
|
||||
// 语义是「把历史上已经入库的明文凭据彻底清掉」,所以是 DROP COLUMN 而不是
|
||||
// 置空:置空后明文仍可能残留在表的死元组/备份里,DROP 至少让新产生的
|
||||
// 备份与后续查询不再包含它们。真正的历史备份需要运维自行处理。
|
||||
func (h *honeypots) PurgeLegacyCredentialColumns(ctx context.Context) ([]string, error) {
|
||||
present, err := h.LegacyCredentialColumns(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dropped := make([]string, 0, len(present))
|
||||
for _, col := range present {
|
||||
if err := h.db.WithContext(ctx).Migrator().DropColumn(&HoneypotHit{}, col); err != nil {
|
||||
return dropped, err
|
||||
}
|
||||
dropped = append(dropped, col)
|
||||
}
|
||||
return dropped, nil
|
||||
}
|
||||
|
||||
type honeypots struct {
|
||||
@@ -163,23 +190,6 @@ func (h *honeypots) ListSince(ctx context.Context, since time.Time, limit int) (
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (h *honeypots) ListSinceByIP(ctx context.Context, since time.Time, ip string, limit int) ([]*HoneypotHit, error) {
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 200
|
||||
}
|
||||
if ip == "" {
|
||||
return h.ListSince(ctx, since, limit)
|
||||
}
|
||||
var rows []*HoneypotHit
|
||||
err := h.db.WithContext(ctx).
|
||||
Where("created_at >= ?", since).
|
||||
Where("ip = ?", ip).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (h *honeypots) CountSince(ctx context.Context, since time.Time) (int64, error) {
|
||||
var n int64
|
||||
err := h.db.WithContext(ctx).Model(&HoneypotHit{}).
|
||||
@@ -220,23 +230,6 @@ func (h *honeypots) TopPaths(ctx context.Context, since time.Time, limit int) ([
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (h *honeypots) TopCredentials(ctx context.Context, since time.Time, limit int) ([]*HoneypotCredentialStat, error) {
|
||||
if limit <= 0 || limit > 500 {
|
||||
limit = 20
|
||||
}
|
||||
var rows []*HoneypotCredentialStat
|
||||
err := h.db.WithContext(ctx).
|
||||
Table("honeypot_hits").
|
||||
Select("username AS username, password AS password, count(*) AS count").
|
||||
Where("created_at >= ?", since).
|
||||
Where("username <> '' OR password <> ''").
|
||||
Group("username, password").
|
||||
Order("count DESC").
|
||||
Limit(limit).
|
||||
Scan(&rows).Error
|
||||
return rows, err
|
||||
}
|
||||
|
||||
func (h *honeypots) DeleteBefore(ctx context.Context, before time.Time) (int64, error) {
|
||||
res := h.db.WithContext(ctx).Where("created_at < ?", before).Delete(&HoneypotHit{})
|
||||
return res.RowsAffected, res.Error
|
||||
|
||||
@@ -34,8 +34,19 @@ import (
|
||||
// 此时只记一条告警日志,不删任何数据。
|
||||
// - 清理失败只记日志,绝不 panic、绝不阻断主流程:蜜罐是安全增强功能,
|
||||
// 不能因为它把站点搞挂。
|
||||
//
|
||||
// CredPurgeable 是「凭据文件按月清理」的能力(由 honeypot.CredStore 实现)。
|
||||
//
|
||||
// 用本地小接口而不是直接 import honeypot 包:存储层不该反向依赖安全层,
|
||||
// 也能让清理器在测试里用假实现。
|
||||
type CredPurgeable interface {
|
||||
// PurgeBefore 删除早于 before 的整天文件,返回删除的文件数。
|
||||
PurgeBefore(before time.Time) (int, error)
|
||||
}
|
||||
|
||||
type HoneypotRetention struct {
|
||||
store HoneypotStore
|
||||
creds CredPurgeable
|
||||
retainDays int
|
||||
maxRows int
|
||||
interval time.Duration
|
||||
@@ -44,13 +55,14 @@ type HoneypotRetention struct {
|
||||
// NewHoneypotRetention 构造保留期清理器。
|
||||
//
|
||||
// retainDays <= 0 表示不自动清理;maxRows <= 0 表示不设数量上限;
|
||||
// interval <= 0 时使用默认 1 小时。
|
||||
func NewHoneypotRetention(store HoneypotStore, retainDays int, maxRows int, interval time.Duration) *HoneypotRetention {
|
||||
// interval <= 0 时使用默认 1 小时;creds 为 nil 表示不清理凭据文件。
|
||||
func NewHoneypotRetention(store HoneypotStore, creds CredPurgeable, retainDays int, maxRows int, interval time.Duration) *HoneypotRetention {
|
||||
if interval <= 0 {
|
||||
interval = time.Hour
|
||||
}
|
||||
return &HoneypotRetention{
|
||||
store: store,
|
||||
creds: creds,
|
||||
retainDays: retainDays,
|
||||
maxRows: maxRows,
|
||||
interval: interval,
|
||||
@@ -126,6 +138,17 @@ func (r *HoneypotRetention) runOnce() {
|
||||
total += m
|
||||
}
|
||||
|
||||
// 凭据文件按同一个保留期清理:明文账号密码不该比元数据活得更久。
|
||||
// 独立于 DB 的删除量 —— 即使这批数据库记录本来就已达标,文件仍可能过期。
|
||||
if r.creds != nil {
|
||||
if n, err := r.creds.PurgeBefore(before); err != nil {
|
||||
logrus.WithError(err).Warn("蜜罐凭据文件清理失败(不影响数据库记录)")
|
||||
} else if n > 0 {
|
||||
logrus.WithFields(logrus.Fields{"files": n, "days": r.retainDays}).
|
||||
Info("已清理过期的蜜罐凭据文件")
|
||||
}
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestHoneypotRetentionRunOnce(t *testing.T) {
|
||||
// 保留期 30 天、行数上限 7:
|
||||
// 时间闸删掉 5 条 90 天前的旧记录 → 剩 5 条新的;
|
||||
// 数量闸:5 < 7,未超上限,不删 → 最终剩 5 条。
|
||||
ret := NewHoneypotRetention(store, 30, 7, 0)
|
||||
ret := NewHoneypotRetention(store, nil, 30, 7, 0)
|
||||
ret.runOnce()
|
||||
|
||||
remaining, err := store.CountSince(ctx, now.Add(-365*24*time.Hour))
|
||||
@@ -82,7 +82,7 @@ func TestHoneypotRetentionDisabled(t *testing.T) {
|
||||
}))
|
||||
}
|
||||
|
||||
ret := NewHoneypotRetention(store, 0, 100, 0)
|
||||
ret := NewHoneypotRetention(store, nil, 0, 100, 0)
|
||||
ret.runOnce()
|
||||
|
||||
total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour))
|
||||
@@ -109,7 +109,7 @@ func TestHoneypotRetentionMaxRowsOnly(t *testing.T) {
|
||||
}))
|
||||
}
|
||||
|
||||
ret := NewHoneypotRetention(store, 30, 5, 0)
|
||||
ret := NewHoneypotRetention(store, nil, 30, 5, 0)
|
||||
ret.runOnce()
|
||||
|
||||
total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour))
|
||||
@@ -147,7 +147,7 @@ func TestHoneypotRetentionBothGates(t *testing.T) {
|
||||
|
||||
// 保留期 30 天、数量上限 4:
|
||||
// 时间闸删 5 条 ancient → 剩 10 条 recent;数量闸删到 4 条(删最旧的 6 条)
|
||||
ret := NewHoneypotRetention(store, 30, 4, 0)
|
||||
ret := NewHoneypotRetention(store, nil, 30, 4, 0)
|
||||
ret.runOnce()
|
||||
|
||||
total, err := store.CountSince(ctx, now.Add(-365*24*time.Hour))
|
||||
@@ -208,44 +208,6 @@ func TestHoneypotDeleteOldestBeyond(t *testing.T) {
|
||||
require.Equal(t, int64(0), n3)
|
||||
}
|
||||
|
||||
// TestHoneypotListSinceByIP 验证按 IP 过滤在 SQL 层生效。
|
||||
//
|
||||
// 背景:原先 CLI 的 --ip 过滤是在取回最近 N 条后在内存里筛,
|
||||
// 导致「指定 IP 的历史命中」基本查不出来(被其他 IP 的更近记录挤出窗口)。
|
||||
func TestHoneypotListSinceByIP(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()
|
||||
|
||||
// 目标 IP 只有 1 条很老的记录;其余 20 条都是别的 IP 且更新
|
||||
require.NoError(t, store.Create(ctx, &HoneypotHit{
|
||||
IP: "9.9.9.9", Method: "GET", Path: "/target", CreatedAt: now.Add(-10 * time.Hour),
|
||||
}))
|
||||
for i := 0; i < 20; i++ {
|
||||
require.NoError(t, store.Create(ctx, &HoneypotHit{
|
||||
IP: "8.8.8.8", Method: "GET", Path: "/other", CreatedAt: now.Add(-time.Duration(i) * time.Minute),
|
||||
}))
|
||||
}
|
||||
|
||||
rows, err := store.ListSinceByIP(ctx, now.Add(-24*time.Hour), "9.9.9.9", 200)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 1, "指定 IP 的老记录应能被查出(不能被其他 IP 的新记录挤出窗口)")
|
||||
require.Equal(t, "/target", rows[0].Path)
|
||||
|
||||
// 空 IP 退化为不过滤
|
||||
all, err := store.ListSinceByIP(ctx, now.Add(-24*time.Hour), "", 200)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, all, 21)
|
||||
}
|
||||
|
||||
// TestHoneypotCompact 验证空间回收在 PostgreSQL 下可执行且不报错。
|
||||
func TestHoneypotCompact(t *testing.T) {
|
||||
database, cleanup := newTestDB(t)
|
||||
|
||||
@@ -31,9 +31,9 @@ func TestHoneypotStore(t *testing.T) {
|
||||
|
||||
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)},
|
||||
{IP: "1.2.3.4", Method: "POST", Path: "/wp-login.php", HadFields: true, CreatedAt: now.Add(-2 * time.Hour)},
|
||||
{IP: "1.2.3.4", Method: "POST", Path: "/wp-login.php", HadFields: true, CreatedAt: now.Add(-time.Hour)},
|
||||
{IP: "5.6.7.8", Method: "POST", Path: "/.ssh/id_rsa", HadFields: true, CreatedAt: now.Add(-30 * time.Minute)},
|
||||
}
|
||||
for _, r := range rows {
|
||||
require.NoError(t, store.Create(ctx, r))
|
||||
@@ -64,13 +64,6 @@ func TestHoneypotStore(t *testing.T) {
|
||||
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)
|
||||
@@ -81,6 +74,45 @@ func TestHoneypotStore(t *testing.T) {
|
||||
require.Equal(t, int64(2), total)
|
||||
}
|
||||
|
||||
// TestHoneypotLegacyCredentialColumns 覆盖「旧版凭据列」的检测与清除。
|
||||
//
|
||||
// 背景:凭据已改为只落文件(见 honeypot.CredRecord),结构体也删掉了
|
||||
// username/password/fields/raw_body 四个字段。但 GORM 的 AutoMigrate
|
||||
// 不会删除既有列,老库升级上来后历史明文仍然留在表里。
|
||||
// 这个测试用 ADD COLUMN 模拟老库,验证检测与 DROP COLUMN 都真的生效。
|
||||
func TestHoneypotLegacyCredentialColumns(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)
|
||||
|
||||
cols, err := store.LegacyCredentialColumns(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cols, "新建的表不应有旧版凭据列")
|
||||
|
||||
// 模拟旧版本留下的列。
|
||||
require.NoError(t, database.Exec("ALTER TABLE honeypot_hits ADD COLUMN username varchar(256)").Error)
|
||||
require.NoError(t, database.Exec("ALTER TABLE honeypot_hits ADD COLUMN password varchar(256)").Error)
|
||||
|
||||
cols, err = store.LegacyCredentialColumns(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{"username", "password"}, cols)
|
||||
|
||||
dropped, err := store.PurgeLegacyCredentialColumns(ctx)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []string{"username", "password"}, dropped)
|
||||
|
||||
cols, err = store.LegacyCredentialColumns(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, cols, "清除后不应再检测到旧版凭据列")
|
||||
}
|
||||
|
||||
// TestHoneypotSize 覆盖体积报告:行数、时间跨度、磁盘占用。
|
||||
//
|
||||
// 目的:让「蜜罐表有没有把数据库撑臃肿」成为运维随时能自己查的事。
|
||||
@@ -102,7 +134,7 @@ func TestHoneypotSize(t *testing.T) {
|
||||
IP: "9.9.9.9",
|
||||
Method: "POST",
|
||||
Path: "/wp-login.php",
|
||||
Username: "admin",
|
||||
HadFields: true,
|
||||
CreatedAt: now.Add(-time.Duration(i) * time.Hour),
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user