mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-31 18:23:50 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f2c44be91 | ||
|
|
57424249ce | ||
|
|
f48f4ec8e2 | ||
|
|
aac79d72e9 | ||
|
|
a55189a649 | ||
|
|
9745e7d019 | ||
|
|
af2f9e3560 |
No files matched your search
+58
-18
@@ -23,20 +23,21 @@ type DBFinding struct {
|
||||
ReportEvidenceVersion int64
|
||||
TrafficBindings []FindingTrafficBinding // populated only for export
|
||||
|
||||
ID int64
|
||||
TaskID *int64
|
||||
NodeID *int64
|
||||
VulnClass string
|
||||
Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass
|
||||
Severity string
|
||||
Summary string
|
||||
Evidence string
|
||||
Worker string
|
||||
AssetIDs []int64
|
||||
Status string
|
||||
Report string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带
|
||||
CreatedAt time.Time
|
||||
TaskDescription string // populated via LEFT JOIN on tasks
|
||||
ID int64
|
||||
TaskID *int64
|
||||
NodeID *int64
|
||||
VulnClass string
|
||||
Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass
|
||||
Severity string
|
||||
Summary string
|
||||
Evidence string
|
||||
Worker string
|
||||
AssetIDs []int64
|
||||
Status string
|
||||
Report string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带
|
||||
ReportConversationID *int64 `json:"report_conversation_id,omitempty"`
|
||||
CreatedAt time.Time
|
||||
TaskDescription string // populated via LEFT JOIN on tasks
|
||||
}
|
||||
|
||||
// Finding triage states (findings.status).
|
||||
@@ -106,9 +107,9 @@ func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, name, severity, summary
|
||||
// findingSelectCols is the column list (with task_description join) every finding
|
||||
// list query selects, so scanFinding stays in sync across callers.
|
||||
const findingSelectCols = `f.id, f.task_id, f.node_id, f.vulnclass, COALESCE(f.name, ''), f.severity, f.summary,
|
||||
f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at,
|
||||
f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at,
|
||||
COALESCE(t.description, '') AS task_description, f.evidence_version, f.report_evidence_version,
|
||||
(SELECT count(*) FROM finding_traffic_bindings b WHERE b.finding_id=f.id)`
|
||||
(SELECT count(*) FROM finding_traffic_bindings b WHERE b.finding_id=f.id)`
|
||||
|
||||
// scanFindings materializes rows selected via findingSelectCols.
|
||||
func scanFindings(rows interface {
|
||||
@@ -661,13 +662,14 @@ func (d *DB) FindingStats() (*FindingStats, error) {
|
||||
func (d *DB) GetFinding(id int64) (*DBFinding, error) {
|
||||
f := &DBFinding{}
|
||||
var aidsJSON string
|
||||
err := d.QueryRow(`SELECT `+findingSelectCols+`, COALESCE(f.report, '')
|
||||
var reportConv sql.NullInt64
|
||||
err := d.QueryRow(`SELECT `+findingSelectCols+`, COALESCE(f.report, ''), f.report_conversation_id
|
||||
FROM findings f
|
||||
LEFT JOIN tasks t ON f.task_id = t.id
|
||||
WHERE f.id = $1`, id).Scan(
|
||||
&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Name, &f.Severity,
|
||||
&f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt,
|
||||
&f.TaskDescription, &f.EvidenceVersion, &f.ReportEvidenceVersion, &f.TrafficCount, &f.Report)
|
||||
&f.TaskDescription, &f.EvidenceVersion, &f.ReportEvidenceVersion, &f.TrafficCount, &f.Report, &reportConv)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -675,9 +677,47 @@ func (d *DB) GetFinding(id int64) (*DBFinding, error) {
|
||||
return nil, err
|
||||
}
|
||||
_ = json.Unmarshal([]byte(aidsJSON), &f.AssetIDs)
|
||||
if reportConv.Valid {
|
||||
f.ReportConversationID = &reportConv.Int64
|
||||
}
|
||||
if f.ReportConversationID == nil && strings.TrimSpace(f.Report) != "" {
|
||||
if resolved, resolveErr := d.ResolveFindingReportConversation(f.ID); resolveErr == nil {
|
||||
f.ReportConversationID = resolved
|
||||
}
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (d *DB) SetFindingReportConversationID(findingID, conversationID int64) error {
|
||||
_, err := d.Exec(`UPDATE findings SET report_conversation_id=$2 WHERE id=$1`, findingID, conversationID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ResolveFindingReportConversation backfills links for reports generated before
|
||||
// report_conversation_id was persisted.
|
||||
func (d *DB) ResolveFindingReportConversation(findingID int64) (*int64, error) {
|
||||
var id int64
|
||||
err := d.QueryRow(`SELECT c.id FROM conversations c
|
||||
JOIN conversation_activities a ON a.conversation_id=c.id
|
||||
WHERE c.agent_key='reporter' AND (
|
||||
c.title = '漏洞报告 · finding#' || $1::text OR
|
||||
c.title ILIKE '%finding#' || $1::text || '%' OR
|
||||
a.detail ILIKE '%finding_id=' || $1::text || '%' OR
|
||||
a.detail ILIKE '%"finding_id":' || $1::text || '%'
|
||||
)
|
||||
ORDER BY c.updated_at DESC, c.id DESC LIMIT 1`, findingID).Scan(&id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := d.SetFindingReportConversationID(findingID, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
// DeleteFinding removes a finding entirely: the standalone findings row and its
|
||||
// originating exploration node (kind='finding'), so it disappears from the findings
|
||||
// list, the per-task 发现 Tab, and the exploration graph alike. Deleting the node
|
||||
|
||||
@@ -1162,11 +1162,13 @@ CREATE TABLE IF NOT EXISTS findings (
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
-- 漏洞详细报告(Markdown);默认空,仅详情页读取/展示,不进列表接口以免 payload 膨胀。
|
||||
report TEXT NOT NULL DEFAULT '',
|
||||
report_conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
ALTER TABLE findings ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending';
|
||||
ALTER TABLE findings ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE findings ADD COLUMN IF NOT EXISTS report TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE findings ADD COLUMN IF NOT EXISTS report_conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_findings_task ON findings(task_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_findings_time ON findings(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status, created_at DESC);
|
||||
|
||||
@@ -701,6 +701,14 @@ func (s *Server) readTriggerBehavior(agentKey string) triggerBehavior {
|
||||
// merging by task / all / none); parallel → run each fire in its own concurrent
|
||||
// conversation up to trigger_max_parallel. Distinct agents always run concurrently.
|
||||
func (s *Server) StartTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string) {
|
||||
s.startTriggeredRun(agentKey, title, message, taskID, mergeable, taskDesc, taskGoal, 0)
|
||||
}
|
||||
|
||||
func (s *Server) StartTriggeredRunWithConversation(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string, conversationID int64) {
|
||||
s.startTriggeredRun(agentKey, title, message, taskID, mergeable, taskDesc, taskGoal, conversationID)
|
||||
}
|
||||
|
||||
func (s *Server) startTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string, conversationID int64) {
|
||||
if s.m.pg == nil || s.chatAgentRef() == nil {
|
||||
return
|
||||
}
|
||||
@@ -714,7 +722,7 @@ func (s *Server) StartTriggeredRun(agentKey, title, message string, taskID int64
|
||||
cfg := s.readTriggerBehavior(agentKey) // DB read BEFORE the lock (never under queueMu)
|
||||
s.queueMu.Lock()
|
||||
s.triggerCfg[agentKey] = cfg
|
||||
s.triggerQ[agentKey] = append(s.triggerQ[agentKey], triggeredRun{agentKey: agentKey, title: title, message: message, taskID: taskID, taskDesc: taskDesc, taskGoal: taskGoal, mergeable: mergeable})
|
||||
s.triggerQ[agentKey] = append(s.triggerQ[agentKey], triggeredRun{conversationID: conversationID, agentKey: agentKey, title: title, message: message, taskID: taskID, taskDesc: taskDesc, taskGoal: taskGoal, mergeable: mergeable})
|
||||
s.pumpLocked(agentKey)
|
||||
s.queueMu.Unlock()
|
||||
}
|
||||
|
||||
@@ -58,20 +58,30 @@ func (s *Server) generateFindingReport(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
message := manualFindingReportMessage(f, taskID)
|
||||
s.StartTriggeredRun(
|
||||
conversation, err := s.m.pg.CreateConversation("reporter", fmt.Sprintf("漏洞报告 · finding#%d", f.ID), nil)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "创建报告对话失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
if err := s.m.pg.SetFindingReportConversationID(f.ID, conversation.ID); err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "关联报告对话失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
s.StartTriggeredRunWithConversation(
|
||||
"reporter",
|
||||
fmt.Sprintf("人工生成步骤与报告 · finding#%d", f.ID),
|
||||
message,
|
||||
taskID,
|
||||
false,
|
||||
taskDesc,
|
||||
taskGoal,
|
||||
taskGoal, conversation.ID,
|
||||
)
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{
|
||||
"queued": true,
|
||||
"agent_key": "reporter",
|
||||
"finding_id": f.ID,
|
||||
"finding_node_id": *f.NodeID,
|
||||
"conversation_id": conversation.ID,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/Autumn-27/artex/agent"
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/artex/intercept"
|
||||
"github.com/Autumn-27/norma/permission"
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
@@ -479,6 +480,11 @@ func (s *Server) toolUpdateFindingReport() actool.CoreTool {
|
||||
return actool.Errorf("归档报告截图失败: " + err.Error()), nil
|
||||
}
|
||||
}
|
||||
if conversationID := intercept.ConvIDFromContext(ctx); conversationID > 0 {
|
||||
if err := s.m.pg.SetFindingReportConversationID(fid, conversationID); err != nil {
|
||||
return actool.Errorf("关联报告对话失败: " + err.Error()), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
n, err := s.m.pg.SetFindingReportVersionByNodeID(ctx, nodeID, a.Report, a.EvidenceVersion)
|
||||
if err != nil {
|
||||
|
||||
@@ -424,6 +424,7 @@ function FindingDetailInner() {
|
||||
agentLabel="报告撰写 Agent"
|
||||
promptPlaceholder="描述要如何调整漏洞报告…"
|
||||
modal
|
||||
initialConversationId={finding.report_conversation_id}
|
||||
open={reportChatOpen}
|
||||
onOpenChange={setReportChatOpen}
|
||||
/>
|
||||
|
||||
@@ -26,6 +26,7 @@ export function FindingAutoChat({
|
||||
agentLabel = "Auto Agent",
|
||||
promptPlaceholder = "询问这个漏洞…",
|
||||
modal = false,
|
||||
initialConversationId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
@@ -35,6 +36,7 @@ export function FindingAutoChat({
|
||||
agentLabel?: string;
|
||||
promptPlaceholder?: string;
|
||||
modal?: boolean;
|
||||
initialConversationId?: number;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
@@ -70,7 +72,11 @@ export function FindingAutoChat({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open || conversation || manualEmpty) return;
|
||||
const storedID = Number(window.localStorage.getItem(storageKey));
|
||||
if (initialConversationId && initialConversationId > 0) {
|
||||
setConversation({ id: initialConversationId, agent_key: agentKey, title: `漏洞 #${findingId} · ${safeTitle}`, created_at: "", updated_at: "" });
|
||||
return;
|
||||
}
|
||||
const storedID = agentKey === "auto" ? Number(window.localStorage.getItem(storageKey)) : 0;
|
||||
if (Number.isSafeInteger(storedID) && storedID > 0) {
|
||||
setConversation({
|
||||
id: storedID,
|
||||
@@ -82,7 +88,7 @@ export function FindingAutoChat({
|
||||
return;
|
||||
}
|
||||
void createConversation();
|
||||
}, [conversation, createConversation, manualEmpty, open, safeTitle, storageKey]);
|
||||
}, [conversation, createConversation, initialConversationId, manualEmpty, open, safeTitle, storageKey]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!historyOpen && !running) return;
|
||||
|
||||
@@ -529,6 +529,7 @@ export interface Finding {
|
||||
summary: string;
|
||||
evidence: string;
|
||||
report?: string; // 详细报告(Markdown);仅详情接口返回,列表为空
|
||||
report_conversation_id?: number;
|
||||
intent_id?: string;
|
||||
param_id?: string;
|
||||
task_id?: string;
|
||||
|
||||
Reference in new issue
Block a user