diff --git a/agent/tools.go b/agent/tools.go index d45d8bc..a0bd49e 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -642,21 +642,22 @@ func (t *ToolSet) goalMet() actool.CoreTool { func (t *ToolSet) addFinding() actool.CoreTool { return writeTool("report_finding", "[重要]发现漏洞时必须调用该工具进行记录!记录一个确认的漏洞发现。在任务上下文中 intent_id 必填(当前正在执行的意图 id);在会话上下文中 intent_id 可不填。", obj(map[string]any{ - "vulnclass": str("漏洞类"), - "severity": str("high|medium|low"), + "vulnclass": str("漏洞类(分类,如 SQL Injection / IDOR / XSS)"), + "name": str("漏洞名称(具体可读的标题,如『用户中心订单接口存在越权访问』;建议填写,留空时前端回退展示 vulnclass)"), + "severity": str("critical|high|medium|low(严重/高/中/低)"), "summary": str("发现摘要"), "intent_id": idp("产生本发现的意图 id(任务上下文必填;会话上下文可不填)"), - "asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "受影响资产 id(可选,0/1/多个):参数/端点/站点等。一个漏洞影响多处可全填,纯观察可不填。"}, + "asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "【存在时尽量填写,否则在摘要中必须要写清楚漏洞位置】受影响资产 id(可选,0/1/多个):参数/端点/站点等。一个漏洞影响多处可全填,纯观察可不填。"}, "evidence": str("证据/PoC 文本"), }, "vulnclass", "severity", "summary"), func(_ context.Context, in json.RawMessage) (actool.Result, error) { var a struct { - VulnClass, Severity, Summary, Evidence string - IntentID json.RawMessage `json:"intent_id"` - AssetIDs []json.RawMessage `json:"asset_ids"` + VulnClass, Name, Severity, Summary, Evidence string + IntentID json.RawMessage `json:"intent_id"` + AssetIDs []json.RawMessage `json:"asset_ids"` } _ = json.Unmarshal(in, &a) - payload := map[string]any{"vulnclass": a.VulnClass, "severity": a.Severity, "summary": a.Summary, + payload := map[string]any{"vulnclass": a.VulnClass, "name": a.Name, "severity": a.Severity, "summary": a.Summary, "evidence": map[string]any{"by": t.worker, "poc": a.Evidence}} var anchors []int64 for _, raw := range a.AssetIDs { @@ -674,7 +675,7 @@ func (t *ToolSet) addFinding() actool.CoreTool { if intent := pid(a.IntentID); intent > 0 { _ = t.ts.Link(intent, db.RelYields, id) // chain: intent -> finding } - _, _ = t.ts.AddStandaloneFinding(t.taskID, id, a.VulnClass, a.Severity, a.Summary, a.Evidence, t.worker, anchors) + _, _ = t.ts.AddStandaloneFinding(t.taskID, id, a.VulnClass, a.Name, a.Severity, a.Summary, a.Evidence, t.worker, anchors) // 确证漏洞落库 → 当场唤醒本任务 planner(不等 worker 收工,debounce 合并)。 // 优先带上下文(哪个意图+finding摘要);intent 用工具参数,缺省回退到 owner 意图。 if t.notifyFinding != nil { diff --git a/db/exploration.go b/db/exploration.go index 94e16bc..00f6024 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -632,6 +632,7 @@ FROM activity WHERE exploration_id=$1 AND id>$2 ORDER BY id LIMIT $3`, s.expID, // - Worker != "" → filter by worker name (Main = "mainagent", Plan = "planner"). // Goal Agent 的第 0 轮拆解也以 worker="planner" 落库,故 Plan 会话完整覆盖 Goal+Planner。 // - NodeID != nil → a Worker session, filtered by node_id (= intent id). +// // A zero value (both empty) matches the whole task (no session filter). type ActivitySessionFilter struct { Worker string @@ -885,6 +886,6 @@ FROM activity WHERE exploration_id=$1 AND kind<>'thinking' AND id IN (`+strings. // AddStandaloneFinding writes a finding to the standalone findings table, which // persists across task deletion (task_id / node_id become NULL when the task or // exploration node is deleted). taskID and nodeID may be 0 (stored as NULL). -func (s *ExplorationStore) AddStandaloneFinding(taskID, nodeID int64, vulnclass, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) { - return s.db.AddFinding(taskID, nodeID, vulnclass, severity, summary, evidence, worker, assetIDs) +func (s *ExplorationStore) AddStandaloneFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) { + return s.db.AddFinding(taskID, nodeID, vulnclass, name, severity, summary, evidence, worker, assetIDs) } diff --git a/db/findings.go b/db/findings.go index 23c1cc3..c6903ec 100644 --- a/db/findings.go +++ b/db/findings.go @@ -13,6 +13,7 @@ type DBFinding struct { TaskID *int64 NodeID *int64 VulnClass string + Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass Severity string Summary string Evidence string @@ -46,8 +47,9 @@ func ValidFindingStatus(s string) bool { } // AddFinding inserts a finding into the standalone findings table. taskID and -// nodeID may be 0 (stored as NULL). Returns the new finding id. -func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) { +// nodeID may be 0 (stored as NULL). name may be "" (frontend falls back to +// vulnclass). Returns the new finding id. +func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) { aidsJSON, _ := json.Marshal(assetIDs) if assetIDs == nil { aidsJSON = []byte("[]") @@ -61,16 +63,16 @@ func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, severity, summary, evid } var id int64 err := d.QueryRow( - `INSERT INTO findings (task_id, node_id, vulnclass, severity, summary, evidence, worker, asset_ids) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, - tid, nid, vulnclass, severity, summary, evidence, worker, string(aidsJSON), + `INSERT INTO findings (task_id, node_id, vulnclass, name, severity, summary, evidence, worker, asset_ids) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id`, + tid, nid, vulnclass, name, severity, summary, evidence, worker, string(aidsJSON), ).Scan(&id) return id, err } // 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, f.severity, f.summary, +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, COALESCE(t.description, '') AS task_description` @@ -84,7 +86,7 @@ func scanFindings(rows interface { for rows.Next() { f := &DBFinding{} var aidsJSON string - if err := rows.Scan(&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Severity, + if err := rows.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); err != nil { return nil, err } @@ -162,8 +164,8 @@ func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding order := "f.created_at DESC" if f.Sort == "severity" { - // high > medium > low > 其它, then newest first. - order = `CASE f.severity WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 ELSE 0 END DESC, f.created_at DESC` + // critical > high > medium > low > 其它, then newest first. + order = `CASE f.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 ELSE 0 END DESC, f.created_at DESC` } pageArgs := append(append([]any{}, args...), pageSize, (page-1)*pageSize) q := fmt.Sprintf(` @@ -187,6 +189,7 @@ func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding type FindingStats struct { Total int `json:"total"` Pending int `json:"pending"` + Critical int `json:"critical"` High int `json:"high"` Medium int `json:"medium"` Low int `json:"low"` @@ -200,10 +203,11 @@ func (d *DB) FindingStats() (*FindingStats, error) { err := d.QueryRow(`SELECT COUNT(*), COUNT(*) FILTER (WHERE status = 'pending'), + COUNT(*) FILTER (WHERE severity = 'critical'), COUNT(*) FILTER (WHERE severity = 'high'), COUNT(*) FILTER (WHERE severity = 'medium'), COUNT(*) FILTER (WHERE severity = 'low') - FROM findings`).Scan(&st.Total, &st.Pending, &st.High, &st.Medium, &st.Low) + FROM findings`).Scan(&st.Total, &st.Pending, &st.Critical, &st.High, &st.Medium, &st.Low) if err != nil { return nil, err } diff --git a/db/findings_test.go b/db/findings_test.go index aca4fc7..68b93f6 100644 --- a/db/findings_test.go +++ b/db/findings_test.go @@ -21,19 +21,21 @@ func TestFindingsPageAndStats(t *testing.T) { cleanup() defer cleanup() - // Seed 5 findings under the marker vulnclass: 3 high, 2 low; 2 pending, 3 resolved. + // Seed 6 findings under the marker vulnclass: 1 critical, 3 high, 2 low; 2 pending. + // The critical row carries a name to verify round-trip. seed := []struct { - sev, status string + sev, status, name string }{ - {"high", "pending"}, - {"high", "resolved"}, - {"high", "resolved"}, - {"low", "pending"}, - {"low", "resolved"}, + {"critical", "pending", "严重漏洞标题"}, + {"high", "resolved", ""}, + {"high", "resolved", ""}, + {"high", "pending", ""}, + {"low", "resolved", ""}, + {"low", "resolved", ""}, } var ids []int64 for i, s := range seed { - id, err := d.AddFinding(0, 0, vc, s.sev, "summary", "poc", "tester", nil) + id, err := d.AddFinding(0, 0, vc, s.name, s.sev, "summary", "poc", "tester", nil) if err != nil { t.Fatalf("AddFinding[%d]: %v", i, err) } @@ -43,27 +45,26 @@ func TestFindingsPageAndStats(t *testing.T) { ids = append(ids, id) } - // Filter by our vulnclass → exactly the 5 seeded rows, paged 2 per page. + // Filter by our vulnclass → exactly the 6 seeded rows, paged 2 per page. p1, total, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Sort: "severity"}, 1, 2) if err != nil { t.Fatal(err) } - if total != 5 { - t.Fatalf("total: want 5, got %d", total) + if total != 6 { + t.Fatalf("total: want 6, got %d", total) } if len(p1) != 2 { t.Fatalf("page1 size: want 2, got %d", len(p1)) } - // severity sort → highs first - if p1[0].Severity != "high" || p1[1].Severity != "high" { - t.Fatalf("severity sort broken: %q, %q", p1[0].Severity, p1[1].Severity) + // severity sort → critical first (with its name round-tripped), then high. + if p1[0].Severity != "critical" { + t.Fatalf("severity sort: want critical first, got %q", p1[0].Severity) } - p3, _, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Sort: "severity"}, 3, 2) - if err != nil { - t.Fatal(err) + if p1[0].Name != "严重漏洞标题" { + t.Fatalf("name round-trip: want 严重漏洞标题, got %q", p1[0].Name) } - if len(p3) != 1 { - t.Fatalf("page3 size: want 1 (5 rows / 2), got %d", len(p3)) + if p1[1].Severity != "high" { + t.Fatalf("severity sort: want high second, got %q", p1[1].Severity) } // Combined filter: vulnclass + status=pending → 2 rows. @@ -90,7 +91,10 @@ func TestFindingsPageAndStats(t *testing.T) { if err != nil { t.Fatal(err) } - if st.Total < 5 || st.High < 3 || st.Low < 2 || st.Pending < 2 { + if st.Critical < 1 { + t.Fatalf("stats critical undercount: %+v", st) + } + if st.Total < 6 || st.High < 3 || st.Low < 2 || st.Pending < 2 { t.Fatalf("stats undercount: %+v", st) } if !slices.Contains(st.VulnClasses, vc) { diff --git a/db/schema.sql b/db/schema.sql index 2662e43..874ed2e 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -540,6 +540,9 @@ CREATE TABLE IF NOT EXISTS findings ( task_id BIGINT REFERENCES tasks(id) ON DELETE SET NULL, node_id BIGINT REFERENCES exploration_nodes(id) ON DELETE SET NULL, vulnclass TEXT NOT NULL DEFAULT '', + -- 漏洞名称(可读标题);为空时前端回退展示 vulnclass。severity 取值: + -- critical 严重 / high 高 / medium 中 / low 低(不加 CHECK,与 status 一致由 server 白名单校验)。 + name TEXT NOT NULL DEFAULT '', severity TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', evidence TEXT NOT NULL DEFAULT '', @@ -552,6 +555,7 @@ CREATE TABLE IF NOT EXISTS findings ( 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 ''; 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); diff --git a/report/report.go b/report/report.go index 26a972e..fb7f2d7 100644 --- a/report/report.go +++ b/report/report.go @@ -23,6 +23,7 @@ type Input struct { type findingView struct { VulnClass string + Name string Severity string Summary string PoC string @@ -31,6 +32,7 @@ type findingView struct { func parseFinding(n *db.Node) findingView { var p struct { VulnClass string `json:"vulnclass"` + Name string `json:"name"` Severity string `json:"severity"` Summary string `json:"summary"` Evidence struct { @@ -38,10 +40,10 @@ func parseFinding(n *db.Node) findingView { } `json:"evidence"` } _ = json.Unmarshal(n.Payload, &p) - return findingView{p.VulnClass, p.Severity, p.Summary, p.Evidence.PoC} + return findingView{p.VulnClass, p.Name, p.Severity, p.Summary, p.Evidence.PoC} } -var sevRank = map[string]int{"high": 0, "medium": 1, "low": 2, "": 3} +var sevRank = map[string]int{"critical": 0, "high": 1, "medium": 2, "low": 3, "": 4} // Markdown renders the report. func Markdown(in Input) string { @@ -78,7 +80,7 @@ func Markdown(in Input) string { } sort.SliceStable(fs, func(i, j int) bool { return sevRank[fs[i].Severity] < sevRank[fs[j].Severity] }) for i, f := range fs { - fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), nz(f.VulnClass, "未分类")) + fmt.Fprintf(&b, "### %d. [%s] %s\n\n", i+1, strings.ToUpper(nz(f.Severity, "info")), nz(f.Name, nz(f.VulnClass, "未分类"))) fmt.Fprintf(&b, "%s\n\n", nz(f.Summary, "")) if f.PoC != "" { fmt.Fprintf(&b, "**PoC / 证据:**\n\n```\n%s\n```\n\n", f.PoC) diff --git a/server/dto.go b/server/dto.go index 46cc2bb..ed4c193 100644 --- a/server/dto.go +++ b/server/dto.go @@ -175,8 +175,9 @@ type FindingDTO struct { ID string `json:"id"` FindingID string `json:"finding_id,omitempty"` // standalone findings-table id — the handle for status updates VulnClass string `json:"vulnclass"` - Severity string `json:"severity"` - Status string `json:"status"` // pending | false_positive | ignored | resolved + Name string `json:"name,omitempty"` // 漏洞名称;为空时前端回退展示 vulnclass + Severity string `json:"severity"` // critical | high | medium | low + Status string `json:"status"` // pending | in_progress | confirmed | resolved | false_positive | ignored | duplicate | risk_accepted Summary string `json:"summary"` Evidence string `json:"evidence"` IntentID string `json:"intent_id,omitempty"` @@ -229,6 +230,7 @@ func assetLabel(a *db.Asset) string { // (agent/tools.go addFinding): {vulnclass, severity, summary, evidence:{by,poc}}. type findingPayload struct { VulnClass string `json:"vulnclass"` + Name string `json:"name"` Severity string `json:"severity"` Summary string `json:"summary"` Evidence json.RawMessage `json:"evidence"` @@ -240,6 +242,7 @@ func findingDTO(n *db.Node) FindingDTO { return FindingDTO{ ID: i64s(n.ID), VulnClass: p.VulnClass, + Name: p.Name, Severity: p.Severity, Status: db.FindingPending, Summary: p.Summary, @@ -293,6 +296,7 @@ func findingFromDB(f *db.DBFinding, assets map[int64]*db.Asset) FindingDTO { ID: i64s(f.ID), FindingID: i64s(f.ID), VulnClass: f.VulnClass, + Name: f.Name, Severity: f.Severity, Status: status, Summary: f.Summary, diff --git a/web/src/app/(main)/dashboard/page.tsx b/web/src/app/(main)/dashboard/page.tsx index 07468ba..9aaab90 100644 --- a/web/src/app/(main)/dashboard/page.tsx +++ b/web/src/app/(main)/dashboard/page.tsx @@ -239,7 +239,7 @@ export default function DashboardPage() { }, [tasks]); const findingsBySev = React.useMemo(() => { - const m = { high: 0, medium: 0, low: 0 }; + const m = { critical: 0, high: 0, medium: 0, low: 0 }; for (const f of findings) { if (f.severity in m) m[f.severity as keyof typeof m]++; } @@ -491,7 +491,8 @@ export default function DashboardPage() { {findings.length} - + + 严重 {findingsBySev.critical} 高危 {findingsBySev.high} 中危 {findingsBySev.medium} 低危 {findingsBySev.low} diff --git a/web/src/app/(main)/function/findings/page.tsx b/web/src/app/(main)/function/findings/page.tsx index 3061f27..4b856b9 100644 --- a/web/src/app/(main)/function/findings/page.tsx +++ b/web/src/app/(main)/function/findings/page.tsx @@ -52,6 +52,7 @@ const FINDING_STATUSES: FindingStatus[] = [ const EMPTY_STATS: FindingStats = { total: 0, pending: 0, + critical: 0, high: 0, medium: 0, low: 0, @@ -156,6 +157,7 @@ export default function FindingsPage() { const statCards: { label: string; value: number; tone?: string }[] = [ { label: "发现总数", value: stats.total }, { label: "待处理", value: stats.pending, tone: "text-amber-500" }, + { label: "严重", value: stats.critical, tone: "text-rose-600" }, { label: "高危", value: stats.high, tone: "text-red-500" }, { label: "中危", value: stats.medium, tone: "text-amber-500" }, { label: "低危", value: stats.low, tone: "text-slate-500" }, @@ -168,7 +170,7 @@ export default function FindingsPage() {

跨任务漏洞汇总

-
+
{statCards.map((s) => ( @@ -186,6 +188,7 @@ export default function FindingsPage() { {( [ ["all", "全部"], + ["critical", "严重"], ["high", "高危"], ["medium", "中危"], ["low", "低危"], @@ -293,7 +296,7 @@ export default function FindingsPage() {
- {f.vulnclass || "未分类"} + {f.name || f.vulnclass || "未分类"} {f.summary} @@ -380,6 +383,14 @@ export default function FindingsPage() {
证据 · {f.id} + {f.vulnclass && ( + + · 类型: + + {f.vulnclass} + + + )} {f.param_id && ( {f.param_id} diff --git a/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx b/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx index e1df1df..000836b 100644 --- a/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx +++ b/web/src/app/(main)/function/tasks/detail/_tabs/findings-tab.tsx @@ -52,7 +52,7 @@ function Row({
- {f.vulnclass || "未分类"} + {f.name || f.vulnclass || "未分类"} {f.summary} @@ -161,7 +161,7 @@ export function FindingsTab({ taskId }: { taskId: string }) { const items = findings .filter((f) => f.task_id === taskId) .sort((a, b) => { - const order = { high: 0, medium: 1, low: 2 }; + const order = { critical: 0, high: 1, medium: 2, low: 3 }; return order[a.severity] - order[b.severity]; }); diff --git a/web/src/lib/mock/data.ts b/web/src/lib/mock/data.ts index 9136e61..fc647bf 100644 --- a/web/src/lib/mock/data.ts +++ b/web/src/lib/mock/data.ts @@ -161,7 +161,7 @@ export const assetCounts: Record = assets.reducealert(document.domain) 原样回显于结果标题。", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T22:10:00Z") }, @@ -173,12 +173,12 @@ export const findings: Finding[] = [ { id: "f-10", vulnclass: "Outdated Component", severity: "medium", status: "pending", summary: "shop 使用存在已知 RCE 的老版本组件", evidence: "指纹识别到组件 v2.3.1,对应 CVE-2024-xxxx 反序列化 RCE。", task_id: "t-shop-pay", task_description: "shop.acme.com 支付与订单链路", ts: T("2026-07-23T15:00:00Z") }, // ── 外网→内网纵深链路上的高危发现(active task)── { id: "f-11", vulnclass: "Hardcoded Credentials", severity: "high", status: "pending", summary: "泄露源码中硬编码数据库凭据 sa/Acme@2021", evidence: "git-dumper 还原 www.acme.com/.git 得到 config.php,内含明文 DB 口令,后续被证实可在内网复用。", intent_id: "ig", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T20:36:00Z") }, - { id: "f-12", vulnclass: "Deserialization RCE", severity: "high", status: "pending", summary: "shop.acme.com Fastjson 1.2.24 反序列化远程命令执行", evidence: "POST /api/import 提交 JNDI payload(@type:JdbcRowSetImpl)→ 回连 dnslog 并落地反弹 shell(www-data@dmz-web01)。", intent_id: "i7", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T15:40:00Z") }, + { id: "f-12", vulnclass: "Deserialization RCE", name: "shop 商城 Fastjson 反序列化远程命令执行", severity: "critical", status: "confirmed", summary: "shop.acme.com Fastjson 1.2.24 反序列化远程命令执行", evidence: "POST /api/import 提交 JNDI payload(@type:JdbcRowSetImpl)→ 回连 dnslog 并落地反弹 shell(www-data@dmz-web01)。", intent_id: "i7", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T15:40:00Z") }, { id: "f-13", vulnclass: "Privilege Escalation", severity: "high", status: "pending", summary: "DMZ 立足点 sudo NOPASSWD 错配可本地提权 root", evidence: "sudo -l 显示 (ALL) NOPASSWD: /usr/bin/python3 → sudo python3 -c 'os.setuid(0);os.system(\"/bin/sh\")' 直接拿 root。", intent_id: "i8", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T16:20:00Z") }, { id: "f-14", vulnclass: "Network Segmentation", severity: "medium", status: "pending", summary: "DMZ 主机双网卡直连内网,边界隔离失效", evidence: "dmz-web01 第二网卡在 10.10.10.0/24,可从 DMZ 直达域控/文件服务器/Jenkins,越过网络分区。", intent_id: "i9", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T21:00:00Z") }, { id: "f-15", vulnclass: "Unauthenticated RCE", severity: "high", status: "pending", summary: "内部 Jenkins 未授权 Script Console 远程命令执行", evidence: "GET /script 无需登录即可执行 Groovy:'whoami'.execute().text → SYSTEM;并从凭据库导出域账号 acme\\svc_deploy。", intent_id: "i11", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T21:50:00Z") }, { id: "f-16", vulnclass: "Kerberoasting", severity: "high", status: "pending", summary: "域服务账号 svc_sql 可被 Kerberoast 且口令弱", evidence: "GetUserSPNs 请求 svc_sql 的 TGS,hashcat -m 13100 离线破出 Sql@2020,该账号属于 SQL 管理组。", intent_id: "i12", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-25T23:20:00Z") }, - { id: "f-17", vulnclass: "Domain Compromise", severity: "high", status: "pending", summary: "拿下域控 DC01(Domain Admin)—— 内部靶标达成", evidence: "svc_deploy 属于 Domain Admins,psexec.py 以其登录 DC01 得 SYSTEM,secretsdump 导出全域 NTLM 哈希(含 krbtgt),完成靶标控制。", intent_id: "i13", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-26T00:20:00Z") }, + { id: "f-17", vulnclass: "Domain Compromise", name: "内网域控 DC01 完全控制(Domain Admin)", severity: "critical", status: "confirmed", summary: "拿下域控 DC01(Domain Admin)—— 内部靶标达成", evidence: "svc_deploy 属于 Domain Admins,psexec.py 以其登录 DC01 得 SYSTEM,secretsdump 导出全域 NTLM 哈希(含 krbtgt),完成靶标控制。", intent_id: "i13", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-26T00:20:00Z") }, ]; // ── Exploration graph (active task) ────────────────────────────────────────── diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts index 65c71b9..ced95cb 100644 --- a/web/src/lib/mock/handler.ts +++ b/web/src/lib/mock/handler.ts @@ -78,6 +78,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re return { total: D.findings.length, pending: D.findings.filter((f) => f.status === "pending").length, + critical: D.findings.filter((f) => f.severity === "critical").length, high: D.findings.filter((f) => f.severity === "high").length, medium: D.findings.filter((f) => f.severity === "medium").length, low: D.findings.filter((f) => f.severity === "low").length, @@ -88,7 +89,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re if (task) return D.findings.filter((f) => f.task_id === task); // 全局:带 page/limit → 分页对象;否则裸数组(dashboard)。 if (!q.has("page") && !q.has("limit")) return D.findings; - const sev = { high: 3, medium: 2, low: 1 } as const; + const sev = { critical: 4, high: 3, medium: 2, low: 1 } as const; let list = D.findings.slice(); const fSev = q.get("severity"); const fStatus = q.get("status"); diff --git a/web/src/lib/status.ts b/web/src/lib/status.ts index 5d45ba2..07d4d97 100644 --- a/web/src/lib/status.ts +++ b/web/src/lib/status.ts @@ -7,6 +7,7 @@ export type Tone = | "green" | "amber" | "red" + | "rose" | "violet" | "slate"; @@ -19,6 +20,8 @@ export const toneClasses: Record = { amber: "bg-amber-500/15 text-amber-600 dark:text-amber-400 border-amber-500/20", red: "bg-red-500/15 text-red-600 dark:text-red-400 border-red-500/20", + // rose 用作「严重」——实心高强调,视觉上明显高于「高危」的软红描边。 + rose: "bg-rose-600 text-white border-rose-600 dark:bg-rose-600 dark:text-white", violet: "bg-violet-500/15 text-violet-600 dark:text-violet-400 border-violet-500/20", slate: @@ -31,6 +34,7 @@ export const toneDot: Record = { green: "bg-emerald-500", amber: "bg-amber-500", red: "bg-red-500", + rose: "bg-white", violet: "bg-violet-500", slate: "bg-slate-500", }; @@ -58,6 +62,7 @@ const task: Record = { }; const severity: Record = { + critical: { label: "严重", tone: "rose" }, high: { label: "高危", tone: "red" }, medium: { label: "中危", tone: "amber" }, low: { label: "低危", tone: "slate" }, diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index a199111..6320900 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -237,7 +237,7 @@ export interface TaskNode { } // ---- Findings ---- -export type Severity = "high" | "medium" | "low"; +export type Severity = "critical" | "high" | "medium" | "low"; // 漏洞处置状态:待处理 / 处理中 / 已确认 / 已处理 / 误报 / 忽略 / 重复 / 风险接受。 export type FindingStatus = @@ -261,6 +261,7 @@ export interface Finding { id: string; finding_id?: string; // 独立 findings 表的行 id,状态更新的句柄(任务内旧节点可能缺失) vulnclass: string; + name?: string; // 漏洞名称;为空时展示回退到 vulnclass severity: Severity; status: FindingStatus; summary: string; @@ -285,6 +286,7 @@ export interface FindingsPage { export interface FindingStats { total: number; pending: number; + critical: number; high: number; medium: number; low: number;