第 3 卷

环的前半段(长出候选 → 评测、确认 → 上线)前面已经讲过,本节从“漏拦”接着走:漏掉的请求怎么被记下来、怎么喂回证据库、下一轮提案怎么长出新候选;环之外,再说状态怎么
落盘、重启后不失效。
- 漏掉的请求怎么被记下来。先看漏拦到底漏了什么。为了演示,这里我们故意造一条比评
测那节更窄的候选 block admin only,它用 eq /admin(精确相等)而不是评测节的 starts_with /admin,于是只命中 /admin 这个路径。
拿它回放:9 条异常里只拦到 2 条 /admin,剩下 7 条(/wpadmin、/etc/passwd、/phpmyadmin 这些)一条没拦住,这 7 条就是漏拦。
漏拦不是只记个数就完了。observe 在“放过”时,把漏掉的原始日志行本身留存下来,而不只是计数。为什么非留住原始行不可?计数只够回答“漏了几次”,要让下一轮知道“漏的是什
么”,才能长出补它的规则,必须留住请求本身。
- 漏掉的请求怎么喂回证据库。① 记下来的那些原始行不能白记:把它们脱敏后喂回第 20
讲装证据的那个池子,下一轮提案生成时才能用到。这一步由 feedMissedToEvidence 完
有两处要留意一下:
只喂漏拦、不喂误伤。误伤(拦了正常请求)已由自动回退当场止损;若把误拦的正常请求当异常证据塞回去,模型反而会照着它长出朝正常请求开火的规则。
喂回是 drain(取出并清空)式的。observe 漏拦时收下原始行,drainMissed() 取出后立刻清空,防的是同一批漏拦每轮都被重复喂回,证据库里这一条越积越多,把别的信号
全挤掉,那正是第 20 讲抗挤占采样要防的事。
整件事的实现很短:
复制代码
| 1 | // feedback.ts(注释为讲解所加):漏掉的异常请求脱敏后喂回证据库,返回喂回的证据本身* |
|---|---|
| 2 | *exportfunctionfeedMissedToEvidence(sink: EvidenceSink, lc: RuleLifecycle):San |
| 3 | * constlines:NginxLine[] = lc.drainMissed() // drain:取出并清空* |
| 4 | * constfed:SanitizedEvidence[] = []* |
| 5 | * for(constlineoflines) {* |
| 6 | * constkind =detect(line) //重新detect,带上类别* |
| 7 | * if(!kind)continue //判不出类别:宁可少一条,也不要k |
| 8 | * constev =sanitize({ kind, line }) //脱敏复用ch20的sanitize,口径 |
| 9 | * sink.injectEvidence(ev)* |
| 10 | * fed.push(ev) //返回喂回的证据本身,供下一轮合并 |
| 11 | * }* |
| 12 | * returnfed* |
| 13 | *} |
- 下一轮提案怎么长出新候选。② 喂回的证据不是终点,下一轮把它和原有证据合并,再跑
一次 proposeFromEvidence。模型这次看见了上一轮漏掉的 /wpadmin、/etc/passwd,下一轮就长出覆盖它们的候选(输出里的 cand-8 ~ cand-
e)。这些新候选再过一遍评测、候选确认、生命周期,上一轮漏掉的就被补上了,接着它们上线,又出现新的漏拦,于是再喂回、再提案,环就这样一圈圈转起来。
心跳把误伤率冲淡,noteIdle 不碰误伤率分⺟),都不需要模型判断,也不需要人审批,这是和第 19、20 讲“人审批”最大的不同。
正因为它全自动、没人盯着,状态不能只活在内存:进入终态(回退 / 老化)的规则,会写成 lifecycle.rollback / lifecycle.aged_out 事实并落盘,复用第 19 讲账本那套机制
(同一套 user/message 载体 + JSONL 持久化)。
重启后 replayRetired 把“已判死”的规则重放出来,同一条规则想再走一遍灰度会被直接拒绝;否则,一旦“已回退的规则”被忘掉,自动回退这张安全网就会在重启后失效。示例
demo 直接调 observe / noteIdle 两个入口演示、不挂 loop-task 也不落盘;真实运行才由 loop-task.ts 定时驱动、loop-log.ts 落盘。
以下是 npm run start:ch21 的真实运行输出。其中每次一致的部分:历史流量样本固定,漏拦喂回演示用的窄候选只拦 /admin,也写死在示例里。每次不同的部分:评测、候选
确认、生命周期本身是确定性代码,不受运行时机影响;但输入是模型生成的候选,候选一变,相关数字就变;证据里的 ipHash 用进程内随机盐,每次不同;候选由 ch20 真实模型
生成,名称、条件、条数每次都可能不同。
以下为一次代表性运行:
复制代码 1=== 历史流量(回放样本)===
2请求数=19该拦(bad)=9正常(good)=10
| 3 | [firewall-observe]未设置FIREWALL_IP_HASH_SECRET:改用进程内随机密钥。假名化仍不可反推, |
|---|---|
| 4 | (ch20真实模型提案回复节选: [ |
| 5 | {"name":"block admin probe","when": {"field":"path","op":"starts_with", |
| 6 | {"name":"b…;token in=453 out=265) |
7
| 8 | ===评测三件事(覆盖率/精确率/误伤率)+事实校验=== |
|---|---|
| 9 | cand-1 [block admin probe]覆盖率=22.2%精确率=100.0%误伤率=0.0%逆推=[path-probe] |
| 10 | ...(cand-2 ~ cand-5是/wp-login、/wp-admin、/phpmyadmin、/etc/passwd四条路径候选 |
| 11 | cand-6 [block sqli]覆盖率=0.0%精确率=0.0%误伤率=0.0%逆推=[sqli]事实校验=ok ⚠未 |
| 12 | cand-7 [block ua scanner]覆盖率=0.0%精确率=0.0%误伤率=0.0%逆推=[bad-ua]事实校验 |