Documentation
¶
Overview ¶
Package pii PII 价值模式检测与打码(批次二百七十八,对标 octop-harness PIIMiddleware 的价值面检测):在 logredact(agentkit,凭据类值模式: key=xxx/Authorization/JWT/sk- 前缀等)之外补「个人信息价值面」——大陆手机号与 18 位居民身份证。凭据脱敏按形态命中,个人信息没有键名可依(批266「logredact 键名盲区」教训的價值面延伸:审计链永久留存+SIEM 外发,工单摘要外发 Jira, 值本身即敏感,与键名无关)。
保守检测纪律(octop 同款):宁可漏打不可错打。身份证须过四重校验——GB/T 2260 省级码白名单 + 出生日期合法(含未来拒绝)+ 顺序码非全零 + MOD 11-2 校验码—— 订单号/流水号/银行卡号等长数字串几乎不可能同时通过;手机号要求两侧不邻接 字母数字(Go RE2 无环视断言,边界在代码判定),13 位毫秒时间戳因尾邻数字不 会误命中。缺省策略 mask 保长度(前段+星号+尾段),日志对齐与残留定位友好;其余 策略的处置形态由宿主注入的 StrategyKind 决定(见 policy.go)。
结构面:ApplyValue(按敏感面)递归下探 map[string]any / map[string]string / []any / []string——其中 map[string]string 是 agentkit logredact.RedactValue 的已知结构盲区(alert_svc redactMap 绕行注释在案), 本包一并收口。nil 容器原样保留:JSON 的 null 与空容器在审计链上是两种形状, 处置过程无权把它们合并成一种。
本包是零内部依赖的叶子层(architecture policy 模块 id=pii,requires=[], 错误走 oekit/pkg/errors 单 import):出口面(审计链/事件双出口/工单与告警外发文案) 无条件调用本包,并按各自敏感面走 Apply / ApplyValue;开关收敛在 Enabled 与面开关 (缺省全开)——run.go 从 conf security.pii_value_mask 注入全局开关(缺省开; 显式关闭时启动 WARN 显性化)。
Index ¶
- Constants
- Variables
- func Apply(s Surface, text string) (string, bool)
- func ApplyValue(s Surface, v any) (any, bool)
- func Enabled() bool
- func Gen() uint64
- func SetEnabled(on bool)
- func SetStrategy(s StrategyKind) error
- func SetSurfaces(ss SurfaceSet)
- func SurfaceEnabled(s Surface) bool
- func Unsubscribe(h Subscription)
- func ValidateSurface(s Surface) error
- type Kind
- type Policy
- type Span
- type StrategyKind
- type Subscription
- type Surface
- type SurfaceSet
Constants ¶
const (
// SurfaceAll 三面全开(缺省值)。
SurfaceAll = surfaceInputBit | surfaceOutputBit | surfaceToolResultsBit
)
Variables ¶
var ErrNilSubscriber = errors.New("处置策略订阅回调为空")
ErrNilSubscriber 订阅回调为空。
登记一个必然 panic 的订阅者毫无意义,故直接报错并返回零句柄:调用方能**凭返回值** 知道订阅没生效,而不是等到第一次变更时才发现「原来一直没跟上」。
判在**本包**而不是 subhub:三个使用方各有自己的哨兵与语境文案,共用一个会让 `errors.Is(err, pii.ErrNilSubscriber)` 这类既有调用点判定链断掉。
var ErrUnknownStrategy = errors.New("未登记的处置策略")
ErrUnknownStrategy 未登记的处置策略。
var ErrUnknownSurface = errors.New("未登记的敏感面")
ErrUnknownSurface 未登记的敏感面名(宿主配置注入时的 fail-closed)。
Functions ¶
func Apply ¶
Apply 对指定面处置一段文本。
返回 (处置后文本, 是否阻断):
- 面未启用 / 全局关闭 → (原文, false)
- 检出且策略为 block → (处置后文本, true),**调用方必须履行阻断**
- 其余 → (处置后文本, false)
func ApplyValue ¶
ApplyValue 递归下探常见载荷结构处置(结构面覆盖与原面无关门面一致, 另加上面/策略维度)。返回 (处置后载荷, 是否阻断)。
func SetEnabled ¶
func SetEnabled(on bool)
SetEnabled 注入全局开关(conf security.pii_value_mask / env BQ_PII_VALUE_MASK)。
func Unsubscribe ¶
func Unsubscribe(h Subscription)
Unsubscribe 注销订阅。重复注销同一句柄是安全的空操作。
有了它,订阅者才能在长生命周期场景下解绑:否则回调闭包连同它捕获的一切会被订阅表 永久留住,而这条泄漏不会自己显形。
func ValidateSurface ¶
ValidateSurface 校验面名是否已登记,供宿主在**接线期**自查配置。
为什么需要它 ¶
Apply / ApplyValue 对未登记面采取 fail-closed(整段替换 + 要求阻断),那是 运行期的兜底,代价是一次误伤。宿主若把面名做成配置项,应该在启动时调一次 本函数把拼写错误挡在外面——fail-closed 是保险,不是日常路径。
// 宿主启动时:
for _, name := range cfg.PiiSurfaces {
if err := pii.ValidateSurface(pii.Surface(name)); err != nil {
return fmt.Errorf("PII 面名 %q 未登记: %w", name, err)
}
}
Types ¶
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy 一份完整的运行时策略快照。
为什么不是三个独立的原子变量 ¶
enabled / surfaces / strategy 三样东西**合起来才是一份策略**,不是三个独立开关。 本包此前正是三个各自原子的包级变量(atomic.Bool + 两个 atomic.Uint32), 逐项看每一次读写都线程安全,看起来没问题——但读侧一次处置要分三次取:
Apply: Enabled() → SurfaceEnabled(s) → applySpans → Strategy() ApplyValue: 每个字符串叶子重走上面整条序列
热重载若发生在两次取之间,读到的是**从未被任何写侧设置过的组合**。 policy_atomic_test.go 的对照组量到了这个窗口:2483 万次读取里 252 万次 (约 10%)读到非法组合。真实后果不是「多打一次码」,而是语义错误:宿主已经 `SetSurfaces(0)` 关掉了某个面,本次调用却仍读到旧的面开着 + 新的 block 策略, 于是回报 blocked=true——**面已关闭却要求阻断**,宿主可能据此中断整条编排链。
与 netguard 的改造同形(那边是把主机名与后缀两个指针并成一份 Policy)。
func CurrentPolicy ¶
func CurrentPolicy() Policy
CurrentPolicy 取当前生效策略的快照。构造后不再被任何写侧触碰(写侧只整体 替换新实例),故取出后可安全长期持有与并发遍历,无需再加锁。
处置路径必须经由本函数拿快照,不得分字段调访问器再拼——那正是撕裂读的来源。
type StrategyKind ¶
type StrategyKind string
StrategyKind 处置强度(类型名不与同包的 Strategy() 访问器同名)。
const ( // StrategyMask 保长打码(缺省):前段+星号+尾段,日志对齐与残留定位友好。 StrategyMask StrategyKind = "mask" // StrategyRedact 整段删除换固定占位:不可见但长度可变。 StrategyRedact StrategyKind = "redact" // StrategyHash 换成不可逆摘要:保留「同一值相同摘要」的可关联性。 StrategyHash StrategyKind = "hash" // StrategyBlock 检出即阻断——**必须由宿主显式开启并自行履行**, // 本包只回报 blocked=true,不代为抛错(抛错会把出口面的处置决策 // 焊进内核,与「判定语义归内核、处置归宿主」相悖)。 StrategyBlock StrategyKind = "block" )
type Subscription ¶
type Subscription uint64
Subscription 订阅句柄,传给 Unsubscribe 注销。0 表示无效。
func Subscribe ¶
func Subscribe(fn func(Policy)) (Subscription, error)
Subscribe 登记一个处置策略变更订阅者,返回句柄与是否成功。
为什么注册完立刻回调一次当前快照 ¶
不这么做,订阅者对「现在生效的是什么」一无所知,要等到下一次变更才知道。策略注入 的典型形态恰恰是「注册完之后很久没有下一次变更」,于是订阅者带着一个从没填过的 视图上线——比没有订阅更危险,因为它以为自己已经跟上了。
注册先于立即回调:宁可让订阅者多收一次(立即回调给旧快照、紧接着的变更通知给新 快照,顺序仍单调向前),也不能在两者之间漏掉一次变更。
立即回调同样在锁外调用、panic 同样被 recover,与变更通知完全一致。
type SurfaceSet ¶
type SurfaceSet uint8
SurfaceSet 面集合(位集合)。
func ParseSurfaces ¶
func ParseSurfaces(names ...string) (SurfaceSet, error)
ParseSurfaces 由宿主配置解析面集合(未知面名报错,不静默丢弃)。