miguan v1
This commit is contained in:
@@ -23,6 +23,8 @@ var AllTables = []interface{}{
|
||||
&FeatureControl{},
|
||||
// 站方处置与举报(无管理后台,处置经 CLI 写入、运行时读表生效)
|
||||
&UserRestriction{}, &ContentReport{},
|
||||
// 蜜罐命中记录(审计证据,只追加不软删)
|
||||
&HoneypotHit{},
|
||||
}
|
||||
|
||||
func Init(typ, dsn string) (*gorm.DB, error) {
|
||||
@@ -117,6 +119,7 @@ func Init(typ, dsn string) (*gorm.DB, error) {
|
||||
FeatureControls = NewFeatureControlsStore(db)
|
||||
Restrictions = NewRestrictionsStore(db)
|
||||
Reports = NewReportsStore(db)
|
||||
Honeypots = NewHoneypotStore(db)
|
||||
|
||||
if err := db.Use(otelgorm.NewPlugin(
|
||||
otelgorm.WithDBName(conf.Database.Name),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user