// 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" "time" "gorm.io/gorm" ) // HoneypotHit 是一次蜜罐命中记录(审计证据)。 // // 刻意不嵌 dbutil.Model:审计证据不做软删除、不做 Update/Delete, // 只追加与查询。理由与 ContentReport 一致 —— 证据链一旦被软删除过滤, // 事后追责就断了。 type HoneypotHit struct { ID uint `gorm:"primarykey" json:"id"` CreatedAt time.Time `gorm:"index" json:"created_at"` // IP 为归一化后的客户端 IP(shared.LimitKey 之后,IPv6 折叠到 /64)。 IP string `gorm:"size:64;index" json:"ip"` // Method 与 Path 记录原始请求方法与路径(未归一化的原始值)。 Method string `gorm:"size:16" json:"method"` Path string `gorm:"size:512" json:"path"` // 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」)。 HadFields bool `json:"had_fields"` // UserAgent / Referer / ContentType 用于扫描器画像。 UserAgent string `gorm:"size:512" json:"user_agent"` Referer string `gorm:"size:512" json:"referer"` ContentType string `gorm:"size:256" json:"content_type"` // Provider 为 IP 归属(云厂商/CDN/WAF)标签,Proxied 表示是否经代理。 Provider string `gorm:"size:64;index" json:"provider"` Proxied bool `json:"proxied"` // Banned 为命中时是否触发了封禁。 Banned bool `json:"banned"` } // TableName 固定表名,避免 GORM 复数化差异。 func (HoneypotHit) TableName() string { return "honeypot_hits" } // Honeypots 蜜罐命中记录全局 store,由 db.Init 装配。 var Honeypots HoneypotStore // HoneypotStore 蜜罐命中记录的读写。 type HoneypotStore interface { // Create 追加一条命中记录。 Create(ctx context.Context, hit *HoneypotHit) error // ListSince 返回 since 之后的命中记录,按时间倒序,最多 limit 条。 ListSince(ctx context.Context, since time.Time, 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) } // HoneypotSourceStat 来源 IP 聚合行。 type HoneypotSourceStat struct { IP string Count int64 Provider string } // HoneypotPathStat 路径聚合行。 type HoneypotPathStat struct { Path string Count int64 } // HoneypotCredentialStat 凭据组合聚合行。 type HoneypotCredentialStat struct { Username string Password string Count int64 } type honeypots struct { db *gorm.DB } // NewHoneypotStore 构造蜜罐记录 store。 func NewHoneypotStore(db *gorm.DB) HoneypotStore { return &honeypots{db: db} } func (h *honeypots) Create(ctx context.Context, hit *HoneypotHit) error { if hit == nil { return nil } return h.db.WithContext(ctx).Create(hit).Error } func (h *honeypots) ListSince(ctx context.Context, since time.Time, limit int) ([]*HoneypotHit, error) { if limit <= 0 || limit > 1000 { limit = 200 } var rows []*HoneypotHit err := h.db.WithContext(ctx). Where("created_at >= ?", since). 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{}). Where("created_at >= ?", since). Count(&n).Error return n, err } func (h *honeypots) TopSources(ctx context.Context, since time.Time, limit int) ([]*HoneypotSourceStat, error) { if limit <= 0 || limit > 500 { limit = 20 } var rows []*HoneypotSourceStat err := h.db.WithContext(ctx). Table("honeypot_hits"). Select("ip AS ip, count(*) AS count, max(provider) AS provider"). Where("created_at >= ?", since). Group("ip"). Order("count DESC"). Limit(limit). Scan(&rows).Error return rows, err } func (h *honeypots) TopPaths(ctx context.Context, since time.Time, limit int) ([]*HoneypotPathStat, error) { if limit <= 0 || limit > 500 { limit = 20 } var rows []*HoneypotPathStat err := h.db.WithContext(ctx). Table("honeypot_hits"). Select("path AS path, count(*) AS count"). Where("created_at >= ?", since). Group("path"). Order("count DESC"). Limit(limit). Scan(&rows).Error 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 }