Files
sundynix-agentix/sundynix-gateway/internal/store/audit.go
T
Blizzard 84463394d4 fix(admin): 审计筛选下沉 SQL + 工具数不再写死 + 模型删除加确认
清单里那批小毛病,逐条复核后修(「审计详情列缺失」那条已不成立,早补上了)。

1. 审计筛选只在当前页生效 —— 影响最大的一条。分页是服务端的,筛选却在
   前端对已取回的 50 条做,于是搜一个用户 ID 显示"无结果"时,后面几页
   可能还有几百条。审计的用途就是查证,"搜不到"会被读成"没发生过"。
   改为 action/path/q 三个条件全部落到 SQL,前端只管发条件(防抖 300ms)。

2. 服务状态页把 mcp-go/mcp-py 的工具数写死成 23/4 —— 增删工具后一直骗人,
   且服务离线时照样显示,看不出工具其实一个都没注册上。改取实际上报值。

3. 模型删除一点即删,无任何确认。补二次确认,并对"正在使用中"的模型
   单独说明后果(删掉会立刻打断线上对话/向量能力)。

审计筛选补了 4 组回归测试,两条是踩出来的坑:
  - q 的 OR 组必须带括号:gorm 以 AND 拼接各 Where,裸 OR 会让 action
    条件被绕过(测试里用"同 IP 不同方法"两行钉死这个语义);
  - LIKE 必须显式写 ESCAPE '\':Postgres 默认拿反斜杠当转义符,SQLite
    不写就没有转义符——原来的写法在单测里静默失效,搜 "100%" 命中 0 条。
    顺带把 ILIKE 换成 LOWER()+LIKE,这段才能被内存库覆盖。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:51:59 +08:00

89 lines
3.3 KiB
Go
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.
package store
import (
"context"
"strings"
)
// AppendAudit 追加一条审计留痕(best-effort:审计失败不应影响主流程,调用方忽略返回)。
func (p *Postgres) AppendAudit(ctx context.Context, a *AuditLog) error {
if p.db == nil || a == nil {
return errStoreDisabled
}
return p.db.WithContext(ctx).Create(a).Error
}
// AuditFilter 是审计流的服务端筛选条件。筛选必须落到 SQL:审计的用途是查证,
// 若只在“当前页”里筛,搜不到就等于给出“没有这条记录”的错误结论。
type AuditFilter struct {
Action string // HTTP 方法,精确匹配
Path string // 路径前缀
Q string // 跨 actor / ip / detail / path 的模糊匹配
}
// escapeLike 转义 LIKE 的通配符,让用户输入的 % 和 _ 按字面量匹配
// (否则搜 "100%" 会退化成匹配任意串)。
// 配套的 SQL 必须显式写 ESCAPE '\\'Postgres 默认就拿反斜杠当转义符,但 SQLite 不写
// ESCAPE 就压根没有转义符——依赖隐式默认会在换库/单测时静默失效。
func escapeLike(s string) string {
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(s)
}
// ListAudit 倒序列出审计留痕(管理端审计流;limit 限流、offset 翻页,筛选走 SQL)。
func (p *Postgres) ListAudit(ctx context.Context, limit, offset int, f AuditFilter) ([]AuditLog, error) {
if p.db == nil {
return nil, errStoreDisabled
}
limit, offset = clampPage(limit, offset)
q := p.db.WithContext(ctx).Model(&AuditLog{})
if f.Action != "" {
q = q.Where("action = ?", f.Action)
}
if f.Path != "" {
q = q.Where(`path LIKE ? ESCAPE '\'`, escapeLike(f.Path)+"%")
}
if f.Q != "" {
like := "%" + strings.ToLower(escapeLike(f.Q)) + "%"
// 用 LOWER()+LIKE 而非 Postgres 专有的 ILIKE:语义一样,但 SQLite 也支持,
// 于是这段能被内存库单测覆盖(审计搜索量小,放弃索引可忽略)。
// 括号不能省:gorm 把每个 Where 以 AND 拼接,裸的 OR 串会让优先级变成
// `action = ? AND actor LIKE ? OR ip LIKE ? ...`,前面的条件直接失效。
q = q.Where(`(LOWER(actor) LIKE ? ESCAPE '\' OR LOWER(ip) LIKE ? ESCAPE '\' `+
`OR LOWER(detail) LIKE ? ESCAPE '\' OR LOWER(path) LIKE ? ESCAPE '\')`,
like, like, like, like)
}
var out []AuditLog
err := q.Order("created_at desc").Limit(limit).Offset(offset).Find(&out).Error
return out, err
}
// AppendGuardrailEvent 追加一条护栏命中(best-effort)。
func (p *Postgres) AppendGuardrailEvent(ctx context.Context, e *GuardrailEvent) error {
if p.db == nil || e == nil {
return errStoreDisabled
}
return p.db.WithContext(ctx).Create(e).Error
}
// ListGuardrailEvents 倒序列出护栏事件(管理端安全事件流)。
func (p *Postgres) ListGuardrailEvents(ctx context.Context, limit, offset int) ([]GuardrailEvent, error) {
if p.db == nil {
return nil, errStoreDisabled
}
limit, offset = clampPage(limit, offset)
var out []GuardrailEvent
err := p.db.WithContext(ctx).Order("created_at desc").Limit(limit).Offset(offset).Find(&out).Error
return out, err
}
// clampPage 归一分页参数:limit ∈ [1,200](默认 50),offset ≥ 0。
func clampPage(limit, offset int) (int, int) {
if limit <= 0 || limit > 200 {
limit = 50
}
if offset < 0 {
offset = 0
}
return limit, offset
}