mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
251 lines
8.6 KiB
Go
251 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("原始报告/证据中支持实际共同或不同根因的具体材料;不确定则说明缺失证据及已保存建议")}, "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 WHERE state='pending' AND (left_id=$1 OR right_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 report string
|
|
var version, rv int64
|
|
var active bool
|
|
if err := tx.QueryRow(`SELECT report,version,report_version,active FROM finding_cases WHERE id=$1 FOR SHARE`, cid.Int64).Scan(&report, &version, &rv, &active); err != nil {
|
|
return err
|
|
}
|
|
if !active || rv != version {
|
|
return fmt.Errorf("文件夹 #%d 的统一报告待更新,请重新读取后更新", cid.Int64)
|
|
}
|
|
if err := validateUnifiedFindingReport(report); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|