open-krittKritt
§ 00 · Source-level Observatory

open-kritt 漏洞扫描 Agent 控制面

open-kritt 是一个开源代码漏洞发现平台:前端创建 scan,后端锁定 workflow/post-script/agent skill 配置,Python engine 把仓库复制到隔离 workspace,再通过 Codex、Claude Code、Cursor 等 harness 执行多步扫描和后处理。

★ 259
GitHub Stars
52.2K
LOC
183
Files
8h ago
Last commit
§ 00 · OVERVIEW

它解决什么,凭什么值得拆

open-kritt 最值得拆的点不是“让模型找漏洞”这句口号,而是它把扫描生命周期拆成可审计的状态机。入口侧的 backend/src/routes/scans.js:296 先验证模型、workflow、post-script 和 agent skill;运行侧的 backend/src/routes/scans.js:430 创建 scan 后交给 engine;界面侧的 frontend/src/pages/ScanDetail.jsx:66 和 frontend/src/pages/ScanDetail.jsx:74 持续轮询 scan 与 findings,形成用户能观察的闭环。

源码里更有价值的是约束边界。backend/src/routes/scans.js:356 用事务锁定配置资源,backend/src/routes/scans.js:405 从 workflow 与 post-script 推导必填 extra,frontend/src/pages/ScanDetail.jsx:137 到 frontend/src/pages/ScanDetail.jsx:160 用 per-record mutation queue 处理快速复核点击;这些设计说明它在做漏洞平台,而不是一次性 prompt demo。

▸核心数据

架构模式
前端 scan 控制台 + Express API + Python engine worker + 模型 harness + post-processing triage。
核心抽象
Scan、Workflow、Step metadata、DependencyWorkspace、HarnessResult、Vulnerability。
性能/精度数据
源码没有公布扫描吞吐基准;工程重点放在 worker cap、rate-limit retry、workspace cache 和 post-processing batch。
下游依赖
依赖 GitHub checkout、Prisma/Postgres、Codex/Claude/Cursor/OpenRouter、React scan UI。
本文导航 — 6 个章节依次:概述 → 架构图 → 核心模块 → 关键流程 → 测试与验证 → 风险。子页面在每个 H3 末尾以 深度阅读 → 入口展开。
§ 01 · ARCHITECTURE

三层控制面如何把漏洞扫描跑稳

系统拓扑可以从三个入口看:后端创建 scan,engine 消费 scan,前端展示 scan。backend/src/routes/scans.js:296 是创建入口,backend/src/routes/scans.js:468 是运行时状态修改入口,frontend/src/pages/ScanDetail.jsx:103 和 frontend/src/pages/ScanDetail.jsx:116 把这些能力映射成暂停、恢复、保存运行设置。

▸系统拓扑

[CreateScan UI] -> POST /api/scans -> lock workflow/post-scripts/agent-skills -> Scan(status=pending|queued) -> Worker.process_scan -> prepare workspace + render prompt -> model harness run -> validate structured output -> post-processing: dedupe -> rank -> post-script -> ScanDetail polling + human review

▸模块清单

四个模块最关键:API 层负责参数和资源锁,queue 层根据 workflow depth 生成 pending jobs,workspace 层复制隔离仓库和依赖,post-processing 层把模型输出变成可复核漏洞清单。backend/src/routes/scans.js:103、backend/src/routes/scans.js:120、frontend/src/lib/scanPresentation.js:29 分别说明配置锁、运行时更新和复核摘要不是 UI 附属品。

API 控制面
创建、暂停、恢复 scan,并在事务里锁定 workflow/post-script/agent skill。
Engine Worker
消费 scan 状态,生成 pending jobs,处理 rate limit 和 post-processing。
Review UI
轮询 scan 与 vulnerability,使用 mutation queue 避免快速复核覆盖。

▸关键数据结构

src/engine/open_kritt_engine/queue.py · L76-129Python / JavaScript
76def build_pending_jobs(
77    *,
78    scan: dict[str, Any],
79    workflow: Workflow,
80    completed: set[tuple[int, int, str | None, int]],
81    step_results: dict[tuple[int, int, str | None, int], list[StepResultRow]],
82    claimed: set[tuple[int, int, str | None, int]] | None = None,
83) -> list[Job]:
84    pending: list[Job] = []
85    if claimed is None:
86        claimed = completed
87    runs = repeat_runs(scan)
88    states = [State(prev_id=0, prev_table=None, repeat_run=1, context=scan_context(scan))]
89    previous_depth_complete = True
90
91    for depth in workflow.depths:
92        steps = workflow.steps_at_depth(depth)
93        next_states: list[State] = []
94        depth_complete = previous_depth_complete
95
96        if _depth_consumes_all(steps, depth):
97            # Do not start a batch until every branch in the previous depth has
98            # completed. Otherwise a concurrent worker could run it over a
99            # partial result set.
100            input_states = [_batch_state(scan, states, depth - 1)] if previous_depth_complete and states else []
101        else:
102            input_states = states
103
104        for state in input_states:
105            for step in steps:
106                task_complete = True
107                task_next_states: list[State] = []
108                for repeat_run in range(1, runs + 1):
109                    repeated_state = _state_for_repeat(state, repeat_run)
110                    key = metadata_key(step.id, repeated_state)
111                    if key not in completed:
112                        task_complete = False
113                        depth_complete = False
114                        if key not in claimed:
115                            pending.append(Job(step=step, state=repeated_state))
116                        # The next repeat needs the output from this one, and the
117                        # next depth needs the complete accumulated task output.
118                        break
119                    task_next_states.extend(_next_states(step, repeated_state, step_results))
120                if task_complete:
121                    next_states.extend(task_next_states)
122
123        states = next_states
124        previous_depth_complete = depth_complete
125
126    return sorted(
127        pending,
128        key=lambda job: (-job.depth, job.state.repeat_run, job.step.order, job.state.prev_id),
129    )
深度阅读 · Engine 扫描调度与 workspace 准备
§ 02 · CORE MODULES

扫描队列、workspace 与 harness 的核心模块

▸扫描创建路径:先锁配置,再落库

后端创建 scan 时不是直接 insert。backend/src/routes/scans.js:296 做入参验证,backend/src/routes/scans.js:356 开启事务,backend/src/routes/scans.js:397 到 backend/src/routes/scans.js:421 从 workflow/post-script 反推 extra 必填项,最后 backend/src/routes/scans.js:430 才创建 scan。这个顺序能避免扫描启动后才发现 prompt 变量缺失。

src/../backend/src/routes/scans.js · L296-365Python / JavaScript
296router.post('/', async (req, res, next) => {
297  try {
298    const valid = validateScan(req.body, { localNames: localRepoNames() });
299    await assertModelSelectionAvailable(valid);
300    const activeScanCount = await prisma.scan.count({ where: { status: { in: ACTIVE_SCAN_STATUSES } } });
301    const launchDecision = scanLaunchDecision(req.body, activeScanCount);
302    if (launchDecision.kind === 'choice-required') {
303      return res.status(409).json({
304        error: 'Another scan is running. Choose whether to start immediately or queue this scan.',
305        code: 'scan_launch_policy_required',
306        errors: [
307          {
308            field: 'launchPolicy',
309            message: 'Choose whether to start immediately or queue this scan.',
310          },
311        ],
312      });
313    }
314
315    const configurationObject =
316      valid.configuration && typeof valid.configuration === 'object' && !Array.isArray(valid.configuration)
317        ? valid.configuration
318        : {};
319    const configuredPostScriptIds = [
320      ...new Set(
321        [
322          `${valid.postScriptId}`,
323          ...(Array.isArray(configurationObject.post_script_ids)
324            ? configurationObject.post_script_ids
325            : Array.isArray(configurationObject.post_scripts)
326              ? configurationObject.post_scripts
327              : []
328          ).map((id) => `${id}`),
329        ].filter((id) => id.trim() !== '')
330      ),
331    ];
332    const invalidPostScriptIds = configuredPostScriptIds.filter((id) => !/^\d+$/.test(id));
333    const queryPostScriptIds = configuredPostScriptIds.filter((id) => /^\d+$/.test(id));
334    const requestedAgentSkills =
335      req.body?.agentSkillIds ??
336      req.body?.agent_skill_ids ??
337      configurationObject.agent_skill_ids ??
338      configurationObject.agent_skills ??
339      [];
340    const configuredAgentSkillIds = [
341      ...new Set(
342        (Array.isArray(requestedAgentSkills)
343          ? requestedAgentSkills
344          : typeof requestedAgentSkills === 'string'
345            ? requestedAgentSkills.split(',')
346            : []
347        )
348          .map((item) => (item && typeof item === 'object' ? item.id : item))
349          .map((id) => `${id}`.trim())
350          .filter(Boolean)
351      ),
352    ];
353    const invalidAgentSkillIds = configuredAgentSkillIds.filter((id) => !/^\d+$/.test(id));
354    const queryAgentSkillIds = configuredAgentSkillIds.filter((id) => /^\d+$/.test(id));
355
356    const created = await prisma.$transaction(async (tx) => {
357      await lockScanConfigurationResources(tx, {
358        workflowId: BigInt(valid.workflowId),
359        postScriptIds: queryPostScriptIds.map((id) => BigInt(id)),
360        agentSkillIds: queryAgentSkillIds.map((id) => BigInt(id)),
361      });
362      const [workflow, postScripts, agentSkills] = await Promise.all([
363        tx.workflow.findUnique({ where: { id: BigInt(valid.workflowId) } }),
364        tx.postScript.findMany({ where: { id: { in: queryPostScriptIds.map((id) => BigInt(id)) } } }),
365        tx.agentSkill.findMany({ where: { id: { in: queryAgentSkillIds.map((id) => BigInt(id)) } } }),

▸运行路径:pending job 与 harness 隔离执行

engine 在 backend/src/routes/scans.js:451 写入 pending 或 queued 后接管。运行时先用 workflow depth 生成 job,再用 workspace 和 harness 执行;前端 frontend/src/pages/ScanDetail.jsx:1174 到 frontend/src/pages/ScanDetail.jsx:1230 把 active jobs、attempts、failed attempts 展示给用户,避免长任务黑盒化。

src/engine/open_kritt_engine/worker.py · L821-910Python / JavaScript
821    def process_scan(self, scan: dict[str, Any], worker_id: int = 1) -> bool:
822        scan_id = int(scan["id"])
823        did_work = False
824        with self.db.connect() as conn:
825            workflow = self.db.load_workflow(conn, int(scan["workflow_id"]))
826
827        while True:
828            with self.db.connect() as conn:
829                current = self.db.load_scan(conn, scan_id)
830                if not current or current["status"] in NON_RUNNABLE_SCAN_STATUSES:
831                    return did_work
832                if not self._worker_can_pick_job(worker_id):
833                    return did_work
834                if current["status"] == PREWARMING_SCAN_STATUS:
835                    conn.commit()
836                    jobs = None
837                    completed = claimed = set()
838                elif current["status"] == "post_processing":
839                    conn.commit()
840                    jobs = None
841                    completed = claimed = set()
842                else:
843                    completed = self.db.load_completed_metadata(conn, scan_id)
844                    claimed = self.db.load_claimed_metadata(conn, scan_id)
845                    step_results = self.db.load_step_results(conn, scan_id)
846                    jobs = build_pending_jobs(
847                        scan=current,
848                        workflow=workflow,
849                        completed=completed,
850                        claimed=claimed,
851                        step_results=step_results,
852                    )
853                    conn.commit()
854
855            needs_new_container = current["status"] == "post_processing" or bool(jobs)
856            if needs_new_container and not self._new_scan_container_allowed(scan_id):
857                return did_work
858
859            harness = harness_for(
860                normalize_harness_name(current["harness"]),
861                timeout_seconds=self.runtime_harness_timeout_seconds(),
862                model_provider=scan_model_provider(current),
863                codex_model_provider=getattr(self.config, "codex_model_provider", None),
864                codex_cli_gate=self.codex_cli_gate,
865            )
866
867            if current["status"] == PREWARMING_SCAN_STATUS:
868                if self._ensure_scan_cache_prewarmed(current, restore_status="running"):
869                    did_work = True
870                    continue
871
872            if current["status"] != "post_processing" and jobs:
873                if self._ensure_scan_cache_prewarmed(current, restore_status="running"):
874                    did_work = True
875                    continue
876
877            if current["status"] == "post_processing":
878                if self._ensure_scan_cache_prewarmed(current, restore_status="post_processing"):
879                    did_work = True
880                    continue
881                if not self._worker_can_pick_job(worker_id):
882                    return did_work
883                did_post_work = self.post_processor.process_once(current, harness)
884                if did_post_work:
885                    return True
886                return did_work
887
888            if not jobs:
889                if claimed != completed:
890                    return did_work
891                with self.db.connect() as conn:
892                    transitioned = self.db.set_scan_status_if_current(conn, scan_id, "running", "post_processing")
893                    conn.commit()
894                if not transitioned:
895                    return did_work
896                LOGGER.info("scan %s workflow completed; starting post-processing", scan_id)
897                did_work = True
898                continue
899
900            did_claim = False
901            for job in jobs:
902                if not self._worker_can_pick_job(worker_id):
903                    return did_work
904                did_claim = self.execute_job(
905                    scan=current,
906                    workflow_id=workflow.id,
907                    job=job,
908                    harness=harness,
909                )
910                if did_claim:
深度阅读 · 后处理、去重排序与人工复核
§ 03 · KEY FLOW

从创建 scan 到 post-processing 的关键流

关键流里有两个容易被忽视的边界:scan 何时从 running 进入 post_processing,以及用户复核如何在轮询中保持最新写入。前者由 engine 的 job 完成情况驱动,后者由 frontend/src/pages/ScanDetail.jsx:137、frontend/src/pages/ScanDetail.jsx:145、frontend/src/pages/ScanDetail.jsx:150 到 frontend/src/pages/ScanDetail.jsx:160 的 mutation queue 覆盖处理。

▸扫描生命周期状态机

queued/pending -> prewarming_cache -> running -> post_processing -> completed | paused/stopped/failed rate_limited -> scheduled retry -> pending

▸复核与后处理流水线

raw vulnerabilities -> semantic dedupe batches -> bounty ranker batches -> post-script enrichments -> API serialize -> ScanDetail optimistic review
深度阅读 · 执行链逐行展开
§ 04 · TESTS & VALIDATION

哪些测试在守住工作流边界

测试覆盖能看出作者关注的是边界条件,而不是只测 happy path。backend/test/validation.test.js:46 统一 generation 与 scan 的模型选择规范,backend/test/validation.test.js:125 拒绝非字符串 CLI 参数,frontend/src/pages/WorkflowBuilder.test.js:78 和 frontend/src/pages/WorkflowBuilder.test.js:155 专门测 batch depth 引用重编号。

▸测试覆盖

这些测试覆盖模型选择、workflow output schema、批处理变量迁移和 scan presentation,能防止 Agent workflow 编辑器生成看似可保存、实际运行时变量错位的配置。frontend/src/pages/WorkflowBuilder.jsx:64 与 frontend/src/pages/WorkflowBuilder.jsx:78 对应插入深度时的变量迁移,frontend/src/pages/WorkflowBuilder.jsx:117 对应删除步骤时的迁移。

tests/validation.test.js · L46-123Python / JavaScript
46test('generation and scan model selections share normalization rules', () => {
47  const selection = validateModelSelection({
48    model: ' gpt-5.6 ',
49    model_provider: 'CODEX',
50    harness: 'codex-cli',
51    thinking_effort: 'high',
52  });
53  assert.deepEqual(selection, {
54    model: 'gpt-5.6',
55    modelProvider: 'codex',
56    harness: 'codex',
57    thinkingEffort: 'high',
58  });
59  assert.deepEqual(
60    validateGeneration({
61      kind: 'post_script',
62      request: '  Generate a triage summary. ',
63      model: 'gpt-5.6',
64      model_provider: 'codex',
65      harness: 'codex',
66      thinking_effort: 'high',
67    }),
68    {
69      kind: 'post_script',
70      request: 'Generate a triage summary.',
71      ...selection,
72    }
73  );
74
75  assert.throws(
76    () => validateGeneration({ kind: 'unknown', request: '', model: '', harness: '' }),
77    (error) =>
78      error instanceof ValidationError &&
79      ['kind', 'request', 'model', 'harness'].every((field) => error.errors.some((item) => item.field === field))
80  );
81
82  assert.throws(
83    () =>
84      validateGeneration({
85        kind: 'workflow',
86        request: 'x'.repeat(20_001),
87        model: 'gpt-5.6',
88        model_provider: 'codex',
89        harness: 'codex',
90      }),
91    (error) =>
92      error instanceof ValidationError &&
93      error.errors.some((item) => item.field === 'request' && item.message.includes('20,000'))
94  );
95});
96
97test('model selection accepts the provider effort union', () => {
98  for (const thinkingEffort of ['default', 'low', 'medium', 'high', 'xhigh', 'max', 'ultra']) {
99    assert.equal(
100      validateModelSelection({
101        model: 'custom-model',
102        model_provider: 'codex',
103        harness: 'codex',
104        thinking_effort: thinkingEffort,
105      }).thinkingEffort,
106      thinkingEffort
107    );
108  }
109  assert.throws(
110    () =>
111      validateModelSelection({
112        model: 'claude-fable-5',
113        model_provider: 'claude',
114        harness: 'claude-code',
115        thinking_effort: 'ultra',
116      }),
117    (error) =>
118      error instanceof ValidationError &&
119      error.errors.some(
120        (item) => item.field === 'thinking_effort' && item.message.includes('not supported by harness "claude-code"')
121      )
122  );
123});

▸已知边界 / 失败模式

  • 并发 scan:backend/src/routes/scans.js:300 根据 active scan 数决定立即启动或要求用户选择 queue。
  • 配置漂移:backend/src/routes/scans.js:103 在事务内锁住 workflow/post-script/agent skill,降低扫描中途配置被改的风险。
  • 快速复核点击:frontend/src/pages/ScanDetail.jsx:145 按 vulnerability id 入队,后台轮询不会直接覆盖 pending overlay。
§ 05 · HIGHLIGHTS

真正值得借鉴的工程亮点

事务化 scan 创建

scan 创建从 backend/src/routes/scans.js:296 开始,到 backend/src/routes/scans.js:356 进入事务,再通过 backend/src/routes/scans.js:397、backend/src/routes/scans.js:405、backend/src/routes/scans.js:430 串起配置锁、extra 校验和落库。这段设计的重点是把不可变运行配置一次性固化。

可观察复核 UI

前端在 frontend/src/pages/ScanDetail.jsx:66 与 frontend/src/pages/ScanDetail.jsx:74 双轮询 scan 和 findings,frontend/src/pages/ScanDetail.jsx:101 分页,frontend/src/lib/scanPresentation.js:29 提炼 post-script 摘要,让长时扫描有即时反馈。

src/frontend/src/lib/scanPresentation.js · L18-46JavaScript
18function resultSources(vulnerability) {
19  const sources = [];
20  const primary = vulnerability?.postScriptAnswer;
21  if (primary && typeof primary === 'object' && Object.keys(primary).length) sources.push(primary);
22  for (const enrichment of vulnerability?.enrichments || []) {
23    const result = enrichment?.result;
24    if (result && typeof result === 'object' && Object.keys(result).length) sources.push(result);
25  }
26  return sources;
27}
28
29export function postOutputSummary(vulnerability) {
30  const sources = resultSources(vulnerability);
31  const preferred = ['severity', 'ease_of_exploitability', 'patched', 'resource_exhaustion'];
32  for (const key of preferred) {
33    for (const result of sources) {
34      const value = result[key];
35      if (['string', 'number', 'boolean'].includes(typeof value)) return { label: key, value: String(value) };
36    }
37  }
38  for (const result of sources) {
39    for (const [key, value] of Object.entries(result)) {
40      if (key.startsWith('_reserved_') || key.startsWith('_chip_')) continue;
41      if (['string', 'number', 'boolean'].includes(typeof value)) return { label: key, value: String(value) };
42    }
43  }
44  return sources.length
45    ? { label: 'post-script', value: `${sources.length} output${sources.length === 1 ? '' : 's'}` }
46    : null;

这段展示层逻辑把 post-script 和 enrichment 输出合并成单个摘要,frontend/src/lib/scanPresentation.js:18 收集来源,frontend/src/lib/scanPresentation.js:31 优先 severity/ease/patched 等字段,frontend/src/lib/scanPresentation.js:39 再回退到任意可显示字段。对安全扫描平台来说,这是把模型附加判断变成可扫读 UI 的关键小抽象。

§ 06 · RISKS & GAPS

技术亮点之外,别忽略这些

open-kritt 的风险主要来自三处:模型 harness 是外部进程,workspace 会复制用户仓库,post-processing 又会继续调用模型做去重和排序。backend/src/routes/vulnerabilities.js:34 的人工复核接口很克制,但 engine 的运行成本、凭据隔离和错误恢复仍是上线时要重点压测的部分。

凭据与 workspace 隔离成本

workspace 准备会创建 job home、复制凭据、设置 HOME/CODEX_HOME/CLAUDE_HOME。虽然 backend/src/routes/scans.js:423 记录 dependenciesDetail,frontend/src/pages/ScanDetail.jsx:253 展示 commit,但真正的安全边界在 engine 侧,需要部署者认真配置容器和 token 权限。

后处理仍会消耗模型额度

post-processing 不是免费整理,它会继续做 dedupe、ranker、post-script。前端 frontend/src/pages/ScanDetail.jsx:206 展示 rate limit,frontend/src/pages/ScanDetail.jsx:1176 标记 rate_limited,但团队仍需要设置 worker cap 和重试策略。

src/frontend/src/pages/WorkflowBuilder.jsx · L64-107JavaScript
64function shiftBatchDepthReferences(content, depthMap) {
65  if (typeof content !== 'string' || depthMap.size === 0) return content;
66  return content.replace(BATCH_DEPTH_REFERENCE, (reference, opening, rawDepth, closing) => {
67    const nextDepth = depthMap.get(Number(rawDepth));
68    return nextDepth === undefined ? reference : `${opening}${multiOutputDepthKey(nextDepth)}${closing}`;
69  });
70}
71
72/**
73 * Insert a blank workflow level immediately before an existing depth. Existing
74 * levels at and below that point move down together. Batch variables contain
75 * their source depth in the key, so valid references to moved batch boundaries
76 * are renumbered at the same time.
77 */
78export function insertWorkflowDepthBefore(builder, depth, newStepId) {
79  if (!builder || !Array.isArray(builder.levels)) throw new TypeError('A workflow builder is required.');
80  if (!Number.isInteger(depth) || !builder.levels.some((level) => level.depth === depth)) {
81    throw new RangeError('Choose an existing workflow depth.');
82  }
83  if (!newStepId) throw new TypeError('A new step id is required.');
84
85  const next = JSON.parse(JSON.stringify(builder));
86  const batchDepthMap = new Map(
87    next.levels
88      .filter((level) => level.depth > 0 && level.consumesAll && level.depth >= depth)
89      .map((level) => [level.depth - 1, level.depth])
90  );
91
92  next.levels.forEach((level) => {
93    level.steps.forEach((step) => {
94      step.content = shiftBatchDepthReferences(step.content, batchDepthMap);
95    });
96    if (level.depth >= depth) level.depth += 1;
97  });
98  next.levels.push({
99    depth,
100    multiOutput: true,
101    consumesAll: false,
102    schema: [],
103    steps: [{ id: newStepId, name: 'New step', content: '' }],
104  });
105  next.levels.sort((left, right) => left.depth - right.depth);
106  next.selStepId = newStepId;
107  return next;

WorkflowBuilder 的批处理变量迁移说明作者知道 workflow 是可编辑图,而不是静态 prompt。frontend/src/pages/WorkflowBuilder.jsx:64 只在 depth map 存在时替换,frontend/src/pages/WorkflowBuilder.jsx:78 插入新层,frontend/src/pages/WorkflowBuilder.jsx:92 遍历每个 step 内容,frontend/src/pages/WorkflowBuilder.jsx:98 再插入新 step;这个路径仍需要更多端到端测试覆盖真实复杂 workflow。

诚实记录 — 这部分列的是我在源码里看到、但作者没有明确文档化的限制。读者请按需取舍。
§ 07 · EVIDENCE

源码证据,串起来看

▸API 到 UI 的证据链

从接口到界面是一条连续链:backend/src/routes/scans.js:204 列表 API 提供 runningCount,backend/src/routes/scans.js:243 读取单个 scan,backend/src/routes/scans.js:254 拉取 ranked findings,frontend/src/pages/ScanDetail.jsx:66 和 frontend/src/pages/ScanDetail.jsx:74 持续轮询,frontend/src/pages/ScanDetail.jsx:101 做 findings 分页,frontend/src/pages/ScanDetail.jsx:121 负责删除确认。它说明 open-kritt 把长任务的进度、结果和人工复核都放在同一个可观察页面。

运行设置也不是前端随便拼 JSON:frontend/src/pages/ScanDetail.jsx:1070 生成 draft,frontend/src/pages/ScanDetail.jsx:1086 合并 patch,frontend/src/pages/ScanDetail.jsx:1103 只提交变化字段,后端 backend/src/routes/scans.js:120 重新验证运行时设置,backend/src/routes/scans.js:143 上锁后 patch scan。这个设计让用户能修改模型和 harness,但不会绕过服务端可用性检查。

人工复核的最小写接口也很清晰:backend/src/routes/vulnerabilities.js:34 接收 PATCH,backend/src/routes/vulnerabilities.js:42 校验 interesting 只能是 1、0 或 null,backend/src/routes/vulnerabilities.js:51 处理 comments,backend/src/routes/vulnerabilities.js:58 只返回被允许的字段。前端 frontend/src/pages/ScanDetail.jsx:139 到 frontend/src/pages/ScanDetail.jsx:160 负责乐观更新和失败回滚。

src/backend/src/routes/vulnerabilities.js · L32-68JavaScript
32// PATCH /api/vulnerabilities/:id — user review: interesting flag and/or comments.
33// interesting: 1 (interesting), 0 (not interesting), or null (unmarked).
34router.patch('/:id', async (req, res, next) => {
35  try {
36    const id = BigInt(req.params.id);
37    const existing = await prisma.vulnerability.findUnique({ where: { id }, select: { id: true } });
38    if (!existing) return res.status(404).json({ error: 'Vulnerability not found.' });
39
40    const body = req.body || {};
41    const data = {};
42    if ('interesting' in body) {
43      const val = body.interesting;
44      if (val === null) data.interesting = null;
45      else if (val === 0 || val === 1 || val === '0' || val === '1') data.interesting = BigInt(Number(val));
46      else
47        return res
48          .status(422)
49          .json({ errors: [{ field: 'interesting', message: 'interesting must be 0, 1, or null.' }] });
50    }
51    if ('comments' in body) {
52      data.comments = body.comments === null || body.comments === '' ? null : String(body.comments);
53    }
54    if (Object.keys(data).length === 0) {
55      return res.status(422).json({ errors: [{ field: 'body', message: 'Provide interesting and/or comments.' }] });
56    }
57
58    const updated = await prisma.vulnerability.update({
59      where: { id },
60      data,
61      select: { id: true, interesting: true, comments: true },
62    });
63    res.json({
64      id: updated.id.toString(),
65      interesting:
66        updated.interesting === null || updated.interesting === undefined ? null : Number(updated.interesting),
67      comments: updated.comments ?? null,
68    });

▸工作流编辑器的变量安全

workflow 编辑器要守住 batch depth 变量,否则模型输入会在深层 workflow 中错位。实现里 frontend/src/pages/WorkflowBuilder.jsx:62 定义 batch 引用正则,frontend/src/pages/WorkflowBuilder.jsx:64 迁移引用,frontend/src/pages/WorkflowBuilder.jsx:117 删除 step,frontend/src/pages/WorkflowBuilder.jsx:131 判断是否删除整层,frontend/src/pages/WorkflowBuilder.jsx:132 到 frontend/src/pages/WorkflowBuilder.jsx:139 重建 depth map。对应测试 frontend/src/pages/WorkflowBuilder.test.js:78 和 frontend/src/pages/WorkflowBuilder.test.js:155 覆盖插入和删除两条路径。

这些证据也暴露一个限制:backend/test/validation.test.js:173 验证 terminal vulnerability 字段,backend/test/validation.test.js:177 枚举非法类型,backend/test/validation.test.js:46 确认 generation 与 scan 共享模型选择规范;但 engine 级别的真实 harness 失败、workspace 权限和 post-processing retry 更依赖集成测试。