fix: 修复合并后审查发现的 6 个中/高问题

#102 高:证据正文下载在全局 advisory lock 事务内 io.Copy 给 HTTP 客户端,
verifyFile 又要整读一遍算 SHA-256 —— 一次大文件下载或慢客户端就会挂住
全进程的证据写入。改为事务内只解析绑定元数据(Store.Binding),出锁后再校验
与串流;blob 内容寻址且 GC 有 24h 宽限,已打开的 fd 也不受 unlink 影响。

#102 高:traffic_evidence_snapshots 的 url/req_head/resp_head 直接来自目标
原始报文,未清洗就写 PG TEXT。目标返回非 UTF-8 头(GBK filename)或 NUL 会让
RecordFindingTx 整个事务回滚,已确认的漏洞完全不落库。新增
TrafficEvidenceSnapshot.Normalize(),在算 ID 之前套用仓库既有的 utf8Clean,
保证哈希与入库字节一致。

#102 中:自动绑定默认关闭,reporter 便不会传 evidence_version,
SetFindingReportVersionByNodeID 按 legacy 语义写 -1,报告永久显示「待更新」
且 UI 无处清除;老库还因 reporter_agent_seed_v1 守卫拿不到新触发文案。
触发消息改为无条件先读 get_finding_traffic(只读工具,不依赖捕获开关)并回传
版本,并加一次性迁移刷新老库中未被用户改动的默认文案。

#102 中:开关关闭时 report_finding 只要收到 traffic_refs/evidence_hint_id
就在写入前报错,模型幻觉出多余字段即丢掉一个真实漏洞。改为忽略这两个参数
照常登记,由既有的 evidence_status=not_bound 说明未绑定。

#101 中:删掉 RuleID!=0 判断后,每次模型放行都写一行带完整审计快照的
intercept_pending(单行最坏 24×8KiB 上下文 + 32KiB 输入)。开启兜底裁判的
渗透任务会灌入数百 MB,并经 task_archives 的 SELECT * 进入归档。放行行改为
只留决策元数据与执行结果,丢掉重放快照;ask/deny 路径不变。

#103 中:FindingRetestForConversation 出错时在收尾 defer 注册前就 return,
复测行永远停在 pending —— 漏洞列表常驻「复测中」,后续发起复测被去重拦下,
只有重启才能清。新增 FailPendingRetestForConversation 按会话 ID 收尾。
This commit is contained in:
Autumn-27 committed 2026-09-10 10:15:24 -04:00
1 parent e8efce605c
commit 72a52e9202
10 files changed
+197 -55

No files matched your search

+12
View File
@@ -157,6 +157,18 @@ func (d *DB) FindingRetestForConversation(ctx context.Context, conversationID in
return r, err
}
// FailPendingRetestForConversation seals a conversation's unfinished retest when
// the runner could not even load it — the retest ID is unknown on that path, so
// the conversation ID is the only handle. Without it a transient read error
// leaves the row 'pending' forever: the findings list keeps showing 复测中 and
// every later 发起复测 is deduped against a run that is not happening, with only
// a process restart (RecoverFindingRetests) able to clear it.
func (d *DB) FailPendingRetestForConversation(conversationID int64, reason string) error {
_, err := d.Exec(`UPDATE finding_retests SET status='failed', error=$2, finished_at=now()
WHERE conversation_id=$1 AND status IN ('pending','running')`, conversationID, reason)
return err
}
func (d *DB) StartFindingRetest(ctx context.Context, id int64) (bool, error) {
res, err := d.ExecContext(ctx, `UPDATE finding_retests SET status='running', started_at=now() WHERE id=$1 AND status='pending'`, id)
if err != nil {
+21
View File
@@ -109,7 +109,24 @@ type PreparedTrafficEvidence struct {
Snapshot TrafficEvidenceSnapshot
}
// Normalize makes the snapshot's text columns safe for PostgreSQL. URL and the
// head blocks come straight off the wire, so a target answering with a non-UTF-8
// header (a GBK `Content-Disposition: filename=…`, a NUL byte) would otherwise
// abort the INSERT and roll back the whole finding — losing a confirmed finding
// over a malformed response header. Applied before hashing so the ID always
// matches the bytes that actually land in the table.
func (s TrafficEvidenceSnapshot) Normalize() TrafficEvidenceSnapshot {
s.SourceTrafficID = utf8Clean(s.SourceTrafficID)
s.URL = utf8Clean(s.URL)
s.Method = utf8Clean(s.Method)
s.ContentType = utf8Clean(s.ContentType)
s.ReqHead = utf8Clean(s.ReqHead)
s.RespHead = utf8Clean(s.RespHead)
return s
}
func TrafficSnapshotID(snapshot TrafficEvidenceSnapshot) string {
snapshot = snapshot.Normalize()
snapshot.ID = ""
snapshot.CreatedAt = time.Time{}
raw, _ := json.Marshal(snapshot)
@@ -169,6 +186,10 @@ func InsertEvidenceSnapshotTx(tx *sql.Tx, s TrafficEvidenceSnapshot) error {
if s.ID != TrafficSnapshotID(s) {
return errors.New("证据快照元数据哈希不匹配")
}
// The ID was computed over the normalized form; store those same bytes.
id := s.ID
s = s.Normalize()
s.ID = id
if s.CreatedAt.IsZero() {
s.CreatedAt = time.Now().UTC()
}