Commit Graph
474 Commits
Author SHA1 Message Date
BaaRai 3eebf02596 Merge pull request #12 from BaaRai/bot/issue-pr-bot
Add triage and reply bot for issues and pull requests
2026-10-10 18:18:20 +02:00
BaaRai 815a12d91c Merge pull request #11 from BaaRai/ci/add-ci
Add CI: go build/vet/test with postgres, web typecheck and static build
2026-10-10 18:18:16 +02:00
BaaRai 525ad9ffde Skip the four known-broken tests in code instead of CI -skip flags
The go test -skip flag proved unreliable in the full-suite CI shape
(the same pattern skipped correctly in isolation but ran on the
runner), so each of the four tests now carries an explicit t.Skip with
the reason and a pointer to issue #9. The CI workflow drops the -skip
argument and keeps serial package execution.
2026-10-10 18:12:42 +02:00
BaaRai 78a2d2c01f ci: serialize test packages against the shared postgres
The rotating agent-package failures were cross-package database
interference: go test runs package binaries in parallel and they all
share the one postgres service, so fixtures delete each other's rows.
-p 1 makes package execution serial and the database deterministic.
2026-10-10 17:50:07 +02:00
BaaRai 24e40338b1 ci: fix skip typo, add the CI-flaky llmrec test to the list
First CI run caught a typo in the skip list (the test name is
TestGraphOverviewExpands, with a d) and exposed that
TestSideUsageRecordedOnceOnConsumerCancellation is timing-sensitive
on 2-core runners: it passes locally, fails on CI. Both noted in #9.
2026-10-10 17:44:57 +02:00
BaaRai 88dbe0cb66 Add triage and reply bot for issues and pull requests
Closes #10. A GitHub Action bot with no server: one triage comment per
new issue (classification, missing-info list, grounded first answer),
answers comments that @artex-bot, and a first-look summary checklist on
opened pull requests. Comment-only, never an approving review. Skips
the maintainer, bots, and itself; exits cleanly without the API key;
all model inputs are size-capped.
2026-10-10 17:38:07 +02:00
BaaRai 74e85bc945 Add CI: go build/vet/test with postgres, web typecheck and static build
Closes #9. Go job runs the test suite against a postgres:16 service
container, skipping the three tests that fail on unmodified upstream
code with a fresh database (documented in the issue). Web job runs
tsc --noEmit and the static export build.
2026-10-10 17:36:38 +02:00
BaaRai 858021252e Merge pull request #7 from BaaRai/fix/credential-username-placeholder
Clarify credentials username placeholder
2026-10-09 15:17:24 +02:00
BaaRai 39e630f3fb Clarify credentials username placeholder
Bug bounty alias accounts are nearly always emails, so a bare
"username" placeholder read like a handle only. Say "email or
username" in both the creation dialog and the task detail tab.
The agent tool description already said "login name / email used".
2026-10-09 15:17:12 +02:00
BaaRai ea933902c9 Merge pull request #6 from BaaRai/fix/credentials-ui-regression
Fix credentials UI regression from #4
2026-10-09 14:46:17 +02:00
BaaRai d3e6f4167e Fix credentials UI regression from #4
The test accounts section was mounted after the scrollable field area
of the New task sheet, so it sat pinned under the scrollbar instead of
scrolling with the other fields, and its Add account button floated in
a header row instead of sitting in the field content like Add row.

Move it inside the scroll container as a proper Field with the add
button below the rows, matching the intercept rules editor pattern.
Also place the credentials TabsContent after assets in the task detail
page to match the TABS array order and use the local id variable.
2026-10-09 14:45:52 +02:00
BaaRai 6c7169565b Merge pull request #4 from BaaRai/feat/task-credentials
feat(tasks): per-task test-account credentials
2026-10-09 14:34:15 +02:00
BaaRai c177ab9d86 feat(tasks): per-task test-account credentials
Task credentials (#3): declare test accounts at task creation, and never
lose the accounts the agent registers on its own.

- task_credentials table (unique per task+domain+username; upsert
  overwrites the password in place), idempotent schema migration
- POST /api/tasks accepts credentials; GET/POST/DELETE
  /api/tasks/{id}/credentials serve the UI (manual add, e.g. relaying an
  OTP-verified account; delete)
- empty password = the "agent generates" mode: the entry surfaces in the
  worker's graph overview with an explicit instruction to register that
  account with a strong password and record it
- new tools save_task_credential / list_task_credentials for worker and
  pentest; the graph overview injects the task's credentials each round
  with a standing note: reuse matching entries, always record accounts
  you register
- New Task dialog: optional credentials section (domain, username,
  password, note) with a dedicated-test-accounts hint
- task detail: Credentials tab, passwords masked with explicit
  reveal/copy, agent-recorded badge, light polling so mid-run recordings
  appear

One-shot binding migration (settings-flag-guarded, same pattern as the
other seeds) grants the two tools to worker+pentest on databases seeded
before this change; fresh DBs get them via WorkerTools()/the pentest
catalog.

Closes #3
2026-10-09 14:28:08 +02:00
BaaRai b28829e36d Merge pull request #2 from BaaRai/fix/pause-preserves-timeout-budget
fix(tasks): freeze the timeout budget while a task is paused
2026-10-09 11:46:18 +02:00
BaaRai 353a947285 fix(tasks): freeze the timeout budget while a task is paused
deadline_at is a wall-clock timestamp, so pause time counted against the
task timeout: pausing overnight (or shutting the machine down while
paused) made the deadline ring while parked, and resuming landed
straight into the settle sequence and a timeout terminal state.

ApplyTaskPause now stamps paused_at, and ApplyTaskAdmission credits the
paused interval back onto deadline_at in the same atomic UPDATE that
ends the pause (paused_at is cleared there too). The shifted deadline
flows to the live handle via the existing RETURNING mirror, so the
engine clock context picks it up. Matches the re-basing the
archive-restore path already did for paused tasks.

Migration is an idempotent ADD COLUMN; tasks paused before the upgrade
have no paused_at and keep the old behavior.

Fixes #1
2026-10-09 11:37:30 +02:00
BaaRai 0aa837149d README: note this is a private English fork of Autumn-27's ARTEX 2026-10-08 23:22:05 +02:00
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00
Autumn-27 d1196853dc docs(changelog): 定版 0.3.15 2026-10-07 08:54:02 -04:00
Autumn bd4a6e31e8 Merge pull request #197 from Autumn-27/fix/auth-init-fail-open
fix: 密码初始化/登录接口在数据库读失败时 fail closed
2026-10-07 19:47:48 +08:00
Autumn-27 66b7006c69 fix bug 2026-10-07 07:45:28 -04:00
Autumn f4aea9859d Merge pull request #191 from Autumn-27/feat/judge-token-usage
feat(intercept): 模型兜底审批单独计量 token 并在配置页展示
2026-10-04 14:43:54 +08:00
Autumn-27 8bc4d58c85 feat(intercept): 模型兜底审批单独计量 token 并在配置页展示
兜底审批(LLM judge)调模型判定漏网命令,其 token 消耗此前无法单独查看:
usage 行要么无 worker 归属、要么被并进复用的模型 profile 桶里。

- llmrec 增加 WithWorker 上下文覆盖,reviewer 调用前标注 worker='judge',
  让审批调用的计量行可被单独筛出(复用现有 llm_usage.worker 列,无需迁移)。
- db.JudgeUsageStats 返回累计总量 + 近 N 天每日序列(WHERE worker='judge')。
- 新增 GET /api/intercept/judge/usage。
- 配置页「模型兜底审批」开关下方新增用量卡片:调用次数 / 输入 / 输出 /
  缓存读写 五项统计 + 近 30 天每日消耗迷你柱图 + 刷新。
2026-10-04 02:43:26 -04:00
Autumn 2bd1dab7c7 Merge pull request #189 from Autumn-27/fix/sse-same-origin
fix(web): SSE 活动流默认同源,修复反代部署连不上 :8787
2026-10-03 08:10:35 +08:00
Autumn-27 6239c46c12 fix(web): SSE 活动流默认同源,修复反代部署连不上 :8787
sseUrl() 原先无条件把 SSE 地址拼成 ${hostname}:8787,连生产静态导出
也带这个硬编码。反代到 443 的部署里页面从 https://域名/ 加载,SSE 却被
指向 https://域名:8787/...,而 8787 未对公网开放,导致活动流连不上、反复重连。

改为用 NODE_ENV 区分:生产默认同源(相对路径),仅 next dev 保留 :8787
以绕过 Next.js /api 反代对 SSE 的缓冲。NEXT_PUBLIC_SSE_BASE 覆盖口保留。
验证:NEXT_EXPORT=1 next build 后生产包里 :8787 分支已被 tree-shake。

补充 README 反向代理部署文档(同源说明 + nginx 关缓冲配置)。

Closes #169
2026-10-02 20:10:08 -04:00
Autumn 9ff7e6f5e4 Merge pull request #187 from Autumn-27/fix/notify-digest-and-mask
fix(notify): 汇总模式退化、目的地留空误判、SMTP 缺 SSRF 守卫
2026-10-01 19:00:43 +08:00
Autumn 82a2567cec Merge pull request #185 from lucksec/feat/vuln-im-push
feat(notify): 漏洞发现支持推送到钉钉/飞书/企业微信等六种渠道
2026-10-01 18:58:50 +08:00
Autumn-27 4c8273d9f8 fix(notify): 邮件渠道的 SMTP 拨号补上 SSRF 守卫
emailDial 用的是裸 net.Dialer,没挂 blockInternalDial,Validate 也只检查
host 非空。于是 SMTP 是整套 SSRF 防护里唯一的缺口:host 填 169.254.169.254
或 127.0.0.1 能直接连上,而 smtp.NewClient 握手失败时会把对端返回的那一行
(textproto 的 short response)包进错误、经 last_error 由投递历史接口回显,
正是其它渠道已经关掉的半盲读原语;「连接被拒 vs 超时」的耗时差异还能
用来探测端口。

拨号阶段是最终生效点,也覆盖 DNS 重绑定,与 HTTP 系渠道共用同一道守卫。
RFC1918 私网继续放行,与 isBlockedDialIP 既有的取舍一致——内网自建 SMTP
中继是常见的合法部署。

这个缺口没有测试会发现:本包 TestMain 全局打开了 ARTEX_NOTIFY_ALLOW_LOCAL,
邮件用例无论守卫在不在都会通过。新增的两条用例自己清掉该变量,
并配一条反向用例确认显式放开后本机 SMTP 仍可投递。
2026-10-01 06:43:48 -04:00
Autumn-27 b658591f06 fix(notify): 目的地字段留空不应被判成地址变更
Telegram 渠道从第二次保存起会永久失败,用户什么都没改也存不上:

  新建时 base_url 留空(用官方 API 地址),创建路径直接存下 base_url:""
  → 第一次保存,MergeConfig 把空串当显式清空、delete 掉该键
  → 第二次保存,前端照常提交 base_url:"",而 stored 里已经没这个键
  → sameConfigValue("", nil) 比较 `""` 与 `null`,判为不等 → changed=[base_url]
  → bot_token 是掩码回显值 → 400「目标地址已变更,请同时重新填写凭据字段」

此后每次保存都失败,除非重新粘贴一遍 Bot Token,而错误提示还把人
往「凭据有问题」上引。

根因是 MergeConfig 与 sameConfigValue 对「空」的口径不一致:前者认为
空串就是「该删掉这个键」,后者认为 "" 与键不存在是两个不同的值。
现在比较前先归一化,口径与 MergeConfig 的清空判定对齐。

这是上一轮修「改目标地址、保留凭据」那条凭据外发路径时引入的回归——
守卫本身是对的,只是把一个没变的空字段误判成了地址变更。配套用例覆盖了
两个方向的真实变更(空→自建反代、自建反代→空)仍必须要求重填 Token,
确认防护没有被削弱。
2026-10-01 06:43:48 -04:00
Autumn-27 9b88c55af0 fix(notify): 汇总模式按消息计量限流,批次大小不再被每轮请求预算掐死
令牌桶的额度此前被同时当成两个量纲用:对实时模式它是「发几条消息」,
对汇总模式却被直接当成「一批装几条漏洞」传给 ClaimDigestBatch。

notifyMaxSendsPerChannelPerTick(=5)是由租约时长倒推的 HTTP 请求预算,
而一个汇总批次只发一次请求,两者本不该相乘。稳态下更糟:rate_per_min=20
的渠道在 3 秒的 tick 里只补得到 1 个令牌,于是每条汇总消息只装 1 个漏洞,
头部还写着「近 30 分钟新增 1 个漏洞」——digest 退化成带汇总文案的实时推送,
MaxDigestBatchSize(500) 永不可达。

改为统一按**消息条数**计量:一批漏洞合成一条消息、消耗一个令牌,
rate_per_min 对 digest 依然生效(每分钟最多这么多条汇总消息),
而批次大小只受 MaxDigestBatchSize 这个内存上界约束。

ClaimDigestBatch 原先的注释声称「单批同时受限流额度与内存上界约束」,
那正是本问题的来源——限流额度本不该充当批次大小。注释一并改掉,
免得下次又按「rate_per_min 对 digest 不生效」把它改回去。

顺带让 takeTokens 接受 want 上限:原先整桶抽空后才由调用方截断,
多出来的令牌(飞书默认 100/min 时是 95 个)直接丢弃,渠道这一轮
没有待发投递时也照扣,注释声称的突发容量在任何情况下都做不到。

决策收在 digestTickPlan 里由新增用例直接钉住。现有端到端用例都手动给
stepDigest 传一个够大的 limit,绕过了 step 里的额度计算,所以覆盖不到这里。
2026-10-01 06:43:29 -04:00
luckoneandClaude Code ee383aa1a9 fix(notify): 复测判定「已修复」时也要推送状态变更
复测结论为 fixed 时,db/finding_retests.go 里是裸的
`UPDATE findings SET status=...`,绕过了带通知的版本——于是配了
on_status_change 的渠道对这种状态流转完全收不到推送:状态在界面上悄悄变了,
运维要打开平台才知道。

把「更新状态 + 登记状态变更事件」抽成事务级的 SetFindingStatusTx,供
patchFinding 与复测路径共用,两者的语义(状态没变就不登记、快照带全渲染字段)
由同一份代码保证,不会再出现只改一处的情况。

附回归用例:复测判 fixed 后必须有一条 finding_status_changed 事件,
且快照里的 from/to 与渲染字段正确。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:25:06 +08:00
luckoneandClaude Code 311d9d85cf refactor(web): 通知页拆分为组件,1095 行降到 605 行
页面此前接近 1100 行,超过 CLAUDE.md 的 800 行硬上限。按项目的 _components/
约定拆开,每个文件都能单独读懂:

  _components/channel-fields.ts   渠道字段表与配置值解析(纯数据,无视图)
  _components/channel-form.tsx    按字段定义渲染控件的表单
  _components/delivery-list.tsx   投递记录表与筛选
  _components/stat-tile.tsx       概览统计卡片
  page.tsx                        只负责编排:加载、表单状态、调接口

拆分是纯模块搬移,未改行为;tsc 与 next build 均通过,/system/notify 仍正常
预渲染。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:25:06 +08:00
luckoneandClaude Code 765c0e99e3 fix(notify): 安全审计修复 —— 掩码绕过、SSRF、静默丢失等 12 项
对推送功能做了一轮安全审计,每条都先复现、再修、再补回归测试。
以下按严重程度排列。

【严重】掩码可被「改目标地址、保留凭据」绕过
目标地址与凭据是两套独立字段,而配置合并对「未提及的键」保留原值。于是
只改地址、对凭据避而不谈,服务端就会把库里的真凭据发到任意地址。已实测
复现四个渠道:通用 Webhook 的 Authorization 头、Telegram 的 Bot Token
(进请求路径)、邮件的 SMTP 密码(STARTTLS 后交给对端)、钉钉/飞书的加签
密钥(该项不可利用,一并列出仅为说明面)。完全静默、不依赖重定向,所以
上一轮加的「拒绝跨主机跳转」挡不住它。

修法:新增 Channel.DestinationKeys() 声明「消息发往哪」,PrepareConfigUpdate
在地址变化时要求对**每一个**凭据字段显式表态(给新值,或显式留空表示不再
需要)——原样回传掩码值等于「沿用旧凭据」,正是攻击形状,一并拒绝。
刻意不做「自动丢弃凭据」:对可选字段(如 headers)那会变成「鉴权静默没了
但接口返回成功」,比报错更难排查。

【中】markdown 系三渠道对不可信内容零转义
钉钉/企微/飞书此前完全不转义(Telegram 与邮件都做了),而标题与摘要来自
模型输出、资产 url 来自被测目标。一条标题里的 `[文字](http://evil)` 会在
工程师的钉钉里变成可点击外链;`![](http://evil/beacon)` 会被客户端拉取,
泄露阅读者 IP 并暴露「这条已被看过」。现统一单行化 + markdown 元字符转义。

修的过程中还纠了自己一个设计错误:转义一度被放进 markdownTitle,而它被
markdown 正文、Telegram 的 HTML、飞书卡片、Webhook 的 JSON 与邮件主题四种
语境共用,markdown 的反斜杠泄漏到了 Telegram 消息里(用户会看到 `\(1\)`)。
转义必须发生在各自的渲染出口——测试直接抓到了这个回归。

【中】投递地址的 SSRF 面
此前只校验 scheme 与 host,云元数据 169.254.169.254、环回、内网一律可投递;
而失败响应体前 200 字节会进 last_error 并被投递历史接口回显,构成半盲读
原语。现在在**拨号阶段**设防(最终生效点,同时覆盖 DNS 重绑定与同主机
重定向)。环回/链路本地需显式 ARTEX_NOTIFY_ALLOW_LOCAL=1 才放行(本机
SMTP 中继是合法配置);RFC1918 私网**刻意放行**——内网自建 Mattermost /
SMTP 很常见,做到那个程度会把正常部署一起废掉,这条取舍写进了测试断言。

【中】汇总消息被截断后整批标记已送达 = 静默丢失
渠道都有长度上限(企微 4096 字节最紧)。此前渲染完整篇再截断、然后整批
标记成功:被截掉的那些既不在消息里、也不在失败列表里,投递历史还显示
成功。现改为按**整条**打包,只标记真正装下的,其余回队续发,消息头部
如实写明还有多少条在下一条。被推迟的条目**不消耗重试次数**(领取时乐观
+1 的那次减回去),否则 500 条的积压会在第三段就把尾部判成失败。

【中】其余修复
- min_severity 打错字让过滤器静默失效(未知级别序数为 0 → rank>=0 恒真,
  用户以为「仅高危」实际全推)。改为写入时校验取值。
- rate_per_min=0(不限流)不可达:文档/UI/令牌桶都按 0=不限流解释,唯独
  写库那层 `if <=0 取默认值` 把它改成 20 或 100。默认值改到接口层填。
- digest 渠道完全绕过令牌桶(allow 被扣却没人用)。
- 失败处置按整批 max(attempts) 判断,让老投递连坐同批的新投递。
- 汇总批次里快照无法解析的投递被静默标记成功。
- url.Parse 失败时错误文本仍带完整地址(上一轮只修了 http.Client.Do 那侧;
  且当时的用例全走 scheme 分支、没覆盖解析失败路径,是假保证)。
- 单渠道每轮投递条数可能超出租约时长(多实例下重复发送)。加了防漂移
  断言,写的时候立刻发现原取值 6 恰好用满租约、零余量,已改 5。
- Telegram 截断可能切断 HTML 实体,导致整条被拒收。
- 邮件把 SMTP 4xx(灰名单等临时失败)判成永久失败,最该重试的场景从不重试。
- 提交嵌套结构时会把掩码字面量真的写进库,导致鉴权静默失效。

测试:notify 包从 82% 提到 86%,新增 SSRF 守卫、掩码绕过(用攻击形状的
输入而非「防御逻辑的正确输入」)、分段投递收敛、markdown 转义、SMTP 4xx/5xx
分类等回归用例;端到端补了一条「60 条长标题跨多轮发完且不卡死」的收敛测试。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:25:06 +08:00
luckoneandClaude Code 4eb5311998 feat(notify): 漏洞发现支持推送到钉钉/飞书/企业微信等六种渠道
新增「系统 → 通知推送」页,把扫描发现的漏洞推送到钉钉、飞书、企业微信、
通用 Webhook、Telegram、邮件。同一类型可配任意多个机器人实例,每个实例独立
设启停、限流与过滤规则(最低级别 / 任务资产范围 / 漏洞类型关键词 / 是否接收
处置状态变更)。推送时机分实时与汇总两种模式,「高危实时、其余汇总」建两个
渠道即可表达,策略不写死在代码里。带投递历史与失败重发。

三个关键设计:

1. 写漏洞的事务只做一次盲 INSERT。notification_events 由 RecordFindingTx
   在同一事务内写入,保证「漏洞落库」与「推送任务存在」原子一致;这条 INSERT
   刻意不读渠道表、不跑用户的过滤规则,否则一条配错的过滤条件就能污染甚至
   中止事务,让高危漏洞存不进库。为把失败隔离在这一条语句上(PostgreSQL 中
   事务内任一语句报错会让整个事务作废、连 COMMIT 都失败),它被 SAVEPOINT
   包住,失败时只记日志。

2. 投递用租约领取而非长事务。FOR UPDATE SKIP LOCKED 领取后把行置为 sending、
   把 next_attempt_at 推到未来作为租约,提交后再做网络投递,因此投递期间不
   持有数据库锁;崩溃留下的 sending 行会被下一轮重新领取,自愈且不会无限重试。

3. 限流不消耗重试预算。先按渠道令牌桶算出本轮还能发几条,再按这个数量领取;
   顺序反过来会让被限流挡下的投递白计一次尝试,三次预算被等待耗光后落入失败。

审计并修复的四个问题:

- 推送凭据经错误信息外泄:http.Client.Do 返回的 *url.Error 会把完整 URL 打进
  错误文本,而钉钉 access_token、企业微信 key、飞书 hook id、Telegram
  /bot<token>/ 都就在 URL 里。该串流入 last_error(明文落库)、投递历史接口
  (绕过渠道掩码)、服务端日志与前端错误提示。现统一脱敏为 scheme://host
  + 底层原因,并拒绝跨主机重定向。
- 最低级别门槛打错字会让过滤器静默失效:未知级别序数为 0,判定退化成
  rank >= 0 恒真,用户以为限制「仅高危」实际全推。现于写入路径校验取值。
- 汇总批次无上限:一次领取把全部待发行读进内存、消息被渠道长度上限截掉大半
  且静默丢失。现单批上限 500 条,超出部分留库成为下一批。
- 邮件渠道把 SMTP 4xx(灰名单等临时失败)判成永久失败,导致这类最该重试的
  场景从不重试。现按应答码首位区分 4xx/5xx。

测试:notify 包 82% 覆盖(含用自建最小 SMTP 服务器驱动的完整邮件协议会话、
两种签名算法对 OpenSSL 基准值的比对、UTF-8 边界截断、以及模板能力边界的
反射断言);db 与 server 侧共 29 个用例跑真实 PostgreSQL,其中端到端用例
真发 HTTP 到假接收端并断言消息内容与投递状态。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:25:06 +08:00
luckoneandClaude Code e2679dac2d fix(db): 修正公司 ICP 归属用例连接关闭顺序导致的资产残留
清理注册在 t.Cleanup 里,而连接关闭用的是 defer d.Close()。defer 在函数
返回时先执行、t.Cleanup 在其后,于是两条 DELETE 全部落在**已关闭的连接**
上,错误又被 `_, _ =` 丢弃,测试资产与公司因此永久残留在库里。

该用例用 MAX(companies.id)+1 当假 TaskID 给资产打标。残留本身不会立刻报错,
但一旦这个数字与其他用例的任务 id 相撞,那个按「恰好 N 个资产」断言的用例
就会失败,而现象(某个无关测试的资产计数不对)与原因(另一个测试的连接
关闭顺序)相距极远,排查成本很高。

改为关闭连接也走 t.Cleanup 并注册在清理之前(t.Cleanup 后进先出,保证清理
先跑),同时让清理失败显式报错而不是被吞掉。

注:同一模式(defer d.Close() + 在 t.Cleanup 里写库)在 db 包另有十余处,
本次只修被实证触发的那一处。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 16:24:47 +08:00
Autumn d663655e9c Merge pull request #184 from Autumn-27/fix/finding-asset-tree-scroll
fix(findings): 「按资产」视图资产列表溢出时补回滚动条
2026-09-30 16:17:08 +08:00
Autumn-27 587ef91f68 fix(findings): 「按资产」视图资产列表溢出时补回滚动条
左侧资产树的滚动容器靠 height:100% 取高,但外层卡片只给了 max-height
没有确定高度,百分比高度解析失败,资产多时列表撑破卡片或被截断且无法
滚动。改为把高度上限直接放到资产树的原生滚动容器上(overflow-y-auto +
max-h,随窗口自适应),资产少随内容收缩、资产多封顶并出滚动条。

Closes #170
2026-09-30 04:16:43 -04:00
Autumn cfe5a7d740 Merge pull request #183 from Autumn-27/feat/intercept-delete-path
feat(intercept): 新增内置拦截规则「删除类接口路径」
2026-09-30 16:15:15 +08:00
Autumn-27 5825b3f80c feat(intercept): 新增内置拦截规则「删除类接口路径」
内置的 HTTP 破坏性规则只认 DELETE 方法(curl -X DELETE、requests.delete(、
method:'DELETE'),路径类规则的词表又只有 /clear /wipe /flush /purge /truncate
/drop /destroy /factory-reset /reset-all。而多数应用的删除接口用 GET/POST 就能
触发,于是 `curl 'http://t/api/user/delete?id=1'` 不命中任何内置规则,会真实删掉
目标数据。

补一条 deny 规则覆盖 /delete /del /remove /unlink /erase /destroy,允许
/deleteAll、/delete_user、/delete-user 这类后缀形式(destroy 是重复覆盖:v1 那条
不允许后缀,/destroyAll 是漏的)。动词后强制跟分隔符,/delivery、/details、
/delta、/delegate 不会被误拦。

种子走独立标记位 intercept_default_rules_v3,已有实例升级后也会拿到——改 v1 的
字面量对老库无效(标记位已是 done)。插入带 WHERE NOT EXISTS 兜底,标记位丢失也
不会插重复。正则提为包级常量,配纯正则单测(不依赖 DB)覆盖命中与误伤两侧。
2026-09-30 04:12:57 -04:00
Autumn 992ae571b8 Merge pull request #182 from Autumn-27/feat/traffic-clear-all
feat(traffic): 流量列表新增「清空全部」,并借空库时机压实索引
2026-09-30 14:55:47 +08:00
Autumn-27 eb347d5965 feat(traffic): 流量列表新增「清空全部」,并借空库时机压实索引
一次删除全部流量记录,忽略当前筛选条件。与按 host 删除的两条路径相比多做
两件事:

- 清理索引已不再记录的历史 host 目录。按 host 删除只能从索引反查目录,
  行已消失的孤立目录会一直留着;「清空全部」既然语义是不留残余,就直接
  扫顶层目录(跳过 _index/_blobs/_ca/_delete_staging)。
- 收尾做一次全量压实(optimize + VACUUM + wal_checkpoint(TRUNCATE)),把
  索引占用的磁盘空间还给系统,并把实际释放量返回给前端提示。

VACUUM 的耗时与临时空间只和它保留的内容成正比,所以空库是做全量压实唯一
免费的时机——也正因如此,这一步顺带成为已有实例的转换途径:auto_vacuum
只能通过 VACUUM 生效,清空一次之后旧库即转为增量回收模式,此后日常按
host 删除就能自行回收空间(TestDeleteAllConvertsLegacyIndex 覆盖)。

已绑定到漏洞的流量证据保存在独立的证据库中,不受影响——绑定时已拷出独立
副本,正是为了让热流量可以随时清空而不带走证据。确认弹窗里明确写了这点。

前端把「清空全部」做成 outline 样式而非第二个 destructive 按钮:它忽略一切
筛选条件,不该看起来和「删除该目标」只差一次误点;无流量时禁用。

测试:新增 TestDeleteAllPurgesAndCompacts(清空后索引、正文、blob 引用与
孤立目录全清,空间回收到 1/8 以下,清空后仍可继续录制)与
TestDeleteAllConvertsLegacyIndex(旧库经清空后转为增量回收,随后普通删除
即可回收)。
2026-09-30 02:50:49 -04:00
Autumn 399f6768fc Merge pull request #180 from Autumn-27/fix/traffic-index-reclaim
fix(traffic): 删除流量后把索引空间真正还给操作系统
2026-09-30 13:54:28 +08:00
Autumn-27 e50f5a3083 fix(traffic): 删除流量后把索引空间真正还给操作系统
删除流量只让数据不可见,磁盘占用从不下降。两处原因叠加:

1. SQLite 删行仅把页挂到 freelist,而索引库建库时未启用 auto_vacuum,
   文件永不收缩。256KB 以下的正文全部内联在这个库里,所以体积主体就在
   这里,且只增不减。
2. ex_fts 是 contentless_delete 全文索引,DELETE 只写 tombstone、不回收
   原 postings,从来没有合并过——删流量反而让索引变大(实测 exchanges
   清零后 ex_fts_data 仍占 3.3MB / 840 行)。

trigram 索引约为正文体积的 2 倍,加上内联正文,抓包量大的实例会为早已
删掉的流量长期占用数倍磁盘:实测抓 6MB 文本正文 → 索引 16.2MB,删光后
仍是 16.2MB。

修复:
- 新建索引库在建表前启用 auto_vacuum=incremental。该 pragma 只在库中尚
  无表时可设,且只有紧随的 VACUUM 才把它写入文件头,两步都必须落在同一
  条连接上,故新增 initIndex 固定一条 *sql.Conn 串起 pragma 与建表。
- 新增 reclaim/reclaimChunk:删除提交后在后台分块执行全文索引增量合并、
  incremental_vacuum 与 wal_checkpoint(TRUNCATE)(不做 TRUNCATE 则空间
  留在 WAL 里、文件大小不变,PASSIVE 也清不掉)。接入 DeleteHost、
  HostDeleteStage.Commit 与 RecoverHostDeleteStages 三条路径。
- 回收分块进行并在块间释放写锁:record() 由 go-mitmproxy 在把响应写回
  客户端之前同步调用,一次性回收数 GB 会连带卡住被测请求。Close 先发停止
  信号,剩余预算留给下次删除,不拖住进程退出。
- 终止条件按「是否有进展」判定:incremental_vacuum 只能释放挪得到文件
  末尾的页,残留少量空闲页是正常结果;另加步数上限兜底。fts5 无法查询
  索引是否已合并完(其特殊 INSERT 自身即计一次 changes),故改为固定合并
  预算,未做完的留给后续删除。
- busy_timeout 改由 DSN 提供:它是每连接设置,原先的 db.Exec 只配到池中
  碰巧被选中的那一条,其余连接在有写者时会立即失败而非等待。驱动按第一个
  '?' 切分 DSN,故数据目录路径含 '?' 时退回裸路径 DSN。

已有实例的索引库仍是旧模式(auto_vacuum 无法事后开启),incremental_vacuum
在其上为空操作,启动时打印提示;此类库的 tombstone 增长已止住,已占体积
待后续「存储压缩」入口一次性回收。

测试:新增 reclaim_test.go(空间回收回归、tombstone 合并、新库模式、旧库
不卡死)与 upgrade_test.go(旧装机升级后同库同数据、全文与正文可用、
legacy path 行不受影响;以及降级回旧二进制仍可读写)。
2026-09-30 01:44:44 -04:00
Autumn f4de2292eb Merge pull request #179 from Autumn-27/feat/traffic-search-filter-sort
feat(traffic): 流量列表支持内容检索、路径/状态码/长度筛选与列排序 (#177)
2026-09-30 10:04:15 +08:00
Autumn-27 6d8c50a12e feat(traffic): 流量列表支持内容检索、路径/状态码/长度筛选与列排序 (#177)
- 后端 traffic.Page 改为 PageQuery:新增响应内容(FTS)、路径(url_template)、
  状态码(精确/Nxx 分桶)、响应长度范围筛选,并支持按 ts/status/resp_len 排序
  (尾随 id DESC 保证分页稳定);补 status/resp_len 两个索引(IF NOT EXISTS,旧库自动生效)
- server getTraffic 解析 body/path/status/resp_min/resp_max/sort/order 参数
- 前端流量页新增高级筛选行(响应内容/路径/状态码/长度范围)与可排序表头,
  排序偏好持久化到 localStorage;抽出公共组件 ui/sortable-head
- 仅改动前端页走的 Page,不动 Agent 工具的 query 契约
2026-09-29 21:58:17 -04:00
Autumn-27 524efdb1d0 chore(deps): 升级 norma 到 v0.4.3——工具输出全局兜底截断(harness 统一 CaptureOnce),任何工具都不再溢爆上下文 2026-09-29 21:14:08 -04:00
Autumn-27 c63955481d fix(tools): MCP/自定义工具输出统一走 Capture 截断,修复 mcp 截断不生效 (#176) 2026-09-29 10:09:23 -04:00
榆木 6b146d6c1b fix(llm): 一次性 LLM 调用补挂 session id,让自定义会话头真正生效 (#175)
自定义会话头(profile 的 session_header_key)的头值取自请求 context 上的
session id,而该 id 由 agentcore 仅在挂载 transcript store 时才写入 context
(agentcore.Prompt 中 `if s.writer != nil` 那一行)。

目标拆解(第 0 轮,DecomposeGoalsWithProvider)与冷节点压缩(§4 body 调用)
都是不挂 store 的一次性调用,context 上没有 session id,quotaAwareTransport
因此跳过该头。对 opencode zen 这类「缺 x-opencode-session 直接 400
MissingSessionID」的端点,表现为「第 0 轮目标拆解 400 失败、后续 planner
轮次却完全正常」;压缩失败则只留一行日志,极难关联到本设置。

改为在这两条路径显式挂上按探索稳定的 session id(exp<N>-goals /
exp<N>-compactor),与旁路问答、连接测试已有的做法一致。附带修好用量归因:
这两个 id 能被 llmrec.parseSession 正确解析,此前这两条路径的 token 完全
记不进 llm_usage。
2026-09-29 18:21:30 +08:00
Autumn-27 84ce61b8c4 chore(deps): 升级 norma 到 v0.4.2——修复 noa 压缩后 thinking/tool 消息被 provider 拒绝(#174) 2026-09-29 04:18:58 -04:00
Autumn-27 68ebf51698 docs(changelog): 定版 0.3.14 2026-09-24 13:35:48 -04:00
Autumn-27 1a10c153b0 feat(tasks): 漏洞列改为按严重度分档显示 严/高/中/低
- TaskListMetricsAll 用 COUNT(*) FILTER 按 severity 分四档统计(同一条查询)
- TaskListMetrics.Findings 及 TaskDTO.findings 由数字改为 {critical,high,medium,low}
- 列表页「漏洞」列渲染 x/x/x/x,配色沿用 severity 语义、为 0 的档位弱化
- severity 为空/白名单外的记录不计入任一档;mock 同步为对象结构
2026-09-24 13:32:45 -04:00