fix(audit): inferAuditFields 下标越界导致进程级 panic(4 段路径全部 500)

=== 现象(线上实际日志)===
    http: panic serving 172.19.16.138:35256: runtime error: index out of range [4] with length 4
    lokistack/middleware.inferAuditFields(...)
    lokistack/middleware/audit.go:179

=== 根因 ===
    segments := strings.Split(strings.Trim(path, "/"), "/")
    if len(segments) >= 4 {
        resource = segments[3]
        resourceID = segments[4]   // ← 4 段时越界
    }
/api/v1/hosts/2 正好拆成 4 段 [api v1 hosts 2],取 segments[4] 直接 panic。
影响面比想象大:**所有 4 段路径的写请求**(PUT/DELETE /hosts/:id、
/storage/:id、/clusters/:id 等)都 500,且中间件 panic 后审计也写不进去
—— 等于审计功能在最常用的更新/删除接口上完全失效。

=== 修法 ===
每个下标单独判断长度:
    if len(segments) >= 3 { module = segments[2] }
    if len(segments) >= 4 { resource = segments[3] }
    if len(segments) >= 5 { resourceID = segments[4] }

=== 回归测试(middleware/audit_test.go)===
  TestInferAuditFieldsNoPanic        : 0/1/2/3/4/5 段的 12 种路径 × 3 种方法,
                                       全部不得 panic(原 bug 的 4 段路径在列)
  TestInferAuditFieldsFourSegments   : 4 段时 module/resource 正确、resourceID 为空
  TestInferAuditFieldsFiveSegments   : 5 段时能解出 resourceID
  TestInferAuditFieldsSemanticActions: deploy/stop/install/uninstall/reset-password/
                                       validate/test 语义动作不被位置解析覆盖
  TestShouldAudit                    : 只读接口跳过、部署安装类必须记

=== 顺带修:CLI 模式下的两处误报 ===
1) backup 的 manifest.recordCount 恒为 0
   原因:CLI(CronJob 里跑 lokistack backup)没 init DB,GetDB() 返回 nil。
   改为 countRecords() 直接从备份出的字节数集合。
2) self-check 报「数据库: 未初始化」
   同上原因,误报会让巡检脚本以为库坏了。
   改为 CLI 模式直接解析数据文件统计(fileDBStats)。

=== 实测(修复后)===
  修复前:POST /api/v1/hosts/2/validate → panic,无审计记录
  修复后:POST /api/v1/hosts/2/validate → 200
          audit-logs total=5,含
            [hosts/validate] admin POST /api/v1/hosts/2/validate -> 200
            [auth/login]                  POST /api/v1/auth/login     -> 200
          panic serving 计数 = 0
  用户数据完好(initial-admin-password.txt 不存在,说明不是首次部署)

镜像:lghost/lokistack:latest + v0.1 = 2493a288758d
This commit is contained in:
opsx committed 2026-10-28 21:40:24 +00:00
1 parent c44ffad61f
commit b2c82da377
4 files changed
+179 -10

No files matched your search

+41 -1
View File
@@ -1,6 +1,7 @@
package services
import (
"encoding/json"
"fmt"
"net/http"
"os"
@@ -50,8 +51,25 @@ func SelfCheck() SelfCheckResult {
db := repository.GetDB()
// 1) 数据库
//
// 注意:CLI 模式(lokistack self-check,CronJob/运维脚本用)不会 init DB,
// repository.GetDB() 返回 nil。此时不能报"未初始化"——那是误报,
// 改为直接解析数据文件统计,让 CLI 与 API 结论一致。
if db == nil {
add(CheckItem{Name: "数据库", Status: "error", Message: "未初始化"})
dbPath := databasePath()
fi, err := os.Stat(dbPath)
if err != nil {
add(CheckItem{Name: "数据库", Status: "error", Message: "数据文件读不到", Detail: err.Error()})
} else {
stats := fileDBStats(dbPath)
status := "ok"
if fi.Size() > 16<<20 {
status = "warn"
}
add(CheckItem{Name: "数据库", Status: status,
Message: fmt.Sprintf("%.1f KB(CLI 模式读取)", float64(fi.Size())/1024),
Detail: stats})
}
} else {
dbPath := databasePath()
fi, err := os.Stat(dbPath)
@@ -223,6 +241,28 @@ func probeDeploymentReady(d *models.LokiDeployment) (bool, error) {
return resp.StatusCode == 200 && strings.Contains(strings.ToLower(string(buf[:n])), "ready"), nil
}
// fileDBStats 直接解析数据文件给出各集合记录数(CLI 模式用)。
func fileDBStats(path string) string {
raw, err := os.ReadFile(path)
if err != nil {
return ""
}
var m map[string]json.RawMessage
if err := json.Unmarshal(raw, &m); err != nil {
return "数据文件不是合法 JSON"
}
count := func(key string) int {
var arr []json.RawMessage
if v, ok := m[key]; ok && json.Unmarshal(v, &arr) == nil {
return len(arr)
}
return 0
}
return fmt.Sprintf("集群 %d / 主机 %d / 存储 %d / 部署 %d / 采集器 %d / 审计 %d",
count("clusters"), count("hosts"), count("storages"),
count("deployments"), count("collectors"), count("auditLogs"))
}
// diskUsage 返回剩余与总空间(字节)
func diskUsage(path string) (free, total uint64, err error) {
var st syscall.Statfs_t