Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/components/ResultTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,26 @@ function VerdictCell({ summary }: { summary: ReturnType<typeof threatSummary> })
);
}

// 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 文本);
Expand Down Expand Up @@ -194,6 +214,23 @@ function ThreatTags({
</Tooltip>
);
})}
{(() => {
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) => (
<span key={`vendor-${v.label}`} title={v.source}
className="rounded bg-orange-500/10 px-1.5 py-0.5 text-[11px] text-orange-300 ring-1 ring-orange-500/20">
{v.label}
</span>
))}
{rest > 0 && <span className="text-[10px] text-zinc-500">+{rest}</span>}
</>
);
})()}
{badges.map((a, i) => (
<span key={`asset-${a.key}`}
className={`${i === 0 ? "" : "ml-1.5"} rounded px-1.5 py-0.5 text-[11px] bg-sky-500/12 text-sky-400 ring-1 ring-sky-500/20`}
Expand Down
47 changes: 47 additions & 0 deletions frontend/src/components/__tests__/ResultTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -382,3 +382,50 @@ describe("ResultTable STIX export (fetch+blob, Q1-B companion)", () => {
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(<ResultTable results={[r]} />);
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(<ResultTable results={[r]} />);
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(<ResultTable results={[r]} />);
const row = screen.getByText("203.0.113.77").closest("tr")!;
expect(within(row).queryByText(/自报段/)).not.toBeInTheDocument();
});
});
Loading