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

782 lines
54 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审计报告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:38ecmall 同)
- /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 <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`
- **证据**
```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` 恒为 falsePostgres 分支根本不迁移)
- **位置**`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-91MySQL 路径无 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 未继承 cloneclone==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_<mode>.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/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)` 断言 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_<mode>.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-fastnil 依赖返回错误而非静默跳过验收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 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.yaml`DM 驱动不受支持,存在“配置文件与实际部署不一致”的可能);
4. 数据库现有表结构、数据量与是否已手工建表/索引无法从代码判定,问题 7、8 的影响面需以线上 schema 为准;
5. `infra.Response` 并发串包(问题 15与 SDK 生产者真实调用方式(`infra.PushLog` 在本仓库无调用点)未经端到端验证。