mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
feat: 漏洞新增 severity=critical(严重) 与 name(漏洞名称)字段
severity 严重级: - report_finding 工具 severity 说明改为 critical|high|medium|low - DB 排序 CASE、report 报告 sevRank、FindingStats 增加 critical - 前端 Severity 类型、status.ts 新增 rose 色调(实心高强调,视觉高于高危)、 发现页严重度筛选/统计卡、任务发现 Tab 排序、dashboard 汇总均加入严重 name 漏洞名称: - findings 表加 name 列(schema + ALTER TABLE ADD COLUMN IF NOT EXISTS 补旧库) - report_finding 工具新增 name 入参(非必填),写入 node payload 与独立 findings 行 - DBFinding/AddFinding/AddStandaloneFinding、FindingDTO、findingPayload、report 全链路透传 - 发现列表页与任务发现 Tab 展示 name,为空时回退 vulnclass;vulnclass 移到展开详情「类型」 - report 报告标题优先 name,回退 vulnclass 无 CHECK 约束,旧库仅需补 name 列;critical/name 均向后兼容。 db 测试补充 critical 排序/统计与 name 往返断言。
This commit is contained in:
1 parent
7b3eec2166
commit
ff3d41ea35
14 files changed
+96
-56
No files matched your search
+9
-8
@@ -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 {
|
||||
|
||||
+3
-2
@@ -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)
|
||||
}
|
||||
+14
-10
@@ -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
|
||||
}
|
||||
|
||||
+24
-20
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+5
-3
@@ -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)
|
||||
|
||||
+6
-2
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex gap-2.5 text-[10px]">
|
||||
<CardContent className="flex flex-wrap gap-2.5 text-[10px]">
|
||||
<span className="text-rose-500">严重 {findingsBySev.critical}</span>
|
||||
<span className="text-red-400">高危 {findingsBySev.high}</span>
|
||||
<span className="text-amber-400">中危 {findingsBySev.medium}</span>
|
||||
<span className="text-slate-400">低危 {findingsBySev.low}</span>
|
||||
|
||||
@@ -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() {
|
||||
<p className="text-muted-foreground text-sm">跨任务漏洞汇总</p>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4 md:gap-6">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5">
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{statCards.map((s) => (
|
||||
<Card key={s.label} className="gap-1 py-4">
|
||||
<CardHeader className="px-4">
|
||||
@@ -186,6 +188,7 @@ export default function FindingsPage() {
|
||||
{(
|
||||
[
|
||||
["all", "全部"],
|
||||
["critical", "严重"],
|
||||
["high", "高危"],
|
||||
["medium", "中危"],
|
||||
["low", "低危"],
|
||||
@@ -293,7 +296,7 @@ export default function FindingsPage() {
|
||||
<TableCell className="max-w-md">
|
||||
<div className="flex min-w-0 flex-col gap-0.5">
|
||||
<span className="truncate font-medium">
|
||||
{f.vulnclass || "未分类"}
|
||||
{f.name || f.vulnclass || "未分类"}
|
||||
</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{f.summary}
|
||||
@@ -380,6 +383,14 @@ export default function FindingsPage() {
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<ShieldAlertIcon className="size-3.5" />
|
||||
证据 · {f.id}
|
||||
{f.vulnclass && (
|
||||
<span>
|
||||
· 类型:
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono">
|
||||
{f.vulnclass}
|
||||
</code>
|
||||
</span>
|
||||
)}
|
||||
{f.param_id && (
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 font-mono">
|
||||
{f.param_id}
|
||||
|
||||
@@ -52,7 +52,7 @@ function Row({
|
||||
<StatusBadge domain="severity" value={f.severity} dot />
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate font-medium">
|
||||
{f.vulnclass || "未分类"}
|
||||
{f.name || f.vulnclass || "未分类"}
|
||||
</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{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];
|
||||
});
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ export const assetCounts: Record<string, number> = assets.reduce<Record<string,
|
||||
|
||||
// ── Findings ─────────────────────────────────────────────────────────────────
|
||||
export const findings: Finding[] = [
|
||||
{ id: "f-1", vulnclass: "SQL Injection", severity: "high", status: "pending", summary: "www.acme.com/search q 参数存在报错型 SQL 注入", evidence: "GET /search?q=1' AND 1=CONVERT(int,@@version)-- → 返回 MSSQL 版本报错,可读库结构。", intent_id: "i-2", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-26T01:20:00Z") },
|
||||
{ id: "f-1", vulnclass: "SQL Injection", name: "官网搜索接口报错型 SQL 注入", severity: "high", status: "pending", summary: "www.acme.com/search q 参数存在报错型 SQL 注入", evidence: "GET /search?q=1' AND 1=CONVERT(int,@@version)-- → 返回 MSSQL 版本报错,可读库结构。", intent_id: "i-2", task_id: "t-acme-web", task_description: "Acme 官网与后台外部渗透", ts: T("2026-07-26T01:20:00Z") },
|
||||
{ id: "f-2", vulnclass: "IDOR", severity: "high", status: "pending", summary: "api.acme.com/v1/orders?id= 可越权读取他人订单", evidence: "将 id=1001 改为 id=1002 返回他人订单(含收货地址、手机号),无归属校验。", intent_id: "i-5", task_id: "t-acme-api", task_description: "api.acme.com 越权与注入测试", ts: T("2026-07-26T02:44:00Z") },
|
||||
{ id: "f-3", vulnclass: "Weak JWT", severity: "high", status: "pending", summary: "API JWT 使用弱密钥、可离线爆破伪造", evidence: "HS256,密钥 'secret',john 5 秒破解 → 可伪造任意 sub 越权。", task_id: "t-acme-api", task_description: "api.acme.com 越权与注入测试", ts: T("2026-07-26T03:02:00Z") },
|
||||
{ id: "f-4", vulnclass: "Reflected XSS", severity: "medium", status: "pending", summary: "搜索页对 q 参数未转义,反射型 XSS", evidence: "q=<script>alert(document.domain)</script> 原样回显于结果标题。", 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) ──────────────────────────────────────────
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -7,6 +7,7 @@ export type Tone =
|
||||
| "green"
|
||||
| "amber"
|
||||
| "red"
|
||||
| "rose"
|
||||
| "violet"
|
||||
| "slate";
|
||||
|
||||
@@ -19,6 +20,8 @@ export const toneClasses: Record<Tone, string> = {
|
||||
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<Tone, string> = {
|
||||
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<string, StatusMeta> = {
|
||||
};
|
||||
|
||||
const severity: Record<string, StatusMeta> = {
|
||||
critical: { label: "严重", tone: "rose" },
|
||||
high: { label: "高危", tone: "red" },
|
||||
medium: { label: "中危", tone: "amber" },
|
||||
low: { label: "低危", tone: "slate" },
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user