From 4865d403d1fc4f7cabe3f19c4edcf60d37ca7664 Mon Sep 17 00:00:00 2001 From: steponeerror Date: Mon, 26 Oct 2026 16:27:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20scanner=20=E5=8E=82=E5=95=86?= =?UTF-8?q?=E8=87=AA=E6=8A=A5=E6=AE=B5=20chip=20=E8=BF=9B=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E8=A1=A8=E6=A0=87=E7=AD=BE=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scanner_ranges native_categories(研究扫描 · X(自报段))原先只在展开行 证人明细可见;标签列直接出 chip(title=来源)。噪声守门:仅 scanner_ranges 源自报段声明进标签列,sw_scanners 计数型标签留展开面板;去重+封顶3溢出+N。 门:vitest 440/440(新三例)+tsc+build 绿 --- CHANGELOG.md | 3 ++ frontend/src/components/ResultTable.tsx | 37 +++++++++++++++ .../components/__tests__/ResultTable.test.tsx | 47 +++++++++++++++++++ 3 files changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 017141f2..9a2db73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ ### 新增 Added + - **查询结果「标签」列新增 scanner 厂商自报段 chip(2026-10-26,单写者直修)**:scanner_ranges 源的 native_categories(「研究扫描 · X(自报段)」)原先只在展开行证人明细可见,现直接在结果表标签列出一条 chip(悬停 title=来源);噪声守门=仅 scanner_ranges 源的自报段声明进标签列,sw_scanners 等计数型标签仍留在展开面板;去重+封顶 3 溢出 +N;门:frontend vitest 440/440(新三例:自报段 chip 出+源在 title/他源计数标签不进/无 native 不渲染)+tsc+build 绿 + - **Scanner vendor self-declared-range chip in the query-result tags column (2026-10-26, single-writer direct fix)**: scanner_ranges native_categories (「研究扫描 · X(自报段)」) previously surfaced only in the expanded row's witness details; the tags column now renders one chip per label (hover title = source); noise guard = only the self-declared-range labels of the scanner_ranges source enter the tags column — sw_scanners-style count labels stay in the expanded panel; dedup + cap 3 with +N overflow; gates: frontend vitest 440/440 (three new: chip renders + source in title / other-source count labels excluded / no-native renders nothing) + tsc + build green + - **Discovery-round tool face actually wired (ticket #217 = T3.5, spec-axis P1 backfill under umbrella #206; the three seams that previously had only selftest consumers are now the production consumption path)**: ① three-point wiring per the discovery-host.mjs wiring protocol — SHORTLIST (model step) gets host.tools direct install (faux.mjs runRealStep `ensureDiscoverHost(ctx)`, same injection shape as DEEP's microprobe host; ctx-level dupRegistry shared with model calls, round-level breaker instance bound to ops/breaker.json); MEASURE (deterministic, zero-model) issues real evidence-face probes via host.call("web_read") — existence stays host-recomputed, URL candidates get a live existence probe through the same guard chain (dup→breaker→cap) with measure-N call-ids and tool_seeds trace; computable criteria compute (measured when all three faces resolve), non-computable ones hold with an explicit uncertainty reason (no-URL candidate / host-refused probe — 被拒≠清白 — / missing SHORTLIST exclusivity estimate: the measure face has no re-estimation right and never fabricates); control_executable = the positive control of the probe itself (url_read_ok); exclusivity stays a passthrough of the SHORTLIST value. CLOSE gains the pure-increment readings key `discover_channels` (live host channelCounts preferred; host absent on resume/replay → recomputed from the d2 transcript registry_seeds + d3 tool_seeds artifacts, 宁少算 — rejected calls replay as nothing; health rounds byte-identical). readDiscoverKnobs gains the dupCallLimit key (same OPS_PIPELINE_DUP_CALL_LIMIT knob name and default as ops rounds — its own header comment had reserved this as the consumption point); faux face gets fixtureDiscoveryMethods (canned web_read table in fixtures/pipeline/measure.json; uncanned = honest io_error envelope, zero-network law). ② the end-to-end positive example finally exists (previously the full-round pin was zero-enqueue only): the faux six-step round now walks one candidate SHORTLIST→MEASURE→PROPOSE into a real queue.md row (fixture rDNS candidate gets exclusivity 0.42 + a canned ok web_read), pinned in both selftest-pipeline (full-round E2E: d3 measured/control_executable true, d4 1-enqueued/1-skipped, PROPOSE rdns row, discover_channels readings) and golden pipeline_discover_full_faux (same pins + queue_file row assertion); four new selftest examples (fixture-method three states incl. canned-fail passthrough / MEASURE tri-state incl. dup interception at the limit with explicit repeat_storm uncertainty / CLOSE live-host-vs-replay-and-health-round-equivalence / wiring mount-point sentinels). ③ queue.md frozen header sentence corrected — 「queue_action 唯一写口」 stopped being true the day ENQUEUE candidate rows and appendCandidateCard cards landed: the header now truthfully names the three write ports (ops-round queue_action · discovery-round ENQUEUE candidate row · discovery-round candidate card appendCandidateCard; the latter two action=PROPOSE one word one spelling), flipped in the same batch across the JS face (queue-lock.mjs QUEUE_HEADER), the python face (_ops_store.py QUEUE_HEADER), both python test fixtures (the northstar fixture's pre-existing missing-rationale-clause drift healed in passing), tools.mjs/discover-candidate.mjs notes — plus a new contract sentinel pinning the three copies (python literal ↔ node literal ↔ the header actually written by appendQueueRowLocked first-create) verbatim-equal and semantically three-write-port (a rollback to the old sentence reddens the sentinel). Gates (worktree-verified): selftest-pipeline 104/0 (base 100 + 4 new; red proof: reverting steps.mjs+faux.mjs wiring → exactly 100/4 with only the T3.5 examples red), golden 10/0, contract 19/0 (18 + header sentinel), t4 63/0, standard-check OK=56 FAIL=0 挂点=40, backend touched tests 48 passed (test_ops_store + test_eval_northstar), node --check all touched faces. - **发现轮工具面真接线(票 #217 = T3.5,Spec 轴 P1 补票,伞 #206;原先只有 selftest 消费面的三接缝成为真生产消费路)**:①三点接线照 discovery-host.mjs 接线协议——SHORTLIST(模型步)host.tools 直装(faux.mjs runRealStep `ensureDiscoverHost(ctx)`,与 DEEP 微探针宿主同注入形;dupRegistry 复用 ctx 级同注册表,熔断实例轮级挂 ops/breaker.json);MEASURE(确定性零模型)经 host.call("web_read") 补发真证据面——存在性仍宿主重算,URL 候选补发实测探测同守卫链(dup→breaker→cap)+measure-N call-id+tool_seeds 留痕;能算的算出(三面全解=measured),不可算的显式 uncertainty 原因(无 URL 候选/宿主拒绝——被拒≠清白——/排他率缺 SHORTLIST 估值:测量面无改估权不虚构);control_executable=探测自身的阳性对照(url_read_ok);排他率仍透传 SHORTLIST 值。CLOSE 增纯增量读数键 `discover_channels`(活宿主 channelCounts 优先;resume/重放宿主缺席=从 d2 transcript registry_seeds+d3 tool_seeds 工件重算,宁少算——被拒不回放;健康轮字节面不变)。readDiscoverKnobs 增 dupCallLimit 键(与运营轮同旋钮名 OPS_PIPELINE_DUP_CALL_LIMIT 同缺省 3——其 header 注记本就预留此处为消费点);faux 面新增 fixtureDiscoveryMethods(fixtures/pipeline/measure.json 罐装 web_read 表;未罐装=诚实 io_error 信封,零网络律)。②端到端候选行正例终于存在(原全轮例只钉零入队):faux 六步全轮 1 候选走通 SHORTLIST→MEASURE→PROPOSE 落真 queue.md 行(fixture rDNS 候选排他率 0.42+罐装 ok web_read),selftest-pipeline(全轮 E2E:d3 measured/control_executable true、d4 1入队/1skip、PROPOSE rdns 行、discover_channels 读数)与 golden pipeline_discover_full_faux(同钉+queue_file 行断言)双面钉;新增四例(罐装方法三态含罐装失败面透传/MEASURE 三态含 limit 次 dup 拦截显式 repeat_storm uncertainty/CLOSE 活宿主优先↔回放重算↔健康轮位等价/接线挂点哨)。③queue.md 冻结表头句翻正——ENQUEUE 候选行与 appendCandidateCard 候选卡落地之日起「queue_action 唯一写口」即不再真:表头如实述三写口(ops 轮 queue_action · 发现轮 ENQUEUE 候选行 · 发现轮候选卡 appendCandidateCard;后两路 action=PROPOSE 一词一拼),JS 面(queue-lock.mjs QUEUE_HEADER)+python 面(_ops_store.py QUEUE_HEADER)+两件 python 测试 fixture(northstar fixture 预存 rationale 子句缺失漂移顺手对齐)+tools.mjs/discover-candidate.mjs 注记同批翻;新 contract 哨钉三抄(python 字面↔node 字面↔appendQueueRowLocked 首建实写串)逐字同+三写词语义哨(回退旧句即哨红)。门(worktree 实跑):selftest-pipeline 104/0(基 100+新 4;红证:steps.mjs+faux.mjs 接线回退→恰 100/4 仅 T3.5 例红)、golden 10/0、contract 19/0(18+表头哨)、t4 63/0、standard-check OK=56 FAIL=0 挂点=40、backend 触面测试 48 passed(test_ops_store+test_eval_northstar)、node --check 全触面。 - **Discovery-round pipeline (umbrella #206, tickets #207-#210; ADR 0010; the discovery side goes from ad-hoc chat to the sixth round type under the same law as ops rounds)**: T1 (#207) — pipeline.mjs PIPELINES gains DISCOVER_PIPELINE (six data-driven steps SCAN→SHORTLIST→MEASURE→ENQUEUE→RETRO→CLOSE; SCAN/MEASURE deterministic zero-model, SHORTLIST model-produced, ENQUEUE write-only) + the OPS_DISCOVER_* knob family (30 calls/3600s/¥5 per round; monthly cap shared with ops rounds at ¥30 through the same admitOrRefuse face) + four discover artifacts into the ARTIFACTS table (22 total) + ledger tag pipeline-discover. T3 (#209) — structured candidate schema with a hand-written validator (CANDIDATE_FIELDS frozen; evidence-pointer existence failures drop the row with a trace, 宁少算, never fabricate), mkDiscoveryHost wiring the three web discovery tools (DISCOVERY_TOOLS frozen; guard chain dup→breaker→cap), queue.md candidate cards written PROPOSE under the #139 bilateral flock, per-channel quota counts as first-class readings. T2 (#208, not yet confluenced when this entry was authored) — G_SHORTLIST span-level verbatim citation (spanMatcher; URL hallucination or reworded quote rejects), G_MEASURE host-side number recomputation, G_ENQUEUE reconciliation, each with negative fixtures. T4 (#210, this entry) — the candidate-row action word unified to PROPOSE (steps.mjs ENQUEUE step consumes CANDIDATE_ACTION from discover-candidate.mjs, one word one spelling; deliberately distinct from the human-action closed set ENQUEUE/REJECT on the python write face), northstar gains two observation-only discovery legs (propose→landing conversion from queue.md PROPOSE rows vs the source registry + exclusivity distribution aggregated from d3-measure; zero-hold triple proven: no exit path, not in the regression family, CI untouched; missing artifacts render N/A sentinels, never fabricated), STANDARD.md D-7 发现与运营同律 with machine-checked mounts 挂点 31→40 (eight at authoring; spanMatcher's citation.mjs mount attached at confluence per the T4 handoff — pre-confluence it would have been a dead anchor = pseudo-standard), GLOSSARY coins PROPOSE, DESIGN §6 discovery wording refreshed. Gates (confluence finals, merger-verified live): selftest-pipeline 100/0 (T4's base 96 + T2's four span cases; red proof: reverting the ENQUEUE-step action word reddens the queue-row assertion 99/1), golden 10/0 (9 + T2's pipeline_discover_full_faux), backend tests/eval 493 passed (tests/core+tests/eval full 1224 passed 8 skipped; northstar 26/26; red-first via ImportError; loaders never fabricate on missing artifacts), standard-check OK=56 FAIL=0 挂点=40 条文=15 (red proof: deleting the D-7 mount row fails with 缺门挂点字段, exit 1). diff --git a/frontend/src/components/ResultTable.tsx b/frontend/src/components/ResultTable.tsx index 8075d45e..57d834a9 100644 --- a/frontend/src/components/ResultTable.tsx +++ b/frontend/src/components/ResultTable.tsx @@ -153,6 +153,26 @@ function VerdictCell({ summary }: { summary: ReturnType }) ); } +// scanner 厂商自报段标签(scanner_ranges 批,PR #216):标签列只显自报段声明源的 +// native_categories(「研究扫描 · X(自报段)」);sw_scanners 等计数型标签仅进展开面板, +// 不进标签列(噪声守门,2026-10-26 用户裁「多加一个标签」)。去重 + 封顶 3 溢出 +N。 +const VENDOR_TAG_SOURCE = "scanner_ranges"; +const VENDOR_TAG_CAP = 3; + +function scannerVendorTags(r: LookupResult): { label: string; source: string }[] { + const out: { label: string; source: string }[] = []; + for (const [type, ca] of Object.entries(r.classifications)) { + if (normType(type) !== "scanner" || !ca.detected) continue; + for (const d of ca.details) { + if (d.source !== VENDOR_TAG_SOURCE) continue; + for (const nc of d.native_categories ?? []) { + if (nc && !out.some((v) => v.label === nc)) out.push({ label: nc, source: d.source }); + } + } + } + return out; +} + // 标签列 = 该 IP 全部事实类徽章的家:威胁 chips 在前、资产 chips 在后、源数/存档/冲突殿后。 // CDN 徽章合并裁定(防同列重复):threat.is_cdn 且存在 cdn-role service chip 时,不再渲染 // 独立 CDN 徽章,该 chip 的 title 换成 cdn.notice(劝禁语义进 tooltip,provider 仍在 chip 文本); @@ -194,6 +214,23 @@ function ThreatTags({ ); })} + {(() => { + const vendors = scannerVendorTags(r); + const shown = vendors.slice(0, VENDOR_TAG_CAP); + const rest = vendors.length - shown.length; + if (shown.length === 0) return null; + return ( + <> + {shown.map((v) => ( + + {v.label} + + ))} + {rest > 0 && +{rest}} + + ); + })()} {badges.map((a, i) => ( { vi.unstubAllGlobals(); }); }); + +// --- scanner 厂商自报段标签(scanner_ranges native_categories → 标签列 chip)--- + +describe("ResultTable scanner vendor tag", () => { + const caScanner = (details: ClassificationAssessment["details"]): ClassificationAssessment => ({ + type: "scanner", verdict: "malicious", detected: true, confidence: 90, + algorithm: "logodds", corroborated: true, reporter_total: 0, + verdict_conflict: false, has_archive: false, malware_names: [], + details, sources: [], + }); + + it("renders the 自报段 vendor chip in the tags column (source in title)", () => { + const r: LookupResult = { ...baseResult, ip: "162.142.125.11", + classifications: { scanner: caScanner([ + { source: "scanner_ranges", reliability: 0.9, verdict: "malicious", + native_categories: ["研究扫描 · Censys(自报段)"] }, + { source: "sw_scanners", reliability: 0.55, verdict: "malicious" }, + ]) } }; + renderWithI18n(); + const tds = screen.getByText("162.142.125.11").closest("tr")!.querySelectorAll("td"); + const chip = within(tds[6]).getByText("研究扫描 · Censys(自报段)"); + expect(chip.getAttribute("title")).toContain("scanner_ranges"); + expect(within(tds[6]).getByText("Scanner")).toBeInTheDocument(); // 分类 chip 仍在 + }); + + it("does not surface native labels from other scanner sources (noise guard)", () => { + const r: LookupResult = { ...baseResult, ip: "40.124.175.29", + classifications: { scanner: caScanner([ + { source: "sw_scanners", reliability: 0.55, verdict: "malicious", + native_categories: ["12 · Stretchoid(3)"] }, + ]) } }; + renderWithI18n(); + const row = screen.getByText("40.124.175.29").closest("tr")!; + expect(within(row).getByText("Scanner")).toBeInTheDocument(); + expect(within(row).queryByText("12 · Stretchoid(3)")).not.toBeInTheDocument(); + }); + + it("renders no vendor chip when scanner_ranges has no native_categories", () => { + const r: LookupResult = { ...baseResult, ip: "203.0.113.77", + classifications: { scanner: caScanner([ + { source: "scanner_ranges", reliability: 0.9, verdict: "malicious" }, + ]) } }; + renderWithI18n(); + const row = screen.getByText("203.0.113.77").closest("tr")!; + expect(within(row).queryByText(/自报段/)).not.toBeInTheDocument(); + }); +});