Files
ARTEX-chixuweihu/server/finding_case_reviews.go
RuoJi6 54f43fcd3f fix(findings): address Grok review r2 server batch (Codex gpt-6.1-sol)
Authored by Codex CLI (gpt-6.1-sol, reasoning high, first attempt). Historical
review task message follows the new suggest rule and treats the unified report
as best-effort; v3 tool refresh no longer re-adds a removed reporter binding.
2026-10-24 23:17:26 +08:00

248 lines
8.6 KiB
Go

package server
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/Autumn-27/artex/intercept"
actool "github.com/Autumn-27/norma/tool"
)
type findingReviewDecision struct {
FindingID int64 `json:"finding_id"`
Verdict string `json:"verdict"`
Reason string `json:"reason"`
RootCauseEvidence string `json:"root_cause_evidence"`
}
type findingReviewConclusion struct {
Summary string `json:"summary"`
Decisions []findingReviewDecision `json:"decisions"`
}
func (s *Server) isHistoricalFindingReview(conv int64) (bool, error) {
var exists bool
err := s.m.pg.QueryRow(`SELECT EXISTS(SELECT 1 FROM finding_case_review_runs WHERE conversation_id=$1)`, conv).Scan(&exists)
return exists, err
}
func (s *Server) toolCompleteFindingCaseReview() actool.CoreTool {
decision := objSchema(map[string]any{
"finding_id": idParam("所选独立漏洞记录 ID"),
"verdict": map[string]any{"type": "string", "enum": []string{"independent", "merged", "suggested"}},
"reason": strParam("判断理由,引用已证据化的共同点或不同代码路径;仅类型、标题或资产相同不足以证明共同根因"),
"root_cause_evidence": strParam("原始报告/证据中支持实际共同或不同根因的具体材料;suggested 须说明具体共同点、缺失的关键证据及已保存分组;没有具体共同点应判 independent")}, "finding_id", "verdict", "reason", "root_cause_evidence")
return wrTool("complete_finding_case_review", "历史整理结束前提交每条所选记录的根因判断并验收:必须已读取全部记录,疑似必须已有待确认分组建议;归并只需已在活动文件夹中,统一报告可稍后补齐。验收失败不能宣布完成。", objSchema(map[string]any{"summary": strParam("整理结果总结"), "decisions": map[string]any{"type": "array", "items": decision}}, "summary", "decisions"), func(ctx context.Context, raw json.RawMessage) (actool.Result, error) {
var a findingReviewConclusion
if err := json.Unmarshal(raw, &a); err != nil {
return actool.Errorf(err.Error()), nil
}
conv := intercept.ConvIDFromContext(ctx)
if err := s.saveFindingReviewConclusion(ctx, conv, a); err != nil {
return actool.Errorf(err.Error()), nil
}
return actool.Text("历史整理验收通过;原始记录和报告保留,逐条根因判断已保存。"), nil
})
}
func (s *Server) saveFindingReviewConclusion(ctx context.Context, conv int64, a findingReviewConclusion) error {
if conv <= 0 {
return errors.New("仅用于有上下文的历史整理会话")
}
return s.m.pg.WithEvidenceTx(ctx, func(tx *sql.Tx) error {
var idsRaw, readsRaw json.RawMessage
var state string
if err := tx.QueryRow(`SELECT finding_ids,reviewed_ids,state FROM finding_case_review_runs WHERE conversation_id=$1 FOR UPDATE`, conv).Scan(&idsRaw, &readsRaw, &state); err != nil {
return err
}
if state != "running" {
return errors.New("整理会话未运行,不能提交验收")
}
var ids, reads []int64
if err := json.Unmarshal(idsRaw, &ids); err != nil {
return err
}
if err := json.Unmarshal(readsRaw, &reads); err != nil {
return err
}
read := map[int64]bool{}
for _, id := range reads {
read[id] = true
}
decisions := map[int64]findingReviewDecision{}
for _, d := range a.Decisions {
if _, ok := decisions[d.FindingID]; ok {
return errors.New("重复的判断编号")
}
decisions[d.FindingID] = d
}
if strings.TrimSpace(a.Summary) == "" || len(ids) != len(decisions) {
return errors.New("须提供总结和覆盖全部所选记录的逐条判断")
}
for _, id := range ids {
d, ok := decisions[id]
if !ok || !read[id] {
return fmt.Errorf("请先 get_finding_record 读取并判断所选记录 #%d", id)
}
if strings.TrimSpace(d.Reason) == "" || strings.TrimSpace(d.RootCauseEvidence) == "" {
return fmt.Errorf("记录 #%d 缺少判断理由或根因证据", id)
}
var cid sql.NullInt64
if err := tx.QueryRow(`SELECT m.case_id FROM findings f LEFT JOIN finding_case_members m ON m.finding_id=f.id WHERE f.id=$1`, id).Scan(&cid); err != nil {
return err
}
switch d.Verdict {
case "merged":
if !cid.Valid {
return fmt.Errorf("记录 #%d 尚未归并", id)
}
case "suggested":
var pending bool
if err := tx.QueryRow(`SELECT EXISTS(SELECT 1 FROM finding_case_suggestions s
JOIN finding_case_suggestion_members sm ON sm.suggestion_id=s.id
WHERE s.state='pending' AND sm.finding_id=$1)`, id).Scan(&pending); err != nil {
return err
}
if !pending {
return fmt.Errorf("记录 #%d 须先保存待确认建议", id)
}
case "independent":
if cid.Valid {
return fmt.Errorf("记录 #%d 已在文件夹中,不能声明为独立", id)
}
default:
return errors.New("无效判断类型")
}
if cid.Valid {
var active bool
if err := tx.QueryRow(`SELECT active FROM finding_cases WHERE id=$1 FOR SHARE`, cid.Int64).Scan(&active); err != nil {
return err
}
if !active {
return fmt.Errorf("文件夹 #%d 已关闭", cid.Int64)
}
}
}
raw, err := json.Marshal(a)
if err != nil {
return err
}
_, err = tx.Exec(`UPDATE finding_case_review_runs SET conclusion=$2 WHERE conversation_id=$1`, conv, string(raw))
return err
})
}
func (s *Server) validateFindingReviewCompletion(conv int64) error {
var raw json.RawMessage
if err := s.m.pg.QueryRow(`SELECT conclusion FROM finding_case_review_runs WHERE conversation_id=$1`, conv).Scan(&raw); err != nil {
return err
}
var a findingReviewConclusion
if err := json.Unmarshal(raw, &a); err != nil {
return err
}
if len(a.Decisions) == 0 {
return errors.New("整理未通过验收:未提交 complete_finding_case_review 逐条根因判断;请查看执行记录")
}
// Recheck after the last model turn; concurrent evidence changes invalidate completion.
return s.saveFindingReviewConclusion(context.Background(), conv, a)
}
// Require substantive sections and balanced fenced blocks. This validates report
// completeness, not the truth of a model's root-cause claim or a live exploit.
func validateUnifiedFindingReport(report string) error {
aliases := [][]string{{"概述", "overview", "summary"}, {"影响", "impact"}, {"前置", "prerequisite", "precondition"}, {"复现", "reproduction", "reproduce"}, {"证据", "evidence"}, {"poc", "完整脚本", "验证脚本", "proof of concept"}, {"根因", "root cause"}, {"修复", "remediation", "recommendation", "fix"}}
names := []string{"概述", "影响", "前置条件", "复现步骤", "证据", "PoC", "根因分析", "修复建议"}
found := make([]bool, len(aliases))
current := -1
hasContent := false
fence := ""
finish := func() {
if current >= 0 && hasContent {
found[current] = true
}
}
for _, line := range strings.Split(report, "\n") {
t := strings.TrimSpace(line)
if strings.HasPrefix(t, "```") || strings.HasPrefix(t, "~~~") {
marker := t[:3]
if fence == "" {
fence = marker
} else if fence == marker {
fence = ""
}
continue
}
if fence != "" {
if t != "" {
hasContent = true
}
continue
}
if strings.HasPrefix(t, "#") {
heading := strings.TrimLeft(t, "#")
if len(heading) == len(t) || !strings.HasPrefix(heading, " ") {
continue
}
// Subheadings belong to the current section unless they start another required section.
next := -1
lower := strings.ToLower(heading)
for i, words := range aliases {
for _, word := range words {
if strings.Contains(lower, word) {
next = i
break
}
}
if next >= 0 {
break
}
}
if next >= 0 {
finish()
current = next
hasContent = false
}
continue
}
if t != "" && t != "---" && t != "***" && t != ">" {
hasContent = true
}
}
finish()
if fence != "" {
return errors.New("统一报告代码块未闭合,可能截断;请补全后再保存")
}
missing := []string{}
for i, ok := range found {
if !ok {
missing = append(missing, names[i])
}
}
if len(missing) > 0 {
return fmt.Errorf("统一报告缺少有内容的章节:%s;请补全后保存", strings.Join(missing, "、"))
}
return nil
}
func (s *Server) historicalFindingReviewSelection(ctx context.Context) ([]int64, error) {
conv := intercept.ConvIDFromContext(ctx)
if conv == 0 {
return nil, nil
}
var raw json.RawMessage
err := s.m.pg.QueryRow(`SELECT finding_ids FROM finding_case_review_runs WHERE conversation_id=$1`, conv).Scan(&raw)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
ids := []int64{}
err = json.Unmarshal(raw, &ids)
return ids, err
}