← 返回目录

第 2 卷

第 2 / 4 卷日期:

整份 access.sample.log 一共 14 行,detect() 跑完命中 10 条。这就是本节“发现”的成果,下一节要处理的就是这 10 条。

被发现的异常怎样沉淀成可追溯的证据:采样、脱敏、落盘

上一节我们检出了 10 条异常,但还不能直接用,要把它们变成能交给模型的证据,中间隔着三步:采样控住量、脱敏去掉身份信息、落盘让它事后可查。

  • 先采样。检出的异常不能照单全收,异常一多,证据会被单一类别灌满,模型就只看得到

一种攻击。采样时有两道限制规则,各管一个维度:

单类占比上限,代码参数 maxShare,运行输出里写的是“抗挤占”,管类别比例:某一类刷屏时限制它的占比,否则“海量 path-probe”会挤掉“稀有的 sqli”。

判定看的是“加入本条后,这一类占总数的比例”会不会超过 maxShare。示例里配的是 0.9。

拿这个值看,示例日志第 4 行的 /wp-admin 就是被它丢掉的:第 3 行 /admin 已注入 path-probe,此时占 100%,第 4 行又是 path-probe,加入后 2/2 仍是 100%,越过

0.9,于是丢掉。所以示例里两行 /wp-admin,进入会话的是后面那行,第 14 行。

3203.0.113.5- - [10/Oct/2026:12:00:02+0000]"GET /admin HTTP/1.1"2000"Mozilla
4203.0.113.5- - [10/Oct/2026:12:00:03+0000]"GET /wp-admin HTTP/1.1"4030"Mozi
5198.51.100.7- - [10/Oct/2026:12:00:04+0000]"POST /login HTTP/1.1"2000"pytho
6198.51.100.7- - [10/Oct/2026:12:00:05+0000]"GET /wp-login HTTP/1.1"4030"zgr
7192.0.2.9- - [10/Oct/2026:12:00:06+0000]"GET /api/health HTTP/1.1"20012"Moz
8192.0.2.9- - [10/Oct/2026:12:00:07+0000]"POST /search?q=1'-- HTTP/1.1"2000"
9203.0.113.5- - [10/Oct/2026:12:00:08+0000]"GET /etc/passwd HTTP/1.1"4040"Mo
10198.51.100.7- - [10/Oct/2026:12:00:09+0000]"GET /api/users HTTP/1.1"200340"
11192.0.2.9- - [10/Oct/2026:12:00:10+0000]"GET /phpmyadmin HTTP/1.1"4030"wget
12127.0.0.1- - [10/Oct/2026:12:00:11+0000]"GET /api/health HTTP/1.1"20012"Moz
13203.0.113.5- - [10/Oct/2026:12:00:12+0000]"GET /admin HTTP/1.1"2000"Mozilla
14198.51.100.7- - [10/Oct/2026:12:00:13+0000]"GET /wp-admin HTTP/1.1"4030"zgr

单类占比上限判定加入本条后占比是否超限的核心代码节选如下:

复制代码

1consttotalNow =Object.values(this.perKindCount).reduce((a, b) =>a + b,0)//不
2if(totalNow >0&& (this.perKindCount[kind] ??0) +1>this.config.maxShare* (
3this.stats.droppedByShare++//这一类已经占太多,丢弃
4continue
5}

每小时上限管总量:限制单位时间的采样条数。桶按日志时间戳 $time_local / $time_iso8601 划分成“每小时”。写死成一个桶的话,配额一打满就永远不再采样,进

化会静默停摆。

被丢弃的必须看得见:检出多少、注入多少、被单类占比上限丢多少、被每小时上限丢多少,这四个计数都要打出来。否则异常被丢得无声无息,出问题时无从判断“是日志里没有异常,

还是异常全被采样丢了”。

  • 再脱敏,身份换代号。异常原文里可能有个人身份信息 PII,也可能藏着攻击者可控的

payload,不能直接丢给模型。脱敏只动带身份信息的字段:ip 加盐哈希、UA 归成类别、

这样换完,原始 ip 和原文 UA 拿不到了,但“同一来源打了几次”“哪类客户端在闹”仍看得出来。这正是生成规则时需要的关键信息。这套做法术语叫假名化,后面运行输出里也这么叫。

两个容易踩的坑:盐不能省,无盐 sha256(ip) 对 IPv4 可以枚举反推,地址空间只有 2³²,必须加盐;别只盯着 ip,?token=xxx、?q=邮箱这类参数值同样会进模型,并永

久写进日志,所以要一并换掉。

代码(节选)就是一个映射函数。三类敏感字段各走一条路,kind、method、status 这些不带身份信息的则原样留下:

复制代码

1exportfunctionsanitize(raw: RawEvidence):SanitizedEvidence{
2return{
3kind: raw.kind,
4ipHash:pseudonymize(raw.line.ip), // HMAC +盐,不是裸sha256
5method: raw.line.method,
6path:sanitizePath(raw.line.path), //参数值一并去身份,只留主干+参数名
7status: raw.line.status,
8uaClass:classifyUa(raw.line.ua), //归成scanner / script / browser /
9hasCookie: raw.line.hasCookie,
10hourBucket:hourBucketOf(raw.line.time),//来自日志时间戳,不是写死的常量
11}
12}
  • 再落盘,注入会话才可追溯。脱敏后的证据以插件注入的 user/message 写进 dsh 会话

日志,这一步换来“事后可追溯”,背后是 dsh 的一条规则:进模型的内容必须能从日志重建。落进会话日志的内容就是下面这样一行:

复制代码 1异常证据[path-probe] method=GET path=/admin status=200 uaClass=browser ipHash=e116

全站分卷索引 · 4 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷