尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

日志里不能有身份证号:PII 脱敏的四个出口(第90篇-E76)

日志里不能有身份证号:PII 脱敏的四个出口(第90篇-E76) 可观测三部曲到这里收官。第 86 篇 OTel 全链路里说过脱敏层在外第 88 篇装行车记录仪时 Langfuse 的四道工序里脱敏排第三棒还埋了个MaskFunc挂点。这篇把脱敏两个字摊开看——同一家公司、同一个仓库PII 处理不是一个函数是四个出口、四把尺子。三个问题串全篇PII 在链路的哪几个出口被拦位置地图一条正则怎么写才不误伤、不漏边界的功夫什么情况下会静默漏掉三条缝一四个出口、四把尺子#出口位置管什么尺子松紧处置①pii.redact工具参数进 LLM 前手机/身份证/email/Visa/MC低误报改的是业务数据等长星号 DecModify②同一 handler 复用模型输出回来后同①同①脱敏版回填 WARN 审计③piiscanHITL 审批卡片6 类 PII 计数高召回误报可接受只计数、不落明文④scrub工具输出回 LLM 前凭据 token/key标签正则标签保留[REDACTED]先看接线实况agent/infrastructure/hooklocal/runner.go。工具调用前是三棒接力BeforeToolUsesql.guard(fail-closed) → require_approval → pii.redact(fail-open)pii.redact挂第三棒:200-213命中 PII 就把脱敏后的 arguments 回填Actionmodify。模型输出侧的AfterModelCall:306-330复用同一个 handler命中则返回脱敏版 slog.Warn审计——审计里只记original_chars/sanitized_chars两个长度不落原文。③ 的piiscan是另一个哲学扫描待写内容只产出类型→数量塞进审批卡片包注释原话是不拦截、不脱敏、不落明文——给人看的提示连证据都不复制。四把尺子为什么不能合成一把因为位置决定松紧①改的是要进 LLM 的业务数据误伤一个 18 位订单号工具调用就废了所以正则要严③只是给审批人提个醒漏报比误报危险所以正则要宽④管的是凭据不是 PII和①互补——源码注释写得很清楚“合规脱敏管输入参数里的用户 PII在 hook 链上做Scrubber 管工具自身执行产生的临时凭据在 broker 返回结果给 LLM 前做。”demo/tmp/e90demo三组正则逐字照抄三个源文件让同一句话过三把尺子 场景 A同一句话过三把尺子 原文: 电话13800138000 身份证 11010119900307391X 订单号 123456789012345678 卡号 4111111111111111 key sk-abcdef0123456789 password pssw0rd99 邮箱 a.bexample.com ① piiscan 计数审批卡片不落明文: map[bank_card:3 email:1 id_card:2 phone:1 secret:1] ② pii.redact 出门: 电话*********** 身份证 ****************** 订单号 123456789012345678 卡号 **************** key sk-abcdef0123456789 password pssw0rd99 邮箱 *************** decmodify reasonpii.redact: masked sensitive data ③ scrub 出门: 电话13800138000 身份证 11010119900307391X 订单号 123456789012345678 卡号 4111111111111111 key sk-abcdef0123456789 password[REDACTED] 邮箱 a.bexample.com同一句话三种结果①连 18 位订单号都计了id_card:2 bank_card:3高召回的代价②只动了四处电话/身份证/卡号/邮箱订单号、sk- key、password 全部原样低误报的克制③只抹了 password 的值PII 一个不动。还有第五个出口——第 88 篇埋的 LangfuseMaskFuncoption.go:84定义、consumer.go:190-197在消费工序里媒体之后、截断之前执行签名就是func(string) string。它由接入方注入DeepFlux 生产目前没接——挂点建好不等于接上这句诚实边界第 88 篇说过这里闭环。二正则的功夫在边界边界断言的两种写法。①用的是\brunner.go:76③用的是(?:^|[^\d])piiscan.go:32-35。Go 的\b基于 ASCII 的\w[0-9A-Za-z_]于是一个反直觉的分裂 场景 B边界功夫\b vs [^\d] / 结构 vs 长度 字母前缀 a13800138000 piiRedactfalse piiscantrue map[phone:1] 中文前缀 电话13800138000 piiRedacttrue piiscantrue map[phone:1] 非法月份 110101199013033916 piiRedactfalse piiscantrue map[bank_card:1 id_card:1] 16 位 1 开头 1234567890123456 piiRedactfalse piiscantrue map[bank_card:1] 15 位连续 138001380001234 piiRedactfalse piiscanfalse map[]a13800138000字母 a 是 word 字符a 和 1 之间没有\b边界——①漏了而[^\d]只挡数字a 也算非数字③抓到。中文前缀反过来两者都抓中文不是 ASCII word 字符\b成立。哪把对看位置变量名里嵌着手机号a13800138000更像 ID 而不是电话①不误伤是对的审批提示宁可多报③也对。还是位置决定尺子。结构校验 vs 长度匹配。身份证正则两版对比①是结构版\b[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]\b——地址码 6 位 年份 19/20 开头 月份 01-12 日期 01-31③是长度版\d{17}[\dXx]——18 位完事。11010119901303391613 月不存在结构版不抓、长度版抓。代价在场景 A 里已经看到18 位订单号被长度版计成 id_card bank_card 双份源码 ponytail 注释自己承认18 位会与 id_card 重叠计一次advisory 可接受。正则做不了的那道门校验位。身份证第 18 位是前 17 位加权算出来的GB 11643-1999MOD 11-2。demo 里补了 10 行代码算这道门 附正则做不了的门 —— 校验位 11010119900307391X: 前17位算出的校验位7 实际X → false 11010119900307391…: 前17位算出的校验位7源码两条正则都不验这道门有意思的是11010119900307391X正是piiscan_test.go里的测试用例——它自己的校验位就不合法而整个测试照样绿因为链上没人验校验位。这不是批评正则抓的是看起来像身份证校验位验证是代码的活但若要做低误报的强校验这 10 行比把正则越写越长划算。替换哲学三分。①strings.Repeat(*, len(m))等长星号——长度保真日志对齐不乱④[REDACTED]且保留标签——password pssw0rd99变成password[REDACTED]读日志的人知道这里有个密码③什么都不替换只计数。scrub 的实现scrub.go:114-133还有个捕获组设计≥2 个捕获组→只把第 2 组值换成占位符保留第 1 组标签否则整段替换。demo 的动态注册用例正好踩到两个分支 场景 Dscrub 标签保留 动态注册 不可变 api_key[REDACTED] Bearer[REDACTED] 动态注册后: session [REDACTED] created ScrubMap 入参不变: token abcdefgh12345678 / 出门: token[REDACTED](?i)(sess_)[A-Za-z0-9]{16}只有 1 个捕获组 → 整段替换与 goclaw 的整段替换语义对齐api_key: abc123XYZ_被替换后连: 分隔符也一起被[\s:]*吞掉——标签保留分隔符不留。而ScrubMap返回新 map 不改入参是第 89 篇浅拷贝铁律的又一次出现观测/脱敏层不碰共享数据。工具还可以在运行时RegisterPattern注册自己产生的临时凭据模式比如某 connector 生成sess_前缀的一次性 token。三三条静默漏掉的缝缝一数字型 PII 的 JSON 往返漏脱。①的实现是marshal → 字符串上正则替换 → unmarshal 回 maprunner.go:82-104。如果手机号是 JSONnumber而不是 string 场景 C数字型 PII 的静默漏脱 string 型: decmodify 出门*********** number 型: decallow fellBacktrue 中间产物: {phone:13800138000} unmarshal 失败 → 原样放行出门 phone13800138000明文无日志替换把phone:13800138000变成phone:***********——非法 JSONUnmarshal 失败代码走到runner.go:100-102的兜底原样返回 DecAllow。手机号明文出门且没有任何日志。“宁可漏脱不破坏 payload的回退方向本身说得通脱敏挂了总不能把业务请求搞崩但回退路径零日志就是静默漏脱——正确形态是放行的同时打一行 WARN让合规侧能统计今天有多少次脱敏失败放行”。缝二流式输出不重写。AfterModelCall的脱敏版回填只覆盖非流式路径源码 ponytail 注释原话“SSE 流级别重写留 V3”。流式场景 token 是一个个推给浏览器的检测只能事后 WARN——用户已经看到了 PII审计里才出现一条告警。这是已声明的技术债不是没意识到。缝三文档与代码的 fail_open 相反。hook/CLAUDE.md的 FAQ 写PII redact 用 false报错就不让数据出去“而实际接线hooklocal/runner.go:86是mustHook(pii.redact, true)——fail-open代码注释明说报错放行不阻塞业务但日志可追溯”。两个方向都是合理选择合规优先 vs 可用性优先但文档写 A 代码跑 B 比没有文档更危险按文档排查的人会得出错误结论。同一族还有两处configs/hooks/example.yaml示例写ref: pii-redactor注册键是pii.redact名字对不上加mask_char/patterns两个配置字段——实现里piiRedact的 hook 参数直接_忽略配置了也没人读hook.go:18注释说 PreModelCall 可做 PII redact实际 in-process 接线里 PreModelCall 是 noop——用户输入进模型这条路没有 PII 挡板这可以是用户对自己的输入负责的设计但没有任何地方写下来。小结问题答案关键源码PII 在哪被拦四个出口工具参数/模型输出/审批卡片/工具输出凭据 观测层挂点未接hooklocal/runner.go:11,86,200,306尺子为什么不同位置决定松紧进 LLM 怕误伤、给人看怕漏报、出工具怕凭据runner.go:75-80vspiiscan.go:26-36边界怎么写\b漏字母前缀(?:^[^\d]) 只挡数字结构版拒 13 月长度版全收校验位谁管正则做不了MOD 11-2 要 10 行代码测试用例自己的号都不过校验demo 附替换用什么等长星号保长度/ [REDACTED]保可读/ 只计数不落明文三处对比什么时候漏number 型 PII → 非法 JSON → 回退放行零日志流式不重写fail_open 文档写反场景 C 三缝几条设计判断没有最好的正则只有放对位置的尺子。同一仓库三条正则三套标准不是不一致是各自对着自己的错误代价误伤业务数据、漏报给人看、放过凭据三者的代价曲线完全不同。检测与脱敏分离提示不复制证据。piiscan 只输出类型→数量连审批卡片都不给原文——把让人知道有 PII和展示 PII拆开。任何marshal → 字符串处理 → unmarshal的模式都要回答unmarshal 失败走哪、有没有日志。场景 C 那条缝就藏在这个回退分支里。fail_open 是行为契约写进文档的值必须和代码一致。方向本身可以争论文档与代码相反不能。下一篇进入 Part 16多 Agent 与上线。从一个 Agent 怎么变安全转向两个 Agent 怎么协作——Host-Worker 模式主 Agent 分派、子 Agent 执行注意力该拆给谁。
返回列表