Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ jobs:
run: |
cd src/agent-sec-core/v2
cargo test --workspace --locked
cargo doc --workspace --no-deps --locked

- name: Run Rust tests with coverage
run: |
Expand Down
14 changes: 13 additions & 1 deletion .github/workflows/sec-core-rpmbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,19 @@ jobs:
echo "ERROR: agent_sec_cli remains importable after hiding V1 source"
exit 1
fi
make -C src/agent-sec-core test-e2e-rpm-v2
printf '%s\n' "$GITHUB_SHA" > src/agent-sec-core/v2-rpm-revision.txt
PYTEST_ADDOPTS=--junitxml=v2-rpm-e2e.xml make -C src/agent-sec-core test-e2e-rpm-v2

- name: Upload V2 installed acceptance evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: agent-sec-core-v2-e2e-${{ github.run_number }}-${{ github.sha }}
path: |
src/agent-sec-core/v2-rpm-e2e.xml
src/agent-sec-core/v2-rpm-revision.txt
if-no-files-found: ignore
retention-days: 7

- name: Show V2 daemon log after E2E failure
if: failure()
Expand Down
195 changes: 172 additions & 23 deletions docs/user-guide/en/agent-security/agent-sec-core/pii-checker.md

Large diffs are not rendered by default.

167 changes: 147 additions & 20 deletions docs/user-guide/zh/agent-security/agent-sec-core/pii-checker.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,86 @@
PII Checker 用于检测 Agent 输入和输出中的个人数据与凭据。它返回结构化 verdict,生成安全的
evidence 和可选脱敏文本,并记录经过清理的 Security Event,供审计和 Observability 关联使用。

## V2 第一阶段

V2 RPM 提供 Rust `agent-sec-cli` 和 `agent-sec-daemon`。在专属 Linux 验证环境使用
`./scripts/rpm-build.sh agent-sec-core-v2` 构建的 RPM;本次迁移不切换现有 Agent 宿主或服务。
安装该 RPM 后,启动系统服务,由服务创建受保护的运行目录:

```bash
sudo systemctl start agent-sec-core
```

通过 daemon 的默认 socket 发起扫描:

```bash
agent-sec-cli scan-pii --text 'contact alice@company.cn' --source manual
```

默认端点为 `/run/agent-sec-core/daemon.sock`。非空的 `AGENT_SEC_DAEMON_SOCKET` 可覆盖默认值,
`agent-sec-cli --socket /absolute/path/to/daemon.sock scan-pii ...` 优先于环境变量。
CLI 不自动启动 daemon,也不回退到 Python。文件和 stdin 由 CLI 读取;daemon 只接收文本,
不接收调用者指定的输入文件路径或规则路径。空文本合法;`--text-stdin` 是 `--stdin` 的别名。
扫描完成并返回 `pass`、`warn`、`deny` 时退出 `0`;扫描或连接失败退出 `1`,CLI 用法错误退出 `2`。
`deny` 是检测分类,不表示 PDP 已拒绝操作。

未指定 `--max-bytes` 时,V2 不默认截断输入。显式正数上限保留有效 UTF-8 前缀;非法 UTF-8
报错。请求和响应必须满足 V2 的 4 MiB 帧上限,其中包括 JSON 转义及信封开销;超限显式失败。
span 以 Unicode 字符计数,与 Python 位置语义一致,不使用 UTF-8 字节或 UTF-16 单元。

V2 保留原有顶层结果字段,在 `summary` 中增加证据元数据:

| 字段 | 含义 |
|------|------|
| `execution_status` | `completed` 或 `failed` |
| `coverage.status` | `complete`、`partial` 或 `unavailable` |
| `coverage.reasons` | 输入截断、规则无效、匹配受限或扫描失败的安全错误码 |
| `input_sha256` | 检测器收到文本的 SHA-256,不代表未收到的原始内容 |
| `scanned_input_sha256`、`scanned_bytes` | 实际扫描前缀的摘要及字节数 |
| `scanner_version` | 检测语义版本;V2 当前为 `2.0.0` |
| `ruleset_id` | scanner 版本及不可变内置/自定义规则配置的标识 |

`bytes_scanned` 保留 V1 前缀计数,可能包含被 `scanned_bytes` 排除的不完整 UTF-8 尾部。
`partial` 结果仍可能为 `pass`:verdict 只聚合已发现的 findings。判断证据是否完整时必须检查 coverage。


V2 在完成检测和全文脱敏后,将返回报告限制在 512 KiB 的格式化 JSON 内。超限报告仅保留每种
类型/严重级别的首条 finding,省略原始证据;若明细或证据发生缩减,设置
`summary.findings_truncated=true`。
verdict、`summary.total`、类别/严重级别统计、摘要和扫描 coverage 仍代表扫描范围内全部已检测
结果。Hook 提示使用这些总数并说明明细已省略;此时返回的 findings 是代表性证据,不是完整位置列表。

若完整 `redacted_text` 仍使报告超限,V2 用 `[REDACTED: output size limit]` 替代整段文本,
并设置 `summary.redacted_text_omitted=true`,不会返回未完成脱敏的尾部。两个标记在 false 时省略。
仅缩减输出不会使扫描 coverage 变成 partial;已完成扫描仍退出 `0`。独立的 `PiiScanner` 保留
完整报告;Executor 在审计投影和 Finalizer 之前执行返回限制。

## 检测语义版本

`summary.scanner_version` 标识检测行为,独立于 AgentSecCore 包版本和 RPC schema。
V2 的成功/失败扫描报告及审计结果均记录 `2.0.0`;内置 finding 的 engine 为 `regex_v2`,
自定义 finding 为 `fancy_regex`。`ruleset_id` 包含此版本、内置模式及自定义配置,
即使 YAML 内容不变,引擎行为变化仍可追溯。CI 的 commit SHA 标识被测构建,不是检测语义版本。

2.0.0 保留 11 类检测、置信度、Unicode span 和脱敏,并明确调整以下 V1 行为:

- JWT 候选包含紧凑的空 claims(`{}`),修复 V1 payload 最短长度门槛造成的漏报。
大整数和深层 JSON 对象采用结构校验,不继承 Python 整数转换或递归限制。非法 JSON 仍拒绝;
检测不验证签名真实性,也不授权 Token 的使用。
- 中国身份证的日期和校验位统一转换 decimal 数字,包括全角日期、数字校验位及 `X`/`x`,
同时保留原文和字符位置。
- 银行卡检测排除能通过 Luhn 的全零占位符;其他长度/校验和检查保留。命中不代表号码已经发行。
- 自定义模式使用下述原生方言。与 Python 的行为不同,本身不会使有效 V2 规则变成 invalid,
也不会因此将 coverage 标记为 partial。

保留的 V1 语料验证不变行为,独立的正反例和资源限制用例定义 V2 变化。这些检查覆盖支持的格式,
不代表对任意数据零误报漏报。短密钥、无标签密钥、11 类之外的格式和上下文歧义,
仍需结合业务规则及有代表性的样本评估。

## 使用随包 Skill

安装 V1 `agent-sec-cli` 且 Agent 能发现 `pii-checker` Skill 后,可以要求它检查指定文件中的
个人信息或凭证,或生成脱敏副本。Skill 报告脱敏证据,仅在用户要求时改写输入文件。
安装 `agent-sec-cli` 且 Agent 能发现 `pii-checker` Skill 后,可以要求它检查指定文件中的
个人信息或凭证,或生成脱敏副本。V2 需先启动 daemon。Skill 报告脱敏证据,仅在用户要求时改写输入文件。
扫描完成且没有命中,不代表内容一定不含敏感信息。

RPM 安装通过 `agent-sec-skills` 分发此 Skill,ANOLISA raw 包将其放入共享 Skill 目录。
Expand Down Expand Up @@ -110,17 +186,23 @@ scanner verdict `deny` 描述扫描风险,hook policy `block` 决定 adapter

## 自定义正则规则

PII Checker 可从一个固定的用户级文件加载业务自定义类型
内置规则随二进制发布,自定义规则独立配置

```text
~/.config/agent-sec/pii-checker/rules.yaml
```
| Runtime | 自定义文件 | 更新生效方式 |
|---------|------------|--------------|
| V2 | `/etc/agent-sec/pii-checker/rules.yaml` | daemon 启动时校验编译,重启后更新 |
| V1 | `~/.config/agent-sec/pii-checker/rules.yaml` | 下一次扫描加载新内容 |

管理员可通过
`agent-sec-daemon --pii-rules /absolute/path/rules.yaml --socket /run/agent-sec-core/daemon.sock`
指定其他绝对路径。所有调用者共享同一份不可变规则集合;V2 不读取调用者 HOME,不按 owner
选择规则,也不自动导入旧用户目录。扫描 RPC 不能覆盖此选择。

YAML 顶层是数组。每条规则包含唯一的自定义类型、一个正则表达式和可选严重级别。

```yaml
- type: dogfood_order_no
regex: '(?i)(?<=order_no[=:])DFT-[A-Z0-9]{8}'
regex: '(?<=order_no[=:])DFT-[A-Z0-9]{8}'
severity: warn

- type: dogfood_customer_token
Expand All @@ -131,18 +213,19 @@ YAML 顶层是数组。每条规则包含唯一的自定义类型、一个正则
| 字段 | 必填 | 说明 |
|------|------|------|
| `type` | 是 | 小写 snake_case 自定义类型,在文件内唯一 |
| `regex` | 是 | 单个正则表达式;flag 使用 `(?i)` 等内联语法 |
| `regex` | 是 | 单个正则表达式;V2 使用原生 `fancy-regex` 语法 |
| `severity` | 否 | `warn` 或 `deny`;默认 `deny` |

正则的完整匹配范围就是 finding 和脱敏范围,普通捕获组和命名捕获组不会改变该范围。如果正则同时
匹配字段名和值,二者都会被脱敏;如需完整匹配只覆盖值,请使用 lookaround。同一个类型的多种格式
需要通过正则 `|` 合并,同一个 type 不能定义多条规则。

自定义 finding 固定使用 category `custom`、confidence `1.0`、detector `custom_rule` 和 engine
`regex`。它会使用 `[DOGFOOD_ORDER_NO_REDACTED]` 这类稳定类型标记进行完全脱敏,并进入与内置
V1 为 `regex`、V2 为 `fancy_regex`。它会使用 `[DOGFOOD_ORDER_NO_REDACTED]` 这类稳定类型标记进行完全脱敏,并进入与内置
finding 相同的 verdict、policy、Security Event 和 Observability 链路。

自定义规则路径不支持 CLI 参数、环境变量、XDG 覆盖、系统级文件或多文件合并。
V1 不支持覆盖规则路径。两个版本都不合并多个规则文件。V2 检测规则属于检测配置,
不是 PAP Policy,也不会单独授予操作权限。

## 自定义规则校验与运行时限制

Expand All @@ -153,22 +236,41 @@ finding 相同的 verdict、policy、Security Event 和 Observability 链路。
| 文件大小上限 | 256 KiB |
| 规则数量上限 | 100 |
| 单条正则长度上限 | 2,048 个字符 |
| 正则分组嵌套深度上限 | 64 |
| YAML 嵌套深度上限 | 64 |
| V2 正则分组递归限制 | 64 个解析帧(包括根帧) |
| Type 格式 | `^[a-z][a-z0-9_]{0,63}$` |
| Severity | `warn` 或 `deny` |
| 单条规则匹配超时 | 20 ms |
| 单条规则匹配限制 | V1:20 ms;V2:1,000,000 次回溯 |
| 单次扫描自定义匹配总预算 | 200 ms |
| 单次扫描自定义 finding 上限 | 100 |

未知 YAML 字段、重复 type、内置类型名、非法正则以及能在空字符串上产生匹配的正则都会让整份
自定义规则集无效。运行时遇到的其他零长度匹配会被忽略。

`deny` 规则先于 `warn` 规则执行,同一 severity 内保持文件顺序。100 条上限只限制输出的自定义
finding,不会停止后续规则的评估;只有额外的有效命中被省略时,`truncated` 才会变为 `true`。
单规则或总时间限制仍可能停止剩余自定义规则,并通过独立状态记录。
`deny` 规则先于 `warn` 规则执行,同一 severity 内保持文件顺序。100 条上限只有在额外有效命中
被省略时才设置 `truncated`。V1 继续评估后续规则;V2 此时停止剩余自定义匹配,并标记覆盖不完整。
V2 在匹配操作之间检查 200 ms 总预算,不承诺单次匹配会在 20 ms 后被中断。
回溯或其他匹配限制触发时保留此前 findings。

V2 直接使用锁定版本的 `fancy-regex` 方言。引擎支持的 `(?i)` / `(?x)` 等 flag、
注释、lookaround 和集合运算均可使用,不因 Python 解释方式不同而改写或拒绝规则:

- `$` 默认断言输入末尾;`(?m)$` 也匹配行末。`\z` 严格表示输入末尾,`\Z` 可匹配末尾换行之前。
- `\h` / `\H` 表示十六进制/非十六进制字符;水平空白可明确写成 `[ \t]`。
- `(?i)` 使用引擎的 Unicode 大小写折叠,不复刻 Python 对带点/无点 I 的处理。
字符类之外的 `\<` / `\>` 是单词边界断言。
- 字符类支持嵌套集合和集合运算。命名组是否参与匹配的条件使用 `(?(<name>)yes|no)`;
Python 的 `(?(name)yes|no)` 写法在此有不同含义。

`invalid_regex` 仅用于实际解析、编译、引擎限制或加载时求值失败,例如不支持的 branch-reset
分组、变长 lookbehind。引擎将根帧计入分组递归限制:63 层分组可接受,64 层可能失败;
嵌套字符类采用独立的引擎限制。YAML 别名和多文档输入会拒绝;执行受限时报告 partial coverage。

迁移自定义规则时,应按有版本的 V2 方言验证正反例,覆盖 Unicode 和末尾换行。
编译成功本身不能证明任意管理员规则符合业务意图。

文件内容发生变化后,下一次扫描会自动校验并编译新内容。如果新版本无效,不会继续使用上一版有效
规则;内置检测仍保持工作,`scan-pii` 也会正常完成
V2 各请求持续使用启动时规则集合,直到重启;下一次启动发现替换文件无效时禁用整份自定义集合,
不会悄悄沿用旧规则。V1 在下一次扫描加载。两者均继续内置检测;V2 标记覆盖不完整

## 自定义规则状态

Expand All @@ -186,7 +288,7 @@ finding,不会停止后续规则的评估;只有额外的有效命中被省
}
```

文件不存在时 `status` 为 `absent`;校验成功时为 `loaded`,空数组也属于加载成功;读取、YAML
默认文件不存在时 `status` 为 `absent`(V2 显式指定文件不存在时为 `invalid`);校验成功时为 `loaded`,空数组也属于加载成功;读取、YAML
解析、schema 校验或正则编译失败时为 `invalid`。无效状态包含经过清理的 `error_code`;已加载或
无效内容可能包含其 SHA-256 摘要。运行时计数器不会包含输入文本或正则内容。直接执行 `scan-pii`
时还会向 stderr 输出经过清理的无效配置告警,同时保持成功退出。
Expand All @@ -205,9 +307,9 @@ finding,不会停止后续规则的评估;只有额外的有效命中被省
| `invalid_rule_type` | 规则 type 不符合要求的命名格式 |
| `duplicate_rule_type` | 同一个自定义 type 出现多次 |
| `reserved_rule_type` | 自定义 type 与内置 PII 类型冲突 |
| `invalid_regex` | 正则无法编译、分组嵌套超过 64 层,或加载期校验超时 |
| `invalid_regex` | 正则语法不支持、无法编译、超出引擎限制或加载期求值失败 |
| `regex_matches_empty_text` | 正则可在空字符串上产生零长度匹配 |
| `load_error` | 未预期的加载器错误已按 fail-open 模式处理 |
| `load_error` | V1:未预期的加载器错误已按 fail-open 模式处理 |

## Security Event 与 Observability

Expand All @@ -220,3 +322,28 @@ finding type、severity、category、span 和脱敏 evidence,不包含自定

Observability 使用现有 trace context 和输入 hash 与 Security Event 建立关联,不重复存储 finding
明细。

## V2 审计与规则迁移

V2 使用公共 Action Runtime 和 Finalizer。正常执行生命周期内,完成、部分完成、扫描失败以及
已授权但扫描参数无效的请求,各产生一次扫描终态事件。无效信封、未知方法、授权失败和传输失败
由各自入口处理;崩溃或强制退出可能阻止 Finalizer 执行。

审计只持久化安全请求元数据、摘要、规则标识、coverage、脱敏 findings 和错误码。
原文、原始 evidence、完整 `redacted_text`、规则内容和可能含输入的异常文本被排除。
JSONL/SQLite sink 健康与扫描成功分别判断;现有 daemon 启动仍要求 SQLite 初始化成功。
V2 暂未提供事件查询 CLI。

Hook 可在 `scan-pii` 之前传入顶层 `--trace-context '{"session_id":"session-1"}'`。
保留 V1 snake_case/camelCase 别名归一化,字符串去除首尾空白并限制为 256 字符。
这些字段是透明关联值,不是 OpenTelemetry TraceId。UID/GID/PID 始终来自 UDS peer;
`agent_name` 只是有长度限制的调用者元数据,不授予权限。

迁移规则时,审核选定的 V1 文件、解决类型名冲突,再显式将批准的 YAML 放入管理员管理的 V2 文件。
启动或重启 daemon,检查 `summary.custom_rules.status`、`ruleset_id` 和 coverage,运行正反例。
不会自动汇总用户目录。回滚规则时恢复原中央文件并重启;回滚 runtime 时停止验证 daemon,
恢复 V1 RPM/入口及独立保留的用户规则。V2 不修改这些 V1 文件。

六种 Hook adapter 使用固定事件和真实 Rust PII 子进程验证,包括观测脱敏;尚未迁移的观测存储
在测试边界隔离。这属于 Hook 契约验收,不代表真实宿主切换或完整 PIP/PDP/PEP 接入。
详见[两阶段设计](../../../../../src/agent-sec-core/docs/design/PII_V2_MIGRATION_zh.md)。
4 changes: 1 addition & 3 deletions src/agent-sec-core/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,11 @@ test-e2e-rpm-v2: ## Run E2E tests against V2 RPM-installed binaries
@# The shared code-scanner E2E imports rule metadata before it can decide
@# whether source-only inventory assertions apply to this installed V2 RPM.
@python3 -c 'import yaml' >/dev/null 2>&1 || pip3 install --quiet pyyaml
CODE_SCANNER_E2E_SKIP_TELEMETRY=1 python3 -m pytest tests/e2e/ tests/v2/ \
CODE_SCANNER_E2E_SKIP_TELEMETRY=1 PII_E2E_RUNTIME=v2 PII_HOOK_LAYOUT=installed python3 -m pytest tests/e2e/ tests/v2/ \
--import-mode=importlib -ra \
--ignore=tests/e2e/cli/test_events_e2e.py \
--ignore=tests/e2e/cli/test_observability_record_jsonl_e2e.py \
--ignore=tests/e2e/cli/test_observability_record_sqlite_e2e.py \
--ignore=tests/e2e/cli/test_scan_pii_e2e.py \
--ignore=tests/e2e/cli/test_session_report_e2e.py \
--ignore=tests/e2e/cli/test_skill_ledger_analyze_e2e.py \
--ignore=tests/e2e/codex-hooks/test_codex_hooks_e2e.py \
Expand All @@ -128,7 +127,6 @@ test-e2e-rpm-v2: ## Run E2E tests against V2 RPM-installed binaries
@# Pending V2 capabilities behind the ignores above:
@# cli/test_events_e2e.py -> security event query
@# cli/test_observability_record_*_e2e.py -> observability record sinks
@# cli/test_scan_pii_e2e.py -> PII scanning
@# cli/test_session_report_e2e.py -> session report
@# cli/test_skill_ledger_analyze_e2e.py -> skill ledger analyze
@# codex|qoder|qwen hook tests -> hooks calling the V2 CLI
Expand Down
9 changes: 7 additions & 2 deletions src/agent-sec-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ Full daemon endpoint, CLI, and host-hook status:
Detects personal data and credentials, and can emit redacted text.

The bundled [pii-checker Skill](skills/pii-checker/SKILL.md) lets an Agent scan
specified text or files and generate redacted text with the V1 CLI.
specified text or files and generate redacted text. V2 uses the Rust daemon:
set `AGENT_SEC_DAEMON_SOCKET` or pass top-level `--socket`; the CLI never starts it automatically.

```bash
agent-sec-cli scan-pii --text "contact alice@example.com" --source manual
Expand All @@ -355,7 +356,11 @@ agent-sec-cli scan-pii --text "card 4111111111111111" --redact-output
agent-sec-cli scan-pii --input ./sample.log --include-low-confidence
```

Custom business types can be added in `~/.config/agent-sec/pii-checker/rules.yaml`.
V2 loads custom business rules from `/etc/agent-sec/pii-checker/rules.yaml` at daemon startup;
`agent-sec-daemon --pii-rules /absolute/path/rules.yaml` selects an alternative. Restart to update.
V1 retains `~/.config/agent-sec/pii-checker/rules.yaml`. V2 reports coverage, rule identity and `summary.scanner_version: "2.0.0"`;
a `pass` result with partial coverage does not establish complete detection. This phase preserves
Hook contracts without switching live hosts or adding PDP enforcement.

Details: [PII Checker User Guide](../../docs/user-guide/en/agent-security/agent-sec-core/pii-checker.md).

Expand Down
Loading
Loading