# 审计报告: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/ping`、`POST /rest/logs/create`、`POST /rest/logs/fetch`、`POST /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-16`、`internal/impl/impl.go:20-35` | | 部署形态 1(独立) | `cmd/main` 独立进程,supervisor 管理,`/data/app/pro-oplogs`,`user=root` | `cmd/main/main.go:21-49`、`etc/supervisor.pro-oplogs.conf:1-8` | | 部署形态 2(宿主) | 被 `pkgs/all`、`pkgs/ecmall` 通过 `service.Expose` 挂到宿主 `gin.Engine` | `service/expose.go:13-17`、`pkgs/all/internal/service/logs.go:9-14` | | 配置名不一致 | yaml 内 `Service: oplogs`,而 srvKey/路由/配置文件名均为 `logs` | `etc/logs_prod.yaml:1` vs `cmd/main/main.go:18`、`cmd/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.yaml`、`etc/logs_prod.yaml`、`etc/logs_test.yaml`、`etc/supervisor.pro-oplogs.conf`、`test/{log,list,cnt}.http`、`README.md`、`go.mod`、`go.sum`。 **交叉验证的外部实现(用于判定结论真伪,非审计对象)** - SDK `D:\work\bsm-sdk\core`:`with/databases.go`、`database/new.go`、`database/sql/postgresql.go`、`types/db.go`、`conf/new.go`、`infra/response.go`、`infra/logs.go`、`middleware/jwt.go`、`utils/net.go`。 - 宿主:`pkgs/all/internal/server/{server.go,authorization.go}`、`pkgs/all/internal/service/logs.go`、`pkgs/all/etc/default_dev.yaml`、`pkgs/ecmall/internal/service/{logs.go,service_test.go}`、`pkgs/ecmall/etc/default_dev.yaml`、`module/base/mgt/internal/{routers/register.go,middleware/rbac.go}`(作为「本仓库既有正确范式」对照)。 - 依赖源码:`github.com/gin-gonic/gin@v1.12.0`(`gin.go`、`binding/json.go`、`codec/json/json.go`)、`gorm.io/gorm@v1.31.2`(`gorm.go`、`scan.go`、`chainable_api.go`、`finisher_api.go`、`callbacks/create.go`、`schema/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` - **证据**: ```go // 验证请求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` 在本仓库未被收窄: ```go // 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-29`、`cmd/main/main.go:26-42`、`etc/supervisor.pro-oplogs.conf:1-8` - **证据**: ```go // 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 与健康检查,没有任何鉴权中间件: ```go 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: ```yaml # pkgs/all/etc/default_dev.yaml:38(ecmall 同) - /rest/logs/ping ``` ```go // 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/all` 与 `pkgs/ecmall` 的清单为复制关系),模块自身没有任何第二道防线。 - **建议**:`create` 与 `fetch/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-31`、`fetch.go:55` - **证据**: ```go 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 扫描分支: ```go // 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` 分支,`reflectValueType` 为 `interface{}`,于是按切片逐行 `elem = reflect.New(reflectValueType)`(`scan.go:328`)得到 `*interface{}`,再进入 `db.scanIntoStruct` → `field.Set` → `field.ReflectValueOf`: ```go // 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` 会 panic(`reflect: call of reflect.Value.Field on interface Value`)。GORM 支持的 map 查询写法是 `Find(&[]map[string]interface{}{})`,而非 `[]any`。本项未实机运行(无可用数据库),故标注【推测】,但因果链完整、可复现。 - **影响**:`POST /rest/logs/fetch` 在表中有任何数据时即 panic,被 `gin.Recovery` 兜成 500,日志列表功能等于不可用;同时 panic 会打印堆栈(含 SQL/表结构信息)。 - **建议**:改为 `var data []map[string]any`(或强类型 `[]models.LogData`)+ `Select` 显式列白名单,禁止把数据库原始列直接回吐给调用方。 #### 4. 客户端可控字段的 `interface{}` 类型断言导致 panic(`level` 分支必然 panic) - **位置**:`module/base/logs/internal/logic/log/fetch.go:35-46` - **证据**: ```go 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)) } ``` `request` 是 `map[string]any`,由 `c.BindJSON` 经 `encoding/json` 解码,而 gin 默认未开启 `UseNumber`: ```go // github.com/gin-gonic/gin@v1.12.0/binding/json.go:19 var EnableDecoderUseNumber = false ``` 因此 `{"level":1}` 解出的动态类型是 `float64`,`request["level"] != ""` 为真(不同动态类型),随后 `.(int)` 断言失败 panic;`{"level":"1"}` 同样 panic。`request["op_ip"]` 分支还取错了 key:判空用 `op_ip`,取值用 `ip`,只传 `op_ip` 时 `request["ip"]` 为 nil,`nil.(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` - **证据**: ```go 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=50`;`test/list.http:6-7` 传入的 `page/size` 被静默忽略。查询无 `ORDER BY`,无 `created_at` 范围条件;`Count` 与 `Find` 复用同一 `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` - **证据**: ```go 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 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` - **证据**: ```go 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-34`:`DeletedAt ... index;`、`Status int8 ... default:0;index;`),本模型除主键外无任何 `index`/`uniqueIndex`。而查询侧使用的是 `op_name like '%x%'`、`service like '%x%'`(前导通配符无法走索引)与无范围 `WHERE`/`GROUP BY`。 - **影响**:日志表是本系统写入最频繁的表之一,随数据增长读写双双劣化;`fetch/total` 将逐渐拖垮数据库连接池(`MaxOpenConns=64`,`internal/impl/impl.go:27`),成为全局性故障源。 - **建议**:至少建立 `index(created_at)`、`index(service, created_at)`、`index(level, created_at)`、`index(op_id)`;把 `like '%x%'` 收敛为前缀匹配或去掉 `%` 前导;为日志表规划分区/归档。 #### 8. 自动迁移形同虚设(`IsAutoMigrate` 恒为 false,Postgres 分支根本不迁移) - **位置**:`module/base/logs/internal/models/log_data.go:23-25`、`module/base/logs/internal/impl/impl.go:25-32` - **证据**: ```go // models/log_data.go func init() { database.AppendMigrate(&LogData{}) } ``` ```go // 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 路径有效: ```go // bsm-sdk/core/database/new.go:43-48 if len(MigrateTables) > 0 && options.IsAutoMigrate { err = db.AutoMigrate(MigrateTables...) ``` ```go // 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 } ``` `NewPostgreSql`(`postgresql.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-21`、`internal/logic/log/create.go:28-46`、SDK `bsm-sdk/core/infra/logs.go:9-19` - **证据**:SDK 提供的生产者模型与本服务消费者模型字段名不一致: ```go // 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"` } ``` ```go // 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` 默认不拒绝未知字段: ```go // github.com/gin-gonic/gin@v1.12.0/binding/json.go:25 var EnableDecoderDisallowUnknownFields = false ``` `create.go:28-44` 只判断数组非空,不做必填/长度/枚举校验;`Hmac`、`Encry`、`DataType` 在模块内除结构体声明外无任何读写(全模块 grep 仅命中 `models/log_data.go`)。 - **影响**:按 SDK/仓库文档调用写入接口时,「谁做了什么」的正文(`text/module/op_type/code`)被静默丢弃,落库记录只剩操作人/级别/IP —— 审计记录事实性残缺;`hmac` 字段留空意味着审计日志无任何防篡改校验,`encry` 开关是空承诺(敏感内容既未加密也未脱敏)。 - **建议**:统一生产者/消费者契约(SDK `LogItem` 与 `LogData` 二选一为准并同步双方),开启 `DisallowUnknownFields` 或在处理器内显式校验必需字段,补 `Content` 长度上限与敏感字段脱敏;要么实现 `hmac` 签名/校验与加密,要么删除这两个字段以免误导。 #### 10. 写入无配额、无限流、无请求体/字段长度上限,且无保留与清理策略 - **位置**:`module/base/logs/internal/logic/log/create.go:28-44`、`internal/models/log_data.go:18` - **证据**: ```go err := c.BindJSON(&request) // 无 MaxBytesReader、无数组长度上限 ... if len(request) == 0 { ... } if err := impl.DBService.Model(&models.LogData{}).Create(&request).Error; err != nil { ``` `Content string` 无 `size`/长度约束(Postgres 下为无限制 `text`),`Service`/`OpName` 之外的业务字段同样无上限。全仓库检索无任何限流实现: ```text 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-44`、`internal/models/log_data.go:10-17` - **证据**:模型字段直接映射 JSON,且处理器从不覆盖: ```go 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 仅在字段为零值时才填充创建时间,客户端提供值即原样入库: ```go // 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()` 覆盖 `ip`、`op_id/op_name` 由认证身份(JWT/服务身份)推导而非请求体提供;对写入内容做签名(`hmac`)以便事后校验完整性。 #### 12. 配置中明文数据库口令,且 prod 与 dev 使用同一库、同一口令 - **位置**:`module/base/logs/etc/logs_dev.yaml:4-10`、`module/base/logs/etc/logs_prod.yaml:4-10` - **证据**:两文件数据库段与缓存段完全一致(含真实口令),差异仅在注释: ```yaml # 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: dm`,SDK 不支持该驱动 → 独立部署启动即 panic - **位置**:`module/base/logs/etc/logs_dev.yaml:5`、`module/base/logs/etc/logs_prod.yaml:5`、`module/base/logs/etc/logs_test.yaml:5` - **证据**: ```yaml Driver: dm ``` ```go // 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) } ``` ```go // 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-47`、`module/base/mgt/internal/middleware/rbac.go:39-54`) - **证据**:mgt 模块给出了本仓库既有的正确范式: ```go // 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,46`、`fetch.go:23,57,61`、`total.go:18,29,33`(根因在 SDK) - **证据**: ```go // 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:40`、`fetch.go:30,55`、`total.go:22,28` - **证据**:模块内无任何 `WithContext`/`context` 引用(全模块 grep 无命中),查询形如: ```go 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-35`、`service/dependencies.go:19-28`、`service/expose.go:13-17` - **证据**: ```go 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 依赖静默跳过:宿主若因故障未初始化 DB(`pkgs/all/internal/impl/impl.go:38` 直接依赖 `with.Databases` 成功),`impl.DBService` 保持 nil,`create/fetch/total` 首行即空指针 panic。同时模块存在两条互相独立的初始化路径(`NewImpl()` 与 `applyDependencies()`),二者写入同一批全局变量且无同步。 - **影响**:依赖缺失从「启动失败并报警」退化为「运行期 500 且原因隐晦」;并发/重复初始化时对全局指针的写入存在竞态;测试与生产易装配出不同依赖组合。 - **建议**:依赖注入收敛为一次显式装配(`Expose` 内校验必需依赖非 nil 并返回错误),`DBService` 为 nil 时 fail-fast;避免包级可变全局,或至少用 `sync.Once` 保护。 #### 18. 独立部署缺 HTTP 超时与优雅退出,启动失败直接 panic - **位置**:`cmd/main/main.go:36-48` - **证据**: ```go 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`。宿主的服务器实现给出了应有基线: ```go // 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 的无体积限制);进程重启时无优雅退出,写入中的请求被硬切断且无 drain;`panic` 使退出码与错误信息不友好,supervisor 只能反复拉起。 - **建议**:显式构造 `http.Server` 并设置超时与 `MaxHeaderBytes`,监听 `SIGTERM/SIGINT` 调用 `Shutdown(ctx)`;启动错误用 `log.Fatal`/结构化错误上报替代 `panic`。 #### 19. 配置校验薄弱,且存在非法 GORM tag(`def:` / `def:1`)被静默忽略 - **位置**:`internal/config/config.go:19-27`、`internal/models/log_data.go:12,15,17` - **证据**: ```go Spec.Port = conf.CheckPort(Spec.Port) // 仅处理空串,非法值不校验 conf.NotNil(Spec.Service, Spec.Cache) // Databases / Rpc / Etcd 均未校验 ``` ```go 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.Databases` 的 `panic("No Database Source Found !")`(`bsm-sdk/core/with/databases.go:13-15`),端口填错(如 `abc`)直到 `app.Run` 才失败;默认值不生效造成数据质量下降(空的 service 会让问题 6 的统计与过滤失真)。 - **建议**:`NotNil` 覆盖 `Databases.Driver/Source`、`Etcd` 等必需项,端口做数值/范围校验;修正为 `default:'def'`/`default:1`,并对缺省字段在写入前做服务端填充与校验。 #### 20. 日志体系不统一:使用标准库 `log` 而非 SDK `printer`,无级别、无结构化、无 trace 关联 - **位置**:`internal/logic/log/create.go:5,22,30,35,41`、`fetch.go:5,22,56`、`total.go:5,17` - **证据**: ```go 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 路径使用默认复数策略: ```go // bsm-sdk/core/database/sql/postgresql.go:39-41 NamingStrategy: schema.NamingStrategy{ SingularTable: true, ... } ``` ```go // bsm-sdk/core/database/new.go:89-91(MySQL 路径无 NamingStrategy) gorm.Open(mysql.Open(dsn0), &gorm.Config{SkipDefaultTransaction: true}) ``` 模型未定义 `TableName()`,故 Postgres 下建/查 `log_data`,MySQL 下为 `log_datas`。同时模块无清理/归档任务、无自监控指标(写入量、失败率、表行数)。 - **影响**:数据库迁移或驱动切换时表名静默漂移,运维脚本/对账查询易指向空表;表无保留策略将无限增长(见问题 10);服务自身无任何健康度量化指标。 - **建议**:显式实现 `func (LogData) TableName() string { return "log_data" }` 统一表名;增加按时间的分区/归档任务与清理阈值,并暴露写入计数/失败计数指标。 #### 22. 时间语义与时区未定义,存在 8 小时偏移风险【推测】 - **位置**:`internal/models/log_data.go:11`、`etc/logs_prod.yaml:7`、`etc/logs_test.yaml:7` - **证据**:模型直接使用 `time.Time`,无 `autoCreateTime`/时区约定;DSN 中显式指定了会话时区,而 Go 侧时间取进程本地时区: ```yaml - dm://...?charset=utf8&parseTime=True&loc=Asia%2FShanghai # logs_prod.yaml:7 - host=127.0.0.1 ... TimeZone=Asia/Shanghai # logs_test.yaml:7 ``` ```go 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. 链式调用丢弃返回值(当前恰好生效,写法脆弱)+ `total` 与 `data` 一致性未校验 - **位置**:`internal/logic/log/fetch.go:36,39,42,45`、`internal/logic/log/total.go:24` - **证据**: ```go tx.Where("op_name like ?", "%"+request["op_name"].(string)+"%") // 未回写 tx ``` 在 gorm v1.31.2 中,`Model()` 返回的实例 `clone==0`,`getInstance()` 对 `clone==0` 直接返回自身,因此上述条件**恰好**会生效: ```go // 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 未继承 clone,clone==0 return tx } return db } ``` 但这依赖 gorm 内部 `clone` 语义:对 `Session` 派生(`clone==2`)或未来版本/`Session(&Session{NewDB:...})` 的实例,同一写法会静默丢条件且不报错(这正是该模式广受诟病的根因)。此外 `total` 与 `data` 之间无任何一致性校验(`count` 是全表计数、`data` 是 50 条,两者不属于同一过滤语境)。 - **影响**:一旦依赖升级或改用 Session(如为修问题 16 加 `WithContext` 后返回新实例),过滤条件会**静默失效**——查询悄悄退化为全表读取而无人察觉(安全问题 5 的越权读取会重新出现)。属高危可维护性缺陷。 - **建议**:统一改为 `tx = tx.Where(...)` 或每步重新赋值的写法,并用 `Session(&gorm.Session{})` 明确快照语义;补一条单测断言生成的 SQL 含所有过滤条件(可用 `DryRun: true`)。 ### P3 #### 24. 文档与真实行为不一致:README 空、服务名混乱、`test/*.http` 全部失效 - **位置**:`README.md:1-2`、`etc/logs_prod.yaml:1`、`cmd/main/main.go:18,35`、`test/log.http:1`、`test/list.http:1`、`test/cnt.http:1` - **证据**:README 仅标题;配置中 `Service: oplogs`,而 srvKey/路由/配置文件名均为 `logs`: ```yaml Service: oplogs ``` ```go ServiceKey = "logs" // cmd/main/main.go:18 cfp := fmt.Sprintf("%s_%s.yaml", strings.ToLower(srvKey), ...) // SDK 按 logs_.yaml 读取 ``` 测试脚本指向不存在的路由与字段: ```text 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` - **证据**: ```go 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,36`、`internal/routers/register.go:13,20`、`internal/logic/log/fetch.go:13-16`、`internal/logic/hello/ping.go:9` - **证据**: ```go app := gin.Default() // 已内置 Logger + Recovery app.Use(gin.Recovery()) // 重复挂载 ``` ```go v1_key := fmt.Sprintf("/rest/%s", srvKey) // mgt 使用 path.Join fmt.Println(v1_key) // 调试输出残留 ``` ```go var ( DefaultPage = 1; DefaultSize = 50 ) // 应为 const ``` ```go ctx.JSON(200, gin.H{"message": "Pong"}) // 未使用 infra.Response 统一响应 ``` - **影响**:日志重复、响应体不统一(客户端需两套解析逻辑)、常量可变导致被意外改写;`gin.DebugMode` 在非 prod 下会输出路由与调试信息(`cmd/main/main.go:32`)。 - **建议**:`gin.New()` + 显式中间件;路径用 `path.Join`;删除调试输出;常量改 `const`;`ping` 走统一响应结构。 #### 27. 模块零测试 - **位置**:`module/base/logs/`(无 `*_test.go`) - **证据**:模块内 12 个 Go 文件全部为业务代码,无任何测试文件(文件清单见第 1 节;对比 `pkgs/all/internal/server/authorization_test.go`、`pkgs/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-21`、`etc/logs_test.yaml:16,18-21`、`etc/logs_dev.yaml:16` - **证据**: ```yaml 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-70` 对 `Authorization.Key` 做长度校验并 panic)。 ## 4. 推荐优化方案 **阶段一:止血(对应 P0/P1,1-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)` 断言 panic;`page/size` 解析并封顶;`total` 改用 `Group + Find` 与别名。 3. **写入防护**:`MaxBytesReader` + 单请求条数/字段长度上限;`created_at` 服务端生成;补齐必填校验与 `Content` 脱敏;与 SDK `LogItem` 字段契约对齐(否则内容继续丢失)。 4. **启动可用性**:修正 `Driver` 与驱动实现的一致性(或补达梦驱动),`SqlOptions` 打开 `IsAutoMigrate`,`config.New` 校验 `Databases.Port`;`gofmt`/`go vet` 已通过,可立即把 `go vet` + 启动冒烟纳入 CI。 **阶段二:可持续(1-2 周)** 5. **存储与查询**:为 `log_data` 建 `created_at`/`(service,created_at)`/`(level,created_at)`/`op_id` 索引;`fetch/total` 强制时间窗与 `created_at DESC` 排序;查询与计数分离。 6. **写入路径改造**:内存队列 + 批量 `CreateInBatches`(批量大小可配)与背压,替代逐请求直写;失败重试与丢弃计数指标;按调用方配额限流。 7. **保留与生命周期**:按月分区 + 定期归档/删除(如 90 天),把清理任务与本服务解耦(独立 Job);对外暴露写入量、失败率、表行数、清理结果指标。 8. **健壮性**:全链路 `WithContext(c.Request.Context())` + 查询超时;`Expose` 对依赖非 nil 与 `Engine` 非 nil 做 fail-fast;独立 main 补 `http.Server` 超时与优雅退出;改用 `printer` 统一日志并带请求 ID。 **阶段三:长期(随迭代)** 9. **契约与完整性**:为写入引入 `hmac` 签名与校验(或删除该字段),敏感字段加密/脱敏策略落地;SDK 与本服务的模型契约由一份来源生成/对齐,并加契约测试。 10. **可观测与治理**:接入 APM(当前 `etc/logs_prod.yaml:24-27` 的 APM 段是注释状态);审计日志的读取行为本身写入独立访问日志;文档与 `test/*.http` 同步更新(问题 24、25)。 11. **测试**:见问题 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:20`、`module/base/logs/cmd/main/main.go:26` - [ ] **P0-2** `create` 与 `fetch/total` 分组鉴权,移除匿名声明|验收:未带合法服务身份/管理令牌调用 `create`、`fetch`、`total` 均被拒(401/403),`ping` 保持匿名|涉及:`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:29`、`module/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:35`、`module/base/logs/internal/logic/log/fetch.go:42`、`module/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:33`、`module/base/logs/internal/logic/log/fetch.go:55` - [ ] **P1-6** 重写 `total`:`Group("level")` + `Find` + 列别名,空结果返回空集合|验收:多级别数据返回多行;空表返回空数组且 code=0|涉及:`module/base/logs/internal/logic/log/total.go:28` - [ ] **P1-7** 为 `log_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:25`、`module/base/logs/internal/models/log_data.go:24` - [ ] **P1-9** 统一 SDK `LogItem` 与 `LogData` 契合并补字段校验/长度上限/必要脱敏;决定 `hmac`/`encry` 去留|验收:按 SDK 写入后 `content/service/data_type` 有值,超长与缺字段请求被拒|涉及:`module/base/logs/internal/models/log_data.go:12`、`module/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:28`、`module/base/logs/internal/models/log_data.go:10` - [ ] **P1-12** 口令与地址改为环境变量注入,生产使用独立低权限账号并轮换已泄露口令|验收:仓库内无明文口令,`grep` 不到 `Yd@2aMwVAcJj4dA`,prod/dev 指向不同库|涉及:`module/base/logs/etc/logs_prod.yaml:7`、`module/base/logs/etc/logs_dev.yaml:7` - [ ] **P1-13** 解决 `Driver: dm` 与 SDK 驱动能力不一致(补驱动或改驱动),并加驱动白名单校验|验收:以 `etc/logs_.yaml` 独立启动成功,非法 driver 在配置校验阶段即报错|涉及:`module/base/logs/etc/logs_prod.yaml:5`、`module/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-fast(nil 依赖返回错误而非静默跳过)|验收:DB 依赖为 nil 时启动即失败并给出明确错误|涉及:`module/base/logs/service/dependencies.go:19`、`module/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:27`、`module/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:11`、`module/base/logs/etc/logs_prod.yaml:7` - [ ] **P2-23** 所有链式调用回写结果(`tx = tx.Where(...)`),并用 DryRun 单测断言过滤条件进入 SQL|验收:单测断言生成的 SQL 含全部条件;改造后 filter 行为不变|涉及:`module/base/logs/internal/logic/log/fetch.go:36`、`module/base/logs/internal/logic/log/total.go:24` - [ ] **P3-24** 补 README(路由/字段/鉴权/示例),统一服务名为 `logs`,修正 `test/*.http`|验收:按 README 与 .http 可直接联调成功|涉及:`module/base/logs/README.md:1`、`module/base/logs/etc/logs_prod.yaml:1`、`module/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:36`、`module/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:16`、`module/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 proxies(P0-1/P0-2/P1-14);2) 修掉必然故障与数据残缺:`fetch` 的 `[]any` 扫描目标与 `level` 类型断言 panic、分页/排序/时间窗失效,以及与 SDK `LogItem` 的字段契约错配(P1-3/P1-4/P1-5/P1-9);3) 补齐存储与写入防护:索引、迁移开关、请求体与字段上限、限流与保留清理策略(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.yaml`(DM 驱动不受支持,存在“配置文件与实际部署不一致”的可能); 4. 数据库现有表结构、数据量与是否已手工建表/索引无法从代码判定,问题 7、8 的影响面需以线上 schema 为准; 5. `infra.Response` 并发串包(问题 15)与 SDK 生产者真实调用方式(`infra.PushLog` 在本仓库无调用点)未经端到端验证。