Files
full/docs/audit/module-base-logs.md
yanweidong 63aeedc2fe docs: add per-module audit reports (18 modules)
Add static security/quality audit reports for all 18 Go service modules under module/, plus a consolidated index (docs/audit/README.md) with per-module statistics, top risks, cross-module systemic defects and a phased TODO list (T1-T19). No production code is modified.
2026-09-14 22:16:27 +08:00

54 KiB
Raw Permalink Blame History

审计报告module/base/logs

1. 模块概览

module/base/logs 是操作日志(审计日志)服务,提供日志写入与查询的 Gin REST 接口,另含一个独立 CLI 工具。模块共 12 个 Go 文件、319 行(不含 pb无任何测试文件。

内容 证据
模块路径 bsm/full/module/base/logs module/base/logs/go.mod:1
对外路由前缀 /rest/logs internal/routers/register.go:13 v1_key := fmt.Sprintf("/rest/%s", srvKey)
接口清单 GET /rest/logs/pingPOST /rest/logs/createPOST /rest/logs/fetchPOST /rest/logs/total internal/routers/register.go:24-27
数据模型 单表 LogData(操作日志) internal/models/log_data.go:9-21
依赖 DM/Postgres配置声明 dm、Redis、Etcd、GORM etc/logs_dev.yaml:4-16internal/impl/impl.go:20-35
部署形态 1独立 cmd/main 独立进程supervisor 管理,/data/app/pro-oplogsuser=root cmd/main/main.go:21-49etc/supervisor.pro-oplogs.conf:1-8
部署形态 2宿主 pkgs/allpkgs/ecmall 通过 service.Expose 挂到宿主 gin.Engine service/expose.go:13-17pkgs/all/internal/service/logs.go:9-14
配置名不一致 yaml 内 Service: oplogs,而 srvKey/路由/配置文件名均为 logs etc/logs_prod.yaml:1 vs cmd/main/main.go:18cmd/main/main.go:35

模块自身定位为「匿名路由」模块(registerAnonymous),写日志接口本应由内网服务调用;但两条部署路径的鉴权、校验、索引、迁移、配额全部缺失或写错,实际不可用于承载可信审计。

2. 审计范围与方法

已读全部源码12 个非 pb Go 文件319 行)

  • cmd/main/main.go(53)、cmd/cli/main.go(21)
  • internal/config/config.go(28)、internal/impl/impl.go(36)
  • internal/logic/hello/ping.go(10)、internal/logic/log/create.go(47)、internal/logic/log/fetch.go(65)、internal/logic/log/total.go(34)
  • internal/models/log_data.go(25)、internal/routers/register.go(30)
  • service/dependencies.go(29)、service/expose.go(17)

配置与测试资产etc/logs_dev.yamletc/logs_prod.yamletc/logs_test.yamletc/supervisor.pro-oplogs.conftest/{log,list,cnt}.httpREADME.mdgo.modgo.sum

交叉验证的外部实现(用于判定结论真伪,非审计对象)

  • SDK D:\work\bsm-sdk\corewith/databases.godatabase/new.godatabase/sql/postgresql.gotypes/db.goconf/new.goinfra/response.goinfra/logs.gomiddleware/jwt.goutils/net.go
  • 宿主:pkgs/all/internal/server/{server.go,authorization.go}pkgs/all/internal/service/logs.gopkgs/all/etc/default_dev.yamlpkgs/ecmall/internal/service/{logs.go,service_test.go}pkgs/ecmall/etc/default_dev.yamlmodule/base/mgt/internal/{routers/register.go,middleware/rbac.go}(作为「本仓库既有正确范式」对照)。
  • 依赖源码:github.com/gin-gonic/gin@v1.12.0gin.gobinding/json.gocodec/json/json.go)、gorm.io/gorm@v1.31.2gorm.goscan.gochainable_api.gofinisher_api.gocallbacks/create.goschema/field.go)。

静态检查

  • gofmt -l .(模块目录):无输出,退出码 0 —— 格式无问题。
  • go vet ./...(模块目录):无输出,退出码 0 —— 未发现 vet 级问题注意vet 无法发现本报告中的多数问题,如接口类型断言 panic 之外的逻辑缺陷)。
  • 未做运行时验证:无可用数据库/Redis未启动服务未发送真实请求。凡依赖运行时才能确认的结论均在条目内标注【推测】。

3. 问题清单

P0

1. 日志写入接口的 IP 校验逻辑完全反向,且可被 X-Forwarded-For 绕过

  • 位置module/base/logs/internal/logic/log/create.go:20-26
  • 证据
// 验证请求IP禁止公网网段提交
ip := c.ClientIP()
if ip == "127.0.0.1" || ip == "localhost" || strings.HasPrefix(ip, "10.") || strings.HasPrefix(ip, "172.") || strings.HasPrefix(ip, "192.") {
    log.Printf("request ip is not allowed")
    err := errors.New("request ip is not allowed")
    infra.Response.Error(c, err)
    return
}

注释与代码语义相反:命中分支返回的是「拒绝」,但条件是内网/回环地址,即内网调用方(真实写日志的服务,源 IP 必为 10./172./192.)全部被拒绝,而任何公网来源(不属于这三个前缀)反而放行。另外 c.ClientIP() 使用的 X-Forwarded-For 在本仓库未被收窄:

// github.com/gin-gonic/gin@v1.12.0/gin.go:225
trustedProxies:         []string{"0.0.0.0/0", "::/0"},
trustedCIDRs:           defaultTrustedCIDRs,

gin 默认信任所有代理(cmd/main/main.go 未调用 SetTrustedProxies),故客户端自带的 X-Forwarded-For: 1.2.3.4 即成为 ClientIP() 返回值,可同时用于「绕过黑名单」与「伪造落库 IP」。此外 ip == "localhost" 永不成立(ClientIP 返回 IP 字面量),192. 前缀覆盖了整个 192.0.0.0/8(含公网段)属过度匹配。

  • 影响:写日志接口对公网来源开放(配合问题 2 即为匿名可写而设计上唯一的合法写入方内网服务100% 被拒 —— 日志伪造/污染与功能不可用同时成立,写日志接口实际处于「攻击者能用、自己人不能用」的状态。审计数据一旦被污染,事后追责与取证基础即被破坏。
  • 建议:删除该 IP 黑名单逻辑,改为在反向代理层/服务层做白名单(允许写入的服务身份),并显式 router.SetTrustedProxies([]string{"<实际代理IP>"});服务端一律以 c.ClientIP() 覆盖请求体中的 ip 字段。

2. 模块路由零鉴权(独立部署形态),审计日志可被匿名伪造与读取

  • 位置module/base/logs/internal/routers/register.go:17-29cmd/main/main.go:26-42etc/supervisor.pro-oplogs.conf:1-8
  • 证据
// registerAnonymous  不需要auth的接口
func registerAnonymous(v1_key string, engine *gin.Engine) {
	anonymous := engine.Group(v1_key)
	{
		// anonymous.Use(middleware.JwtAuth(impl.RedisCache))
		anonymous.GET("/ping", hello.Ping)
		anonymous.POST("/create", log.Create)
		anonymous.POST("/fetch", log.LogFetch)
		anonymous.POST("/total", log.Total)
	}
}

独立部署路径只挂了 Recovery 与健康检查,没有任何鉴权中间件:

app := gin.Default()
if env.Runtime.Mode == vars.RUN_MODE_PROD { gin.SetMode(gin.ReleaseMode) } else { gin.SetMode(gin.DebugMode) }
app.Use(gin.Recovery())
app.HEAD("/", infra.Health)
routers.Register(ServiceKey, app)
err := app.Run(fmt.Sprintf(":%s", config.Spec.Port))

而该独立进程正是被 supervisor 以 user=root 常驻运行的形态(etc/supervisor.pro-oplogs.conf:1-8)。宿主形态下由外层兜底,但仅把 /rest/logs/ping 列入匿名清单,create/fetch/total 依赖宿主 JWT

# pkgs/all/etc/default_dev.yaml:38ecmall 同)
    - /rest/logs/ping
// pkgs/all/internal/server/authorization.go:57-63
requestPath := canonicalHTTPPath(r.URL.Path)
if !a.isAnonymous(requestPath) {
    if err := a.validate(r.Header.Get("Authorization")); err != nil { writeHTTPAuthorizationError(w, err); return }
}
  • 影响:独立部署(含生产 supervisor 配置)下,create 可被任意匿名调用写入伪造日志,fetch/total 可被任意匿名调用读取全量操作日志含操作人、IP、业务内容——未授权写入 + 隐私/审计数据泄露。宿主形态下若任一宿主将来把 /rest/logs/* 整体放入匿名清单(当前 pkgs/allpkgs/ecmall 的清单为复制关系),模块自身没有任何第二道防线。
  • 建议createfetch/total 分离:create 使用服务间身份mTLS/内网 + 共享密钥或 SDK 的 SecretKey 校验),fetch/total 使用 middleware.JwtAuth(true) 并叠加 mgt 风格的 RequireAdmin;不要用注释掉中间件的方式声明匿名。

P1

3. fetch[]any 作为查询目标GORM 扫描路径不成立(有数据即 500【推测】

  • 位置module/base/logs/internal/logic/log/fetch.go:27-31fetch.go:55
  • 证据
	var (
		data  []any
		count int64
		tx    = impl.DBService.Model(&models.LogData{})
	)
	...
	if err := tx.Count(&count).Limit(size).Offset((page - 1) * size).Find(&data).Error; err != nil {

GORM 只对特定目标类型提供 map 扫描分支:

// gorm.io/gorm@v1.31.2/scan.go:146-178
switch dest := db.Statement.Dest.(type) {
case map[string]interface{}, *map[string]interface{}: ...
case *[]map[string]interface{}: ...
case *int, *int8, ...: ...
default: // 走到这里

*[]interface{} 落在 default 分支,reflectValueTypeinterface{},于是按切片逐行 elem = reflect.New(reflectValueType)scan.go:328)得到 *interface{},再进入 db.scanIntoStructfield.Setfield.ReflectValueOf

// gorm.io/gorm@v1.31.2/schema/field.go:509-512
field.ReflectValueOf = func(ctx context.Context, v reflect.Value) reflect.Value {
	v = reflect.Indirect(v)
	return v.Field(fieldIndex)
}

*interface{}Indirect 后 Kind 为 Interface调用 reflect.Value.Field 会 panicreflect: call of reflect.Value.Field on interface Value。GORM 支持的 map 查询写法是 Find(&[]map[string]interface{}{}),而非 []any。本项未实机运行(无可用数据库),故标注【推测】,但因果链完整、可复现。

  • 影响POST /rest/logs/fetch 在表中有任何数据时即 panicgin.Recovery 兜成 500日志列表功能等于不可用同时 panic 会打印堆栈(含 SQL/表结构信息)。
  • 建议:改为 var data []map[string]any(或强类型 []models.LogData+ Select 显式列白名单,禁止把数据库原始列直接回吐给调用方。

4. 客户端可控字段的 interface{} 类型断言导致 paniclevel 分支必然 panic

  • 位置module/base/logs/internal/logic/log/fetch.go:35-46
  • 证据
	if request["op_name"] != "" {
		tx.Where("op_name like ?", "%"+request["op_name"].(string)+"%")
	}
	...
	if request["op_ip"] != "" {
		tx.Where("op_ip = ?", "%"+request["ip"].(string)+"%")
	}
	if request["level"] != "" {
		tx.Where("level = ?", request["level"].(int))
	}

requestmap[string]any,由 c.BindJSONencoding/json 解码,而 gin 默认未开启 UseNumber

// github.com/gin-gonic/gin@v1.12.0/binding/json.go:19
var EnableDecoderUseNumber = false

因此 {"level":1} 解出的动态类型是 float64request["level"] != "" 为真(不同动态类型),随后 .(int) 断言失败 panic{"level":"1"} 同样 panic。request["op_ip"] 分支还取错了 key判空用 op_ip,取值用 ip,只传 op_iprequest["ip"] 为 nilnil.(string) panic。op_name/service 传入数字或对象亦会 panic。

  • 影响:任何按 level 过滤的正常请求(以及只传 op_ip 的请求)都会 500接口约定无法使用panic 堆栈经日志外泄内部实现。
  • 建议:定义强类型请求结构体(struct{ Page,Size int; OpName,Service string; Level uint; ... } + binding tag杜绝 map[string]any + 裸 .(T) 断言。

5. fetch 分页与排序完全失效,且无时间范围、Count 为全表扫描

  • 位置module/base/logs/internal/logic/log/fetch.go:33-55
  • 证据
	var size int
	var page int
	...只读取 op_name/service/op_ip/level从未读取 request["page"]/request["size"]
	if page <= 0 { page = DefaultPage }
	if size <= 0 { size = DefaultSize }
	if err := tx.Count(&count).Limit(size).Offset((page - 1) * size).Find(&data).Error; err != nil {

page/size 声明后从未赋值,恒为 0因此恒定 page=1,size=50test/list.http:6-7 传入的 page/size 被静默忽略。查询无 ORDER BY,无 created_at 范围条件;CountFind 复用同一 tx,计数不受分页限制。

  • 影响:调用方无法翻页(第 51 条以后永远取不到),结果集顺序由数据库决定(分页语义不确定);日志表持续增长后,每次请求都带一次无范围的全表 COUNT + 无索引条件扫描(见问题 7构成性能瓶颈。
  • 建议:解析并校验 page/size(设上限,如 size<=200),默认按 created_at DESC, id DESC 排序,强制/默认 created_at 时间窗(如最近 7 天),Count 与列表查询分离并使用同一套过滤条件构造器。

6. total 接口语义错误,只返回一个 level 的计数,且空表返回误导性错误

  • 位置module/base/logs/internal/logic/log/total.go:22-31
  • 证据
	tx := impl.DBService.Model(&models.LogData{})
	if request["service"] != "" {
		tx.Where("service = ?", request["service"])
	}
	var result map[string]any
	if err := tx.Select("level", "count(level)").Group("level").First(&result).Error; err != nil {
		infra.Response.Error(c, errcode.ErrDB)
		return
	}

Group("level") 会产生多行,但 First 只取第一行(ORDER BY <pk> LIMIT 1),故「按级别统计」实际只返回一个 level聚合列无别名count(level) 的 key 依赖驱动PostgreSQL 下为 count,不可预测);表为空时 First 返回 ErrRecordNotFound,被统一转成 ErrDB,把「无数据」误报为数据库故障。

  • 影响:该接口返回值既非总量也非分布,前端无法使用;无数据场景误报错误码,增加排障噪音;GROUP BY 无任何时间/服务范围约束,属全表聚合。
  • 建议:改用 Select("level AS level, count(*) AS total").Group("level").Find(&rows)[]struct{Level uint; Total int64}),空结果返回空数组;补 created_at 时间窗与服务维度过滤。

7. log_data 表零索引(时间、服务、级别、操作人均无索引)

  • 位置module/base/logs/internal/models/log_data.go:9-21
  • 证据
type LogData struct {
	ID        uint      `gorm:"column:id;primarykey;"  json:"id"`
	CreatedAt time.Time `gorm:"column:created_at;type:TIMESTAMP;" json:"created_at"`
	Service   string    `gorm:"column:service;type:varchar(255);def:'def'" json:"service"` // 服务名称
	OpID      uint      `gorm:"column:op_id;" json:"op_id"`                                // 操作人员ID
	OpName    string    `gorm:"column:op_name;type:varchar(255);" json:"op_name"`          // 操作人员姓名
	OpIp      string    `gorm:"column:ip;type:varchar(255);def:'0.0.0.0'" json:"ip"`       // 操作IP

同仓库 SDK 的标准模型均显式声明索引(// bsm-sdk/core/types/db.go:31-34DeletedAt ... index;Status int8 ... default:0;index;),本模型除主键外无任何 index/uniqueIndex。而查询侧使用的是 op_name like '%x%'service like '%x%'(前导通配符无法走索引)与无范围 WHERE/GROUP BY

  • 影响:日志表是本系统写入最频繁的表之一,随数据增长读写双双劣化;fetch/total 将逐渐拖垮数据库连接池(MaxOpenConns=64internal/impl/impl.go:27),成为全局性故障源。
  • 建议:至少建立 index(created_at)index(service, created_at)index(level, created_at)index(op_id);把 like '%x%' 收敛为前缀匹配或去掉 % 前导;为日志表规划分区/归档。

8. 自动迁移形同虚设(IsAutoMigrate 恒为 falsePostgres 分支根本不迁移)

  • 位置module/base/logs/internal/models/log_data.go:23-25module/base/logs/internal/impl/impl.go:25-32
  • 证据
// models/log_data.go
func init() {
	database.AppendMigrate(&LogData{})
}
// impl/impl.go显式传入非 nil 的 opts未设置 IsAutoMigrate零值 false
opts := &types.SqlOptions{
	MaxIdleConns:    vars.SqlOptionMaxIdleConns,
	MaxOpenConns:    vars.SqlOptionMaxOpenConns,
	ConnMaxLifetime: vars.SqlOptionConnMaxLifetime,
	LogStdout:       false,
	Debug:           false,
}
DBService = with.Databases(config.Spec.Databases, opts)

SDK 只在 IsAutoMigrate 为真时迁移,且该开关仅对 MySQL 路径有效:

// bsm-sdk/core/database/new.go:43-48
if len(MigrateTables) > 0 && options.IsAutoMigrate {
	err = db.AutoMigrate(MigrateTables...)
// bsm-sdk/core/database/sql/postgresql.go:11-23仅当 options==nil 才给默认值,而 impl 传了非 nil
func SetOptions(options *types.SqlOptions) *types.SqlOptions {
	if options == nil { options = &types.SqlOptions{... IsAutoMigrate: false ...} }
	return options
}

NewPostgreSqlpostgresql.go:27-63)全程没有 AutoMigrate 调用;宿主 pkgs/all/internal/impl/impl.go:38 也以 with.Databases(config.Spec.Databases, nil) 建连,同样不会迁移。

  • 影响AppendMigrate 是死代码,log_data 表既不会自动创建也不会随模型演进变更;骨架/新环境部署后所有接口直接报「表不存在」,而错误被统一吞成 errcode.ErrDB,排查成本高。
  • 建议:显式设置 IsAutoMigrate: true 并让 Postgres 路径同样支持;或改为独立的、可版本化的迁移脚本/工具,在启动时校验 log_data 存在并 fail-fast 提示。

9. 写入契约不匹配 + 无字段校验 + 完整性字段(hmac/encry)未实现 → 审计内容丢失

  • 位置module/base/logs/internal/models/log_data.go:9-21internal/logic/log/create.go:28-46、SDK bsm-sdk/core/infra/logs.go:9-19
  • 证据SDK 提供的生产者模型与本服务消费者模型字段名不一致:
// bsm-sdk/core/infra/logs.go:9-19生产者
type LogItem struct {
	OpID uint `json:"op_id"`; OpName string `json:"op_name"`; OpType string `json:"op_type"`
	Text string `json:"text"`; Code string `json:"code"`; Level uint `json:"level"`
	Ip string `json:"ip"`; Module string `json:"module"`; Encry bool `json:"encry"`
}
// models/log_data.go:12-20消费者
Service string `json:"service"`; OpID uint `json:"op_id"`; OpName string `json:"op_name"`
OpIp string `json:"ip"`; DataType string `json:"data_type"`; Level uint `json:"level"`
Content string `json:"content"`; Hmac string `json:"hmac"`; Encry bool `gorm:"-"`

op_type/text/code/module 在消费者侧没有对应字段,而 BindJSON 默认不拒绝未知字段:

// github.com/gin-gonic/gin@v1.12.0/binding/json.go:25
var EnableDecoderDisallowUnknownFields = false

create.go:28-44 只判断数组非空,不做必填/长度/枚举校验;HmacEncryDataType 在模块内除结构体声明外无任何读写(全模块 grep 仅命中 models/log_data.go)。

  • 影响:按 SDK/仓库文档调用写入接口时,「谁做了什么」的正文(text/module/op_type/code)被静默丢弃,落库记录只剩操作人/级别/IP —— 审计记录事实性残缺;hmac 字段留空意味着审计日志无任何防篡改校验,encry 开关是空承诺(敏感内容既未加密也未脱敏)。
  • 建议:统一生产者/消费者契约SDK LogItemLogData 二选一为准并同步双方),开启 DisallowUnknownFields 或在处理器内显式校验必需字段,补 Content 长度上限与敏感字段脱敏;要么实现 hmac 签名/校验与加密,要么删除这两个字段以免误导。

10. 写入无配额、无限流、无请求体/字段长度上限,且无保留与清理策略

  • 位置module/base/logs/internal/logic/log/create.go:28-44internal/models/log_data.go:18
  • 证据
	err := c.BindJSON(&request)   // 无 MaxBytesReader、无数组长度上限
	...
	if len(request) == 0 { ... }
	if err := impl.DBService.Model(&models.LogData{}).Create(&request).Error; err != nil {

Content stringsize/长度约束Postgres 下为无限制 textService/OpName 之外的业务字段同样无上限。全仓库检索无任何限流实现:

grep -r "ratelimit|RateLimit|Limiter|MaxBytesReader" module/  → No matches found

etc/supervisor.pro-oplogs.conf 仅重定向 stdout数据库侧无 TTL/保留策略、无归档任务、无 DELETE 接口。

  • 影响:单次请求可提交任意长度数组与任意大小 content,配合问题 1/2公网可达且匿名即为可持续的写放大攻击撑满磁盘、拖垮数据库连接池无任何自动回收机制表无限增长后问题 5/6/7 的查询会直接压垮数据库。
  • 建议:加 http.MaxBytesReader(如 1MB与单请求条数上限如 500 条)、单字段长度上限并截断;写入路径引入批量/异步缓冲队列与背压;建立保留策略(分区 + 定期归档/删除,如 90 天),并对写入速率按调用方配额限流。

11. 审计字段全部信任客户端:created_at/ip/op_id/op_name/level 均可伪造

  • 位置module/base/logs/internal/logic/log/create.go:28-44internal/models/log_data.go:10-17
  • 证据:模型字段直接映射 JSON且处理器从不覆盖
	CreatedAt time.Time `gorm:"column:created_at;type:TIMESTAMP;" json:"created_at"`
	OpID      uint      `gorm:"column:op_id;" json:"op_id"`
	OpName    string    `gorm:"column:op_name;type:varchar(255);" json:"op_name"`
	OpIp      string    `gorm:"column:ip;type:varchar(255);def:'0.0.0.0'" json:"ip"`

GORM 仅在字段为零值时才填充创建时间,客户端提供值即原样入库:

// gorm.io/gorm@v1.31.2/callbacks/create.go:340-347
if values.Values[0][idx], isZero = field.ValueOf(stmt.Context, stmt.ReflectValue); isZero {
	if field.DefaultValueInterface != nil { ... } else if field.AutoCreateTime > 0 || field.AutoUpdateTime > 0 {
		stmt.AddError(field.Set(stmt.Context, stmt.ReflectValue, curTime))

create.go 中亦没有 request[i].OpIp = c.ClientIP() 之类的强制赋值(仅把 ClientIP() 用在问题 1 的反向黑名单上)。test/log.http:11"ip": "192.168.1.1" 即为客户端自填示例。

  • 影响:任何人都能写入任意历史时间、任意操作人、任意 IP 的"审计"记录,时间线与责任人字段完全不可信;审计日志作为证据链的价值归零(配合问题 2 可匿名实施)。
  • 建议:服务端生成 created_at(忽略客户端该字段,使用 select 白名单或 DTO 剥离)、以 c.ClientIP() 覆盖 ipop_id/op_name 由认证身份JWT/服务身份)推导而非请求体提供;对写入内容做签名(hmac)以便事后校验完整性。

12. 配置中明文数据库口令,且 prod 与 dev 使用同一库、同一口令

  • 位置module/base/logs/etc/logs_dev.yaml:4-10module/base/logs/etc/logs_prod.yaml:4-10
  • 证据:两文件数据库段与缓存段完全一致(含真实口令),差异仅在注释:
# logs_dev.yaml:5-10 与 logs_prod.yaml:5-10 完全相同
  Driver: dm
  Source:
    - dm://SYSDBA:Yd@2aMwVAcJj4dA@172.21.138.165:5236?database=DAMENG&charset=utf8&parseTime=True&loc=Asia%2FShanghai
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/0
  • 影响:生产数据库口令以明文形式进入代码仓库与所有克隆(口令 Yd@2aMwVAcJj4dA、内网地址 172.21.138.165 泄露);生产与开发共用同一实例/账号,任何开发侧误操作直接作用于生产数据;口令一旦外泄无法通过配置回滚。
  • 建议:口令改为环境变量占位(配置已支持 os.ExpandEnv,见 bsm-sdk/core/conf/new.go:55,直接用 ${LOGS_DB_PASSWORD}),仓库内不保留真实值;生产使用独立账号与最小权限(仅 INSERT/SELECT 本表);轮换已泄露口令。

13. prod/dev 声明 Driver: dmSDK 不支持该驱动 → 独立部署启动即 panic

  • 位置module/base/logs/etc/logs_dev.yaml:5module/base/logs/etc/logs_prod.yaml:5module/base/logs/etc/logs_test.yaml:5
  • 证据
Driver: dm
// bsm-sdk/core/database/new.go:29-36
switch driver {
case "mysql":
	db, err = NewMysql(dsn, options)
case "postgres":
	db, err = NewPostgres(dsn, options)
default:
	return nil, fmt.Errorf("unsupported database driver: %s", driver)
}
// bsm-sdk/core/with/databases.go:20-25
db, err := database.NewDatabase(cfg.Driver, cfg.Source, opts)
if err != nil {
	printer.Error("Database Init Failed !")
	panic(err)
}

DM 驱动仅在 conf/types.go:19 的注释中被提及SDK 与模块的 go.mod 中均无任何达梦驱动依赖(module/base/logs/go.mod:13-73 仅 mysql/postgres/mongo/redis 等)。cmd/main/main.go:23 直接 impl.NewImpl() → panic。测试配置用 postgres 但指向 password=CHANGE_ME 的本地库,同样不可用。

  • 影响:以仓库内配置独立启动该服务必然崩溃(panic: unsupported database driver: dm);配置与运行时代码能力不一致,说明这份 etc 从未在 CI 中真正启动验证过。
  • 建议:明确目标数据库并统一(若确需 DM需引入并注册达梦驱动并覆盖 SDK 的驱动分发);Driver 取值在配置校验阶段做白名单校验;把「服务可启动」纳入 CI 冒烟。

14. 宿主模式下无角色/权限校验:任意登录用户可读写全量审计日志

  • 位置module/base/logs/internal/routers/register.go:18-29(对照 module/base/mgt/internal/routers/register.go:41-47module/base/mgt/internal/middleware/rbac.go:39-54
  • 证据mgt 模块给出了本仓库既有的正确范式:
// module/base/mgt/internal/routers/register.go:42-47
auth := middleware.JwtAuth(true)
admin := mgtmw.RequireAdmin()
userGroup := engine.Group(base + "/user")
userGroup.Use(auth)

而 logs 模块仅把 JwtAuth 注释掉、无任何等价角色中间件(详见问题 2 证据);宿主层的 authorization.httpMiddleware 只做「token 有效」判定(pkgs/all/internal/server/authorization.go:57-63),不含角色信息。

  • 影响:宿主形态下任何已登录用户(含普通 C 端账号,若与后台共用同一 JWT 签发密钥都能伪造审计日志条目并读取全量操作日志操作人、IP、业务内容属横向越权与隐私泄露审计数据的可信度依赖「所有登录用户都无恶意」这一不成立假设。
  • 建议create 收敛为服务间调用(网络/密钥双重限制);fetch/total 增加管理员/审计员角色校验(复用 mgt 的 RequireAdmin 或等价的权限点),并对读取行为本身记录访问日志。

15. infra.Response 为包级单例且方法直接改字段 → 并发数据竞争与响应串包

  • 位置module/base/logs/internal/logic/log/create.go:24,42,46fetch.go:23,57,61total.go:18,29,33(根因在 SDK
  • 证据
// bsm-sdk/core/infra/response.go:12
var Response Reply
...
func (reply *Reply) Success(ctx *gin.Context, data any) {
	reply.Code = 0
	reply.Details = data
	reply.Message = ""
	reply.Timeseq = time.Now().UnixMilli()
	...
	ctx.JSON(200, reply)
}

所有处理器共享同一个 Reply 实例并无锁地写 Code/Details/Message/Timeseq,随后序列化该共享结构体。

  • 影响:并发请求下存在数据竞争(go test -race 可复现),且一个请求可能读到另一个请求尚未覆盖的 Details —— 响应串包,把 A 调用方查询到的日志内容返回给 B 调用方(跨请求数据泄露)。日志写入接口是典型高并发入口,触发概率不低。
  • 建议:改用值语义/局部实例(infra.Response.Success 内部 reply := Reply{...} 后回写,或 SDK 改为返回结构体),模块侧可先行规避:处理器内构造并 c.JSON 自有响应结构;同时给 SDK 补并发测试。

P2

16. 全链路未传递 context,也没有查询级超时

  • 位置internal/logic/log/create.go:40fetch.go:30,55total.go:22,28
  • 证据:模块内无任何 WithContext/context 引用(全模块 grep 无命中),查询形如:
if err := impl.DBService.Model(&models.LogData{}).Create(&request).Error; err != nil {

GORM 默认使用其内部上下文,客户端断开或网关超时后数据库操作仍会继续执行到底。

  • 影响:慢查询/大表扫描期间客户端已放弃,数据库仍在消耗连接与 IO放大了问题 5/7 的容量风险;无法实现请求级超时与取消。
  • 建议:统一改为 impl.DBService.WithContext(c.Request.Context()),并在配置中加入查询超时(context.WithTimeout)。

17. 依赖以全局可变变量持有,双初始化路径 + nil 静默跳过

  • 位置internal/impl/impl.go:13-17,20-35service/dependencies.go:19-28service/expose.go:13-17
  • 证据
var (
	RedisService *redis.RedisClient
	EtcdService  *clientv3.Client
	DBService    *gorm.DB
)
...
func applyDependencies(deps Dependencies) {
	if deps.Redis != nil { impl.RedisService = deps.Redis }
	if deps.Etcd != nil { impl.EtcdService = deps.Etcd }
	if deps.DB != nil { impl.DBService = deps.DB }
}

applyDependencies 对 nil 依赖静默跳过:宿主若因故障未初始化 DBpkgs/all/internal/impl/impl.go:38 直接依赖 with.Databases 成功),impl.DBService 保持 nilcreate/fetch/total 首行即空指针 panic。同时模块存在两条互相独立的初始化路径NewImpl()applyDependencies()),二者写入同一批全局变量且无同步。

  • 影响:依赖缺失从「启动失败并报警」退化为「运行期 500 且原因隐晦」;并发/重复初始化时对全局指针的写入存在竞态;测试与生产易装配出不同依赖组合。
  • 建议:依赖注入收敛为一次显式装配(Expose 内校验必需依赖非 nil 并返回错误),DBService 为 nil 时 fail-fast避免包级可变全局或至少用 sync.Once 保护。

18. 独立部署缺 HTTP 超时与优雅退出,启动失败直接 panic

  • 位置cmd/main/main.go:36-48
  • 证据
	app.Use(gin.Recovery())
	...
	err := app.Run(fmt.Sprintf(":%s", config.Spec.Port))
	if err != nil {
		panic(err)
	}

app.Run 使用默认 http.Server(无 ReadTimeout/WriteTimeout/ReadHeaderTimeout/IdleTimeout/MaxHeaderBytes),无信号处理与 Shutdown。宿主的服务器实现给出了应有基线:

// pkgs/all/internal/server/server.go:72-78
s.http = &http.Server{Addr: httpAddr, Handler: s.auth.httpMiddleware(handler),
	ReadHeaderTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, MaxHeaderBytes: 1 << 20}
  • 影响:慢速请求/超大 header 可长期占用连接(叠加问题 10 的无体积限制);进程重启时无优雅退出,写入中的请求被硬切断且无 drainpanic 使退出码与错误信息不友好supervisor 只能反复拉起。
  • 建议:显式构造 http.Server 并设置超时与 MaxHeaderBytes,监听 SIGTERM/SIGINT 调用 Shutdown(ctx);启动错误用 log.Fatal/结构化错误上报替代 panic

19. 配置校验薄弱,且存在非法 GORM tagdef: / def:1)被静默忽略

  • 位置internal/config/config.go:19-27internal/models/log_data.go:12,15,17
  • 证据
	Spec.Port = conf.CheckPort(Spec.Port)   // 仅处理空串,非法值不校验
	conf.NotNil(Spec.Service, Spec.Cache)   // Databases / Rpc / Etcd 均未校验
	Service   string `gorm:"column:service;type:varchar(255);def:'def'" json:"service"`
	OpIp      string `gorm:"column:ip;type:varchar(255);def:'0.0.0.0'" json:"ip"`
	Level     uint   `gorm:"column:level;def:1" json:"level"`

def: 不是 GORM 标签键(正确为 default:SDK 其他模型使用的是合法形式(bsm-sdk/core/types/db.go:34 gorm:"column:status;default:0;index;")→ 默认值不会生效,Service/ip/level 缺省时写入空串/0。

  • 影响Databases 缺省会一路走到 with.Databasespanic("No Database Source Found !")bsm-sdk/core/with/databases.go:13-15),端口填错(如 abc)直到 app.Run 才失败;默认值不生效造成数据质量下降(空的 service 会让问题 6 的统计与过滤失真)。
  • 建议NotNil 覆盖 Databases.Driver/SourceEtcd 等必需项,端口做数值/范围校验;修正为 default:'def'/default:1,并对缺省字段在写入前做服务端填充与校验。

20. 日志体系不统一:使用标准库 log 而非 SDK printer,无级别、无结构化、无 trace 关联

  • 位置internal/logic/log/create.go:5,22,30,35,41fetch.go:5,22,56total.go:5,17
  • 证据
import "log"
...
log.Printf("save log data error: %v", err)

全仓库其他模块统一使用 SDK 的统一日志(printer.Info/Error,抽样 250+ 命中,如 module/base/mgt/internal/logic/user/create.go:46);本模块是无级别、无结构化的 stderr 输出,由 supervisor 落盘(etc/supervisor.pro-oplogs.conf:8 stdout_logfile=/data/app/logs/pro-oplogs.log)。错误日志只带 gorm 错误串,无请求 ID/调用方/trace。

  • 影响:无法按级别过滤与告警,无法关联一次请求的上下文(尤其日志服务本身出问题时难以自证),与全仓日志采集/APM 方案脱节(etc/logs_prod.yaml:24-27 的 APM 段亦被注释)。
  • 建议:统一改用 printer(或 SDK logger带上请求 ID、服务名、耗时字段对写入失败率、拒绝原因分级别输出接入 APM/告警。

21. 表名跨驱动不一致,且无保留策略与自监控

  • 位置internal/models/log_data.go:9(无 TableName()
  • 证据Postgres 连接启用单数表名策略,而 MySQL 路径使用默认复数策略:
// bsm-sdk/core/database/sql/postgresql.go:39-41
NamingStrategy: schema.NamingStrategy{ SingularTable: true, ... }
// bsm-sdk/core/database/new.go:89-91MySQL 路径无 NamingStrategy
gorm.Open(mysql.Open(dsn0), &gorm.Config{SkipDefaultTransaction: true})

模型未定义 TableName(),故 Postgres 下建/查 log_dataMySQL 下为 log_datas。同时模块无清理/归档任务、无自监控指标(写入量、失败率、表行数)。

  • 影响:数据库迁移或驱动切换时表名静默漂移,运维脚本/对账查询易指向空表;表无保留策略将无限增长(见问题 10服务自身无任何健康度量化指标。
  • 建议:显式实现 func (LogData) TableName() string { return "log_data" } 统一表名;增加按时间的分区/归档任务与清理阈值,并暴露写入计数/失败计数指标。

22. 时间语义与时区未定义,存在 8 小时偏移风险【推测】

  • 位置internal/models/log_data.go:11etc/logs_prod.yaml:7etc/logs_test.yaml:7
  • 证据:模型直接使用 time.Time,无 autoCreateTime/时区约定DSN 中显式指定了会话时区,而 Go 侧时间取进程本地时区:
- dm://...?charset=utf8&parseTime=True&loc=Asia%2FShanghai      # logs_prod.yaml:7
- host=127.0.0.1 ... TimeZone=Asia/Shanghai                      # logs_test.yaml:7
CreatedAt time.Time `gorm:"column:created_at;type:TIMESTAMP;" json:"created_at"`

部署侧(etc/supervisor.pro-oplogs.conf)未设置 TZ。若容器/主机为 UTC 而 Go 进程 time.Local 为 UTC、数据库会话为 Asia/Shanghai,则写入与读取的时间会相差 8 小时;本项需实际部署环境确认,故标注【推测】。

  • 影响:审计时间线错位,跨时区排查与合规取证出现偏差;一旦数据落库后才发现,修正需要全表订正。
  • 建议:统一以 UTC 存储(type:timestamptz/TIMESTAMP WITH TIME ZONE)、显式设置进程 TZ、在 DSN 与模型上固定同一时区,并在文档中写明;返回给前端时再按展示时区转换。

23. 链式调用丢弃返回值(当前恰好生效,写法脆弱)+ totaldata 一致性未校验

  • 位置internal/logic/log/fetch.go:36,39,42,45internal/logic/log/total.go:24
  • 证据
	tx.Where("op_name like ?", "%"+request["op_name"].(string)+"%")   // 未回写 tx

在 gorm v1.31.2 中,Model() 返回的实例 clone==0getInstance()clone==0 直接返回自身,因此上述条件恰好会生效:

// gorm.io/gorm@v1.31.2/gorm.go:448-474
func (db *DB) getInstance() *DB {
	if db.clone > 0 {
		tx := &DB{Config: db.Config, Error: db.Error}
		...   // 注意:返回的 tx 未继承 cloneclone==0
		return tx
	}
	return db
}

但这依赖 gorm 内部 clone 语义:对 Session 派生(clone==2)或未来版本/Session(&Session{NewDB:...}) 的实例,同一写法会静默丢条件且不报错(这正是该模式广受诟病的根因)。此外 totaldata 之间无任何一致性校验(count 是全表计数、data 是 50 条,两者不属于同一过滤语境)。

  • 影响:一旦依赖升级或改用 Session如为修问题 16 加 WithContext 后返回新实例),过滤条件会静默失效——查询悄悄退化为全表读取而无人察觉(安全问题 5 的越权读取会重新出现)。属高危可维护性缺陷。
  • 建议:统一改为 tx = tx.Where(...) 或每步重新赋值的写法,并用 Session(&gorm.Session{}) 明确快照语义;补一条单测断言生成的 SQL 含所有过滤条件(可用 DryRun: true)。

P3

24. 文档与真实行为不一致README 空、服务名混乱、test/*.http 全部失效

  • 位置README.md:1-2etc/logs_prod.yaml:1cmd/main/main.go:18,35test/log.http:1test/list.http:1test/cnt.http:1
  • 证据README 仅标题;配置中 Service: oplogs,而 srvKey/路由/配置文件名均为 logs
Service: oplogs
ServiceKey = "logs"                     // cmd/main/main.go:18
cfp := fmt.Sprintf("%s_%s.yaml", strings.ToLower(srvKey), ...)   // SDK 按 logs_<mode>.yaml 读取

测试脚本指向不存在的路由与字段:

POST http://127.0.0.1:16289/oplogs/v1/log      # test/log.http:1实际为 POST /rest/logs/create
{ "code": "...", "op_type": "CREATE", "text": "创建了新用户" }   # 字段在 models.LogData 中不存在
  • 影响:新成员无法据此联调(三份 .http 全部 404 或字段被丢弃),oplogs/logs 双名称在运维脚本、日志检索、配置命名上长期混淆。
  • 建议:补 README路由、字段、鉴权、错误码、示例统一服务名为 logs(或同步修改 srvKey/路由),把 test/*.http 更新为真实路径与字段并纳入日常回归。

25. CLI 工具是死代码且吞掉错误

  • 位置cmd/cli/main.go:12-19
  • 证据
	endpoint := "http://127.0.0.1:16289/oplogs/v1/log"
	data := make([]*infra.LogItem, 0)
	data = append(data, &infra.LogItem{...})
	// oplog.New(endpoint, data)
	jsonBytes, _ := json.Marshal(data)
	utils.HttpPost(endpoint, nil, jsonBytes)

被注释的调用、硬编码 endpoint 与测试数据、HttpPost 的两个返回值([]byte, error,见 bsm-sdk/core/utils/net.go:275)全部丢弃。

  • 影响:该 CLI 既不能用于联调(路由不存在,见问题 24也无法反馈失败原因属误导读者的残留代码是「本服务应由谁调用」这一契约长期含糊的证据。
  • 建议:删除该 CLI或改写为参数化的官方写入客户端正确路由、错误处理、超时并作为契约测试的载体。

26. 风格与一致性:重复中间件、调试输出、var 代替常量、字符串拼路径、ping 绕过统一响应

  • 位置cmd/main/main.go:26,36internal/routers/register.go:13,20internal/logic/log/fetch.go:13-16internal/logic/hello/ping.go:9
  • 证据
app := gin.Default()          // 已内置 Logger + Recovery
app.Use(gin.Recovery())       // 重复挂载
v1_key := fmt.Sprintf("/rest/%s", srvKey)   // mgt 使用 path.Join
fmt.Println(v1_key)                          // 调试输出残留
var ( DefaultPage = 1; DefaultSize = 50 )    // 应为 const
ctx.JSON(200, gin.H{"message": "Pong"})      // 未使用 infra.Response 统一响应
  • 影响:日志重复、响应体不统一(客户端需两套解析逻辑)、常量可变导致被意外改写;gin.DebugMode 在非 prod 下会输出路由与调试信息(cmd/main/main.go:32)。
  • 建议gin.New() + 显式中间件;路径用 path.Join;删除调试输出;常量改 constping 走统一响应结构。

27. 模块零测试

  • 位置module/base/logs/(无 *_test.go
  • 证据:模块内 12 个 Go 文件全部为业务代码,无任何测试文件(文件清单见第 1 节;对比 pkgs/all/internal/server/authorization_test.gopkgs/ecmall/internal/service/service_test.go 等宿主侧测试)。test/ 下仅 3 个已失效的 .http 脚本(问题 24
  • 影响:本报告中的问题 1、3、4、5、6 全部属「一测即现」的类型,却因零测试长期存活;后续修复无回归保护。
  • 建议至少补IP 校验行为(含 XFF 伪造)、level 过滤与分页参数解析、fetch/total 的 SQL 断言(DryRun)、空表与非空表响应、写入缺字段/超长字段的校验用例、鉴权矩阵(匿名/普通用户/管理员)。

28. 凭据卫生:生产 SecretKey 为占位符,且注释中残留第三方密钥

  • 位置etc/logs_prod.yaml:16,18-21etc/logs_test.yaml:16,18-21etc/logs_dev.yaml:16
  • 证据
SecretKey: CHANGE_ME
# Rpc:
#   fts:
#     Endpoint: https://api-v2.traingo.cn/fts/v2
#     SecretKey: 4ef05311358cd1c8f787281f08b38b1c
  • 影响:生产配置保留 CHANGE_ME 占位符说明密钥未按环境注入(若运行时真以此为密钥,等于使用公开默认值);注释中的 32 位十六进制密钥属第三方RPC凭据残留已进入仓库历史属可被直接复用的敏感信息与问题 12 同源,但对象不同)。
  • 建议:删除注释中的历史密钥并在对应系统侧轮换;SecretKey 改为必须由环境变量注入且启动时校验非默认值(宿主已有类似强校验范式:pkgs/all/internal/config/config.go:63-70Authorization.Key 做长度校验并 panic

4. 推荐优化方案

阶段一:止血(对应 P0/P11-3 天)

  1. 鉴权与可达性internal/routers/register.go 拆分路由组 —— create 放入「服务间」组(内网 + 共享密钥/签名校验),fetch/total 放入 middleware.JwtAuth(true) + RequireAdmin(复用 module/base/mgt/internal/middleware/rbac.go 的实现);删除 create.go 中的 IP 黑名单并显式 SetTrustedProxies,服务端以 c.ClientIP() 与认证身份覆盖 ip/op_id/op_name/created_at
  2. 修掉必然故障fetch 的目标类型改为 []map[string]any(或强类型 DTO+ Select 列白名单;map[string]any 全部替换为强类型请求结构体,消除 .(T) 断言 panicpage/size 解析并封顶;total 改用 Group + Find 与别名。
  3. 写入防护MaxBytesReader + 单请求条数/字段长度上限;created_at 服务端生成;补齐必填校验与 Content 脱敏;与 SDK LogItem 字段契约对齐(否则内容继续丢失)。
  4. 启动可用性:修正 Driver 与驱动实现的一致性(或补达梦驱动),SqlOptions 打开 IsAutoMigrateconfig.New 校验 Databases.Portgofmt/go vet 已通过,可立即把 go vet + 启动冒烟纳入 CI。

阶段二可持续1-2 周)

  1. 存储与查询:为 log_datacreated_at/(service,created_at)/(level,created_at)/op_id 索引;fetch/total 强制时间窗与 created_at DESC 排序;查询与计数分离。
  2. 写入路径改造:内存队列 + 批量 CreateInBatches(批量大小可配)与背压,替代逐请求直写;失败重试与丢弃计数指标;按调用方配额限流。
  3. 保留与生命周期:按月分区 + 定期归档/删除(如 90 天),把清理任务与本服务解耦(独立 Job对外暴露写入量、失败率、表行数、清理结果指标。
  4. 健壮性:全链路 WithContext(c.Request.Context()) + 查询超时;Expose 对依赖非 nil 与 Engine 非 nil 做 fail-fast独立 main 补 http.Server 超时与优雅退出;改用 printer 统一日志并带请求 ID。

阶段三:长期(随迭代)

  1. 契约与完整性:为写入引入 hmac 签名与校验(或删除该字段),敏感字段加密/脱敏策略落地SDK 与本服务的模型契约由一份来源生成/对齐,并加契约测试。
  2. 可观测与治理:接入 APM当前 etc/logs_prod.yaml:24-27 的 APM 段是注释状态);审计日志的读取行为本身写入独立访问日志;文档与 test/*.http 同步更新(问题 24、25
  3. 测试:见问题 27 的最小用例集,其中「过滤条件是否真的进入 SQL」用 gorm.Session{DryRun: true} 断言,可有效防止问题 23 类静默退化。

5. TODO 清单

  • P0-1 删除 create 的内网 IP 黑名单,改为服务间来源白名单并显式收窄 trusted proxies验收内网合法调用返回成功、伪造 X-Forwarded-For 无法改变判定与落库 IP涉及module/base/logs/internal/logic/log/create.go:20module/base/logs/cmd/main/main.go:26
  • P0-2 createfetch/total 分组鉴权,移除匿名声明|验收:未带合法服务身份/管理令牌调用 createfetchtotal 均被拒401/403ping 保持匿名|涉及:module/base/logs/internal/routers/register.go:18
  • P1-3 fetch 查询目标改为 []map[string]any 或强类型 DTO 并限制返回列|验收:非空表调用 /rest/logs/fetch 返回 200 与受限列集合,无 panic涉及module/base/logs/internal/logic/log/fetch.go:29module/base/logs/internal/logic/log/fetch.go:55
  • P1-4 用强类型请求结构体替换 map[string]any + 裸类型断言|验收:{"level":1}{"op_ip":"1.2.3.4"}{"op_name":1} 请求均正常返回,无 panic涉及module/base/logs/internal/logic/log/fetch.go:35module/base/logs/internal/logic/log/fetch.go:42module/base/logs/internal/logic/log/fetch.go:45
  • P1-5 解析并封顶 page/size,强制时间窗与 created_at DESC 排序,计数与列表分离|验收:传入 page=2&size=10 返回第 2 页且 total 与过滤条件一致|涉及:module/base/logs/internal/logic/log/fetch.go:33module/base/logs/internal/logic/log/fetch.go:55
  • P1-6 重写 totalGroup("level") + Find + 列别名,空结果返回空集合|验收:多级别数据返回多行;空表返回空数组且 code=0涉及module/base/logs/internal/logic/log/total.go:28
  • P1-7log_data 增加 created_at/(service,created_at)/(level,created_at)/op_id 索引|验收:EXPLAIN 显示索引扫描,千万行级 fetch P99 达标|涉及:module/base/logs/internal/models/log_data.go:9
  • P1-8 修正迁移开关并让 Postgres 路径支持 AutoMigrate或改为独立迁移验收全新空库启动后 log_data 表存在,接口不再报「表不存在」|涉及:module/base/logs/internal/impl/impl.go:25module/base/logs/internal/models/log_data.go:24
  • P1-9 统一 SDK LogItemLogData 契合并补字段校验/长度上限/必要脱敏;决定 hmac/encry 去留|验收:按 SDK 写入后 content/service/data_type 有值,超长与缺字段请求被拒|涉及:module/base/logs/internal/models/log_data.go:12module/base/logs/internal/logic/log/create.go:28
  • P1-10 增加请求体/条数/字段长度限制、写入配额限流与保留清理策略|验收:超限请求 413/400压测下写入速率受限且库容量可回收涉及module/base/logs/internal/logic/log/create.go:28
  • P1-11 服务端强制生成 created_at、覆盖 ip、由身份推导 op_id/op_name|验收:请求体携带的 created_at/ip/op_id 被忽略,落库为服务端值|涉及:module/base/logs/internal/logic/log/create.go:28module/base/logs/internal/models/log_data.go:10
  • P1-12 口令与地址改为环境变量注入,生产使用独立低权限账号并轮换已泄露口令|验收:仓库内无明文口令,grep 不到 Yd@2aMwVAcJj4dAprod/dev 指向不同库|涉及:module/base/logs/etc/logs_prod.yaml:7module/base/logs/etc/logs_dev.yaml:7
  • P1-13 解决 Driver: dm 与 SDK 驱动能力不一致(补驱动或改驱动),并加驱动白名单校验|验收:以 etc/logs_<mode>.yaml 独立启动成功,非法 driver 在配置校验阶段即报错|涉及:module/base/logs/etc/logs_prod.yaml:5module/base/logs/internal/config/config.go:21
  • P1-14 fetch/total 增加管理员/审计员角色校验,读取行为留痕|验收:普通登录用户调用返回 403管理员成功涉及module/base/logs/internal/routers/register.go:26
  • P1-15 消除 infra.Response 并发共享写入(模块内改用局部响应实例,并推动 SDK 修复)|验收:go test -race 并发调用无竞争报告,响应内容不串包|涉及:module/base/logs/internal/logic/log/fetch.go:61
  • P2-16 全链路 WithContext(c.Request.Context()) 并加查询超时|验收:客户端断开 5s 内数据库查询被取消|涉及:module/base/logs/internal/logic/log/fetch.go:55
  • P2-17 依赖装配改为单一路径并 fail-fastnil 依赖返回错误而非静默跳过验收DB 依赖为 nil 时启动即失败并给出明确错误|涉及:module/base/logs/service/dependencies.go:19module/base/logs/service/expose.go:13
  • P2-18 独立 main 增加 HTTP 超时、MaxHeaderBytes 与 SIGTERM 优雅退出|验收:压测慢连接不占满、kill -TERM 后请求 drain 完成再退出|涉及:module/base/logs/cmd/main/main.go:45
  • P2-19 补齐配置校验Databases/Driver/Source/Port并修正非法 GORM tag验收Databases 启动报明确错误;Service/ip/level 缺省时按默认值落库|涉及:module/base/logs/internal/config/config.go:27module/base/logs/internal/models/log_data.go:12
  • P2-20 统一改用 SDK printer 日志并补充请求上下文/级别|验收:日志带级别与请求 ID可被采集与告警涉及module/base/logs/internal/logic/log/create.go:22
  • P2-21 显式 TableName() 统一表名并补保留策略与自监控指标验收Postgres/MySQL 下表名一致;存在可观测的写入/失败/容量指标与清理任务|涉及:module/base/logs/internal/models/log_data.go:9
  • P2-22 统一时间语义UTC 存储 + 显式 TZ + 文档说明)|验收:跨时区部署下写入与读取时间一致,无 8 小时偏移|涉及:module/base/logs/internal/models/log_data.go:11module/base/logs/etc/logs_prod.yaml:7
  • P2-23 所有链式调用回写结果(tx = tx.Where(...)),并用 DryRun 单测断言过滤条件进入 SQL验收单测断言生成的 SQL 含全部条件;改造后 filter 行为不变|涉及:module/base/logs/internal/logic/log/fetch.go:36module/base/logs/internal/logic/log/total.go:24
  • P3-24 补 README路由/字段/鉴权/示例),统一服务名为 logs,修正 test/*.http|验收:按 README 与 .http 可直接联调成功|涉及:module/base/logs/README.md:1module/base/logs/etc/logs_prod.yaml:1module/base/logs/test/log.http:1
  • P3-25 删除或重写 CLI正确路由、错误处理、超时验收CLI 可成功写入且失败时返回非零退出码与错误信息|涉及:module/base/logs/cmd/cli/main.go:17
  • P3-26 统一风格:去重复中间件、删调试输出、常量声明、path.Join 拼路径、ping 用统一响应|验收:go vet/静态检查通过,响应结构一致|涉及:module/base/logs/cmd/main/main.go:36module/base/logs/internal/routers/register.go:20
  • P3-27 补模块测试IP 校验、参数解析、SQL 断言、鉴权矩阵、边界)|验收:新增用例覆盖上述场景且 CI 全绿,关键路径覆盖率达标|涉及:module/base/logs/internal/logic/log/fetch.go:18
  • P3-28 清除生产占位密钥与注释残留密钥,改为强制环境变量注入并校验非默认值|验收:仓库内无 CHANGE_ME/历史密钥;未注入时启动即失败|涉及:module/base/logs/etc/logs_prod.yaml:16module/base/logs/etc/logs_test.yaml:21

6. 审计摘要(供汇总使用)

  • 问题数P0=2 P1=13 P2=8 P3=5合计 28
  • 最高风险(一句话):写日志接口的 IP 校验逻辑反向且可被 X-Forwarded-For 伪造,模块路由又在独立部署形态下完全无鉴权,任何公网来源都能匿名写入伪造审计日志、任意登录用户能读取全量操作日志,而设计上的合法内网写入方反被拒绝——审计日志同时丧失机密性与真实性。
  • 最优先 3 个动作1) 重建鉴权边界:create 走服务间身份、fetch/total 走 JWT+管理员角色,并删除反向 IP 黑名单、收窄 trusted proxiesP0-1/P0-2/P1-142) 修掉必然故障与数据残缺:fetch[]any 扫描目标与 level 类型断言 panic、分页/排序/时间窗失效,以及与 SDK LogItem 的字段契约错配P1-3/P1-4/P1-5/P1-93) 补齐存储与写入防护索引、迁移开关、请求体与字段上限、限流与保留清理策略P1-7/P1-8/P1-10
  • 未能覆盖/无法验证的部分:
    1. 未做运行时验证(无可用 DM/Postgres/Redis 实例),问题 3 的 panic、问题 22 的时区偏移均以源码推理标注【推测】;
    2. 未运行任何测试(模块零测试文件),问题 1、4、5、6 的实际线上表现未经复现验证;
    3. 实际部署环境未知supervisor 配置未声明 TZ 与容器的时区、宿主是否对 /rest/logs/* 整体做网关级鉴权/限流、生产是否真的使用仓库内 etc/logs_prod.yamlDM 驱动不受支持,存在“配置文件与实际部署不一致”的可能);
    4. 数据库现有表结构、数据量与是否已手工建表/索引无法从代码判定,问题 7、8 的影响面需以线上 schema 为准;
    5. infra.Response 并发串包(问题 15与 SDK 生产者真实调用方式(infra.PushLog 在本仓库无调用点)未经端到端验证。