pii

package
v0.34.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package pii PII 价值模式检测与打码(批次二百七十八,对标 octop-harness

生命周期: Stable(v0.33.0 定级)——只允许 additive 变更(新增导出符号 / 新增方法且给零值默认实现 / 新增错误值);改签名、改语义、改默认值、 删导出符号都算 breaking,判定规则与变更流程见 docs/api-compat.md, 逐包理由见 api-lifecycle.yaml。

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

View Source
const (

	// SurfaceAll 三面全开(缺省值)。
	SurfaceAll = surfaceInputBit | surfaceOutputBit | surfaceToolResultsBit
)

Variables

View Source
var ErrNilSubscriber = errors.New("处置策略订阅回调为空")

ErrNilSubscriber 订阅回调为空。

登记一个必然 panic 的订阅者毫无意义,故直接报错并返回零句柄:调用方能**凭返回值** 知道订阅没生效,而不是等到第一次变更时才发现「原来一直没跟上」。

判在**本包**而不是 subhub:三个使用方各有自己的哨兵与语境文案,共用一个会让 `errors.Is(err, pii.ErrNilSubscriber)` 这类既有调用点判定链断掉。

View Source
var ErrUnknownStrategy = errors.New("未登记的处置策略")

ErrUnknownStrategy 未登记的处置策略。

View Source
var ErrUnknownSurface = errors.New("未登记的敏感面")

ErrUnknownSurface 未登记的敏感面名(宿主配置注入时的 fail-closed)。

Functions

func Apply

func Apply(s Surface, text string) (string, bool)

Apply 对指定面处置一段文本。

返回 (处置后文本, 是否阻断):

  • 面未启用 / 全局关闭 → (原文, false)
  • 检出且策略为 block → (处置后文本, true),**调用方必须履行阻断**
  • 其余 → (处置后文本, false)

func ApplyValue

func ApplyValue(s Surface, v any) (any, bool)

ApplyValue 递归下探常见载荷结构处置(结构面覆盖与原面无关门面一致, 另加上面/策略维度)。返回 (处置后载荷, 是否阻断)。

func Enabled

func Enabled() bool

Enabled 当前是否启用(关闭时 Apply / ApplyValue 原样透传,零改写)。

func Gen

func Gen() uint64

Gen 当前策略代次(与快照同步递增,供可观测性使用)。

func SetEnabled

func SetEnabled(on bool)

SetEnabled 注入全局开关(conf security.pii_value_mask / env BQ_PII_VALUE_MASK)。

func SetStrategy

func SetStrategy(s StrategyKind) error

SetStrategy 注入处置策略(未知策略报错并保持原值,不静默降级)。

func SetSurfaces

func SetSurfaces(ss SurfaceSet)

SetSurfaces 注入面集合。

func SurfaceEnabled

func SurfaceEnabled(s Surface) bool

SurfaceEnabled 指定面当前是否启用。

func Unsubscribe

func Unsubscribe(h Subscription)

Unsubscribe 注销订阅。重复注销同一句柄是安全的空操作。

有了它,订阅者才能在长生命周期场景下解绑:否则回调闭包连同它捕获的一切会被订阅表 永久留住,而这条泄漏不会自己显形。

func ValidateSurface

func ValidateSurface(s Surface) error

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 Kind

type Kind string

Kind PII 类型(span 标注与打码策略分派用)。

const (
	KindMobile     Kind = "cn_mobile"      // 大陆手机号(可带 +86/0086 前缀)
	KindResidentID Kind = "cn_resident_id" // 18 位居民身份证(末位 X 大小写均可)
)

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 Span

type Span struct {
	Kind  Kind
	Start int
	End   int
}

Span 一段命中(s 的字节偏移,[Start,End))。

func Detect

func Detect(s string) []Span

Detect 找出 s 中全部 PII span(已完成语义校验与边界判定,重叠时保留更长者)。 返回按 Start 升序、互不重叠。

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"
)

func Strategy

func Strategy() StrategyKind

Strategy 当前处置策略。

type Subscription

type Subscription uint64

Subscription 订阅句柄,传给 Unsubscribe 注销。0 表示无效。

func Subscribe

func Subscribe(fn func(Policy)) (Subscription, error)

Subscribe 登记一个处置策略变更订阅者,返回句柄与是否成功。

为什么注册完立刻回调一次当前快照

不这么做,订阅者对「现在生效的是什么」一无所知,要等到下一次变更才知道。策略注入 的典型形态恰恰是「注册完之后很久没有下一次变更」,于是订阅者带着一个从没填过的 视图上线——比没有订阅更危险,因为它以为自己已经跟上了。

注册先于立即回调:宁可让订阅者多收一次(立即回调给旧快照、紧接着的变更通知给新 快照,顺序仍单调向前),也不能在两者之间漏掉一次变更。

立即回调同样在锁外调用、panic 同样被 recover,与变更通知完全一致。

type Surface

type Surface string

Surface 敏感面。

const (
	// SurfaceInput 进入模型前的外部输入(用户消息、抓取正文、命令输出)。
	SurfaceInput Surface = "input"
	// SurfaceOutput 模型产出(给人看的)。
	SurfaceOutput Surface = "output"
	// SurfaceToolResults 工具返回——独立成面的理由:它会被回灌进下一轮上下文,
	// 泄漏路径与「给人看」完全不同,用同一处置无法同时满足两边。
	SurfaceToolResults Surface = "tool_results"
)

type SurfaceSet

type SurfaceSet uint8

SurfaceSet 面集合(位集合)。

func ParseSurfaces

func ParseSurfaces(names ...string) (SurfaceSet, error)

ParseSurfaces 由宿主配置解析面集合(未知面名报错,不静默丢弃)。

func SurfaceBit

func SurfaceBit(s Surface) (SurfaceSet, error)

SurfaceBit 单个面在集合中的位。

func Surfaces

func Surfaces() SurfaceSet

Surfaces 当前面集合。

func (SurfaceSet) Has

func (ss SurfaceSet) Has(s Surface) bool

Has 该集合是否含指定面。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL