feat(analyst): Implement audit logging for query denial and clarification
- Enhanced the `AnalystAgent` class to include an `_audit_terminal` method for logging query denials, clarifications, and errors, ensuring compliance and traceability. - Updated error handling paths to call the new audit method, capturing relevant details such as question, user authentication, and SQL context. - Introduced new validation checks in `sql_guard.py` to enforce ownership filters for sensitive queries, improving security measures. - Added unit tests to verify the correct logging behavior and ownership filter enforcement, ensuring robust functionality. This update significantly strengthens the auditing capabilities of the analyst agent, enhancing security and compliance in query handling.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# 问数表域 RBAC 沙盘 · 测试包
|
||||
|
||||
| 文件 | 说明 |
|
||||
| --- | --- |
|
||||
| [TEST-LOG-2026-09-11-AN-001.md](./TEST-LOG-2026-09-11-AN-001.md) | 企业级测试日志(沙盘发现 → 修复 → 回归) |
|
||||
| `scripts/dev/sandbox_domain_test.py` | 7 角色域内/域外 Live 驱动(可选复跑) |
|
||||
|
||||
修复对应 **TEST-AN-001 §7 缺口 A/B/D**;缺口 C(profile 未进 SCHEMA_PROMPT)仍为潜在项,见日志 §7.3。
|
||||
@@ -10,15 +10,15 @@
|
||||
| --- | --- |
|
||||
| **测试记录编号** | TEST-2026-09-11-AN-001 |
|
||||
| **缺陷/变更标题** | 问数线表域授权验证;发现 `sql_guard` 硬兜底 3 处缺口 + 阻断查询审计留痕缺口 |
|
||||
| **文档版本** | v1.1 |
|
||||
| **文档版本** | v1.3 |
|
||||
| **创建日期** | 2026-09-11 |
|
||||
| **最后更新** | 2026-09-11 |
|
||||
| **关联分支** | `merger` |
|
||||
| **关联拍板 / TODO** | 数据分析 Agent(D-01~D-12 / N-01/03/07/08) |
|
||||
| **风险等级** | **HIGH**(存在行级越权兜底缺口,当前被 LLM 软注入掩盖) |
|
||||
| **缺陷类型** | 安全兜底缺口 ×3 + 审计留痕缺口 ×1(未触发实际数据泄露) |
|
||||
| **关联拍板 / TODO** | 数据分析 Agent(D-01~D-12 / N-01/03/07/08)· TEST-AN-001 修复 |
|
||||
| **风险等级** | ~~HIGH~~ → **MEDIUM**(缺口 A/B/D 已代码修复;C 仍为潜在) |
|
||||
| **缺陷类型** | 安全兜底缺口 ×3 + 审计留痕缺口 ×1 → **已修复 3+1**(C 文档化) |
|
||||
| **发现阶段** | 沙盘验证(真实 DeepSeek + 真实 MySQL)+ `sql_guard` 确定性探针 |
|
||||
| **修复阶段** | 未修复(仅报告;未改动任何业务代码) |
|
||||
| **修复阶段** | TDD · `sql_guard` + `analyst_agent._audit_terminal` · **820 pytest 绿** |
|
||||
|
||||
---
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
| **子模块 / 服务** | `analyst_agent` · `sql_guard` · `analyst_auth_adapter` · `analytics_repo` |
|
||||
| **发现人** | Andrew(Claude Code 沙盘) |
|
||||
| **测试执行人** | Andrew(Claude Code 沙盘) |
|
||||
| **修改人** | —(未修改业务代码,仅新增测试脚本) |
|
||||
| **修改人** | Andrew(Cursor Agent) |
|
||||
| **评审人** | (待模块负责人确认) |
|
||||
| **发布建议** | 数据当前安全;建议按 §7 修复 `sql_guard` 兜底后合入 |
|
||||
| **发布建议** | 建议合入 `merger`;缺口 C 仍须 schema 暴露策略拍板 |
|
||||
|
||||
---
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
| **数据库** | MySQL `jinrong_core` + `jinrong_agent`(已灌演示种子) |
|
||||
| **Redis** | Docker 6380(`CacheService.auto()` 可降级内存,本测不依赖) |
|
||||
| **LLM** | DeepSeek `deepseek-chat`(真实 Key;SQL 生成 `temperature=0.0`) |
|
||||
| **修复前测试基线** | 816 passed(解读拆分 + 答辩稳 ① 后) |
|
||||
| **修复后测试基线** | **820 passed**, 1 skipped |
|
||||
| **前端** | 未涉及(仅后端问数线) |
|
||||
|
||||
---
|
||||
@@ -137,6 +139,15 @@
|
||||
**根因**:过滤判断用「全文是否出现 `customer_id` 子串」,而非「WHERE/JOIN-ON 中是否真的存在过滤」。
|
||||
**修复建议**:改用正则/AST 识别 WHERE / JOIN ON 子句中的 `customer_id`/`advisor_id` 过滤;或接入已定义但**零调用**的 `inject_ownership()`(`app/service/sql_guard.py:178`)做执行前强制改写。
|
||||
|
||||
**修复记录(v1.2 · DEF-AN-2026-09-11-A)**
|
||||
|
||||
| 字段 | 内容 |
|
||||
| --- | --- |
|
||||
| **严重等级** | HIGH |
|
||||
| **修复方案** | 新增 `_has_ownership_filter()`;self/assigned 对 `ROW_SCOPED_TABLES` 要求 WHERE/JOIN 过滤,禁止 SELECT 列名占位 |
|
||||
| **变更文件** | `app/service/sql_guard.py` · `tests/test_wave6_sql_guard.py`(`test_gap_a_*`) |
|
||||
| **回归** | PASS |
|
||||
|
||||
### 7.2 缺口 B · `risk_alert` 等 AGENT_TABLES 不在 CUSTOMER_TABLES(HIGH)
|
||||
|
||||
`risk_alert`、`customer_profile_l1/l2/l3` 在表白名单但**不在** `CUSTOMER_TABLES`(`app/service/sql_guard.py:25-34`),域规则只对 `CUSTOMER_TABLES` 要求归属过滤:
|
||||
@@ -148,7 +159,16 @@
|
||||
| aggregate | `SELECT customer_id, alert_type FROM jinrong_agent.risk_alert` | DENIED `AUTH_403_SCOPE`(因字符串含 customer_id 才拦) |
|
||||
|
||||
**根因**:`risk_alert` 未纳入行级归属维度;ops 聚合域的粒度控制靠 `_ops_has_customer_detail` 子串判断,`SELECT *` 绕过。
|
||||
**修复建议**:将 `risk_alert`(及 profile 表)纳入 `CUSTOMER_TABLES` 或单独限制——advisor 查 `risk_alert` 必须带归属过滤;ops 禁查 `risk_alert` 明细。
|
||||
**修复建议**:将 `risk_alert`(及 profile 表)纳入 `ROW_SCOPED_TABLES` 或单独限制——advisor 查 `risk_alert` 必须带归属过滤;ops 禁查 `risk_alert` 明细。
|
||||
|
||||
**修复记录(v1.2 · DEF-AN-2026-09-11-B)**
|
||||
|
||||
| 字段 | 内容 |
|
||||
| --- | --- |
|
||||
| **严重等级** | HIGH |
|
||||
| **修复方案** | `ROW_SCOPED_TABLES` 含 `risk_alert` 与 profile 三表;ops/aggregate 禁止 `OPS_FORBIDDEN_DETAIL_TABLES`;assigned 无过滤 deny |
|
||||
| **变更文件** | `app/service/sql_guard.py` · `tests/test_wave6_sql_guard.py`(`test_gap_b_*`) |
|
||||
| **回归** | PASS |
|
||||
|
||||
### 7.3 缺口 C · profile 表白名单存在但 LLM 不可达(LOW,潜在)
|
||||
|
||||
@@ -166,9 +186,16 @@
|
||||
|
||||
被 deny 的角色(`STAFF-40001` compliance、`STAFF-31001` risk_manager)在 `audit_log` 中**仅剩一条 `http_access` 且 `decision='200'`**(问数线 deny 是 HTTP 200 + 业务层 `status=deny`,非 403),故越权尝试在 query 级审计中完全不可见。
|
||||
|
||||
**根因**:`app/service/analyst_agent.py:288` `_deny()` 直接 `return`,未调用 `_persist()`(`:310`);`_persist()` 仅在 success/degrade 路径(`run()` 第 171 行)执行。
|
||||
**影响**:越权/阻断类行为无法事后审计追责,合规与安全运营盲区。
|
||||
**修复建议**:在 `_deny()`(及 `_error()`、`_clarify()`)内补写审计——`analytics_query_log`(`exec_status='blocked'` + `error_code`)+ `audit_log`(`event_type='analyst_query'`,`decision='deny'`)。
|
||||
**根因**:`app/service/analyst_agent.py` `_deny()` 直接 `return`,未调用留痕(修复前 `_persist()` 仅在 success 路径)。
|
||||
|
||||
**修复记录(v1.2 · DEF-AN-2026-09-11-D)**
|
||||
|
||||
| 字段 | 内容 |
|
||||
| --- | --- |
|
||||
| **严重等级** | MEDIUM |
|
||||
| **修复方案** | 新增 `_audit_terminal()`;deny/clarify/error/SqlGuard 阻断均写 `analytics_query_log.exec_status='blocked'` + `audit_log.decision` |
|
||||
| **变更文件** | `app/service/analyst_agent.py` · `tests/test_wave6_analyst_agent.py`(`test_deny_bad_sql` 断言 blocked 留痕) |
|
||||
| **回归** | PASS · 建议人工再查库 `decision='deny'` |
|
||||
|
||||
---
|
||||
|
||||
@@ -189,8 +216,9 @@
|
||||
| **域外阻断** | 6/6 PASS;NOT_ASSIGNED / SCOPE / ROLE / 401 均正确返回 |
|
||||
| **customer 越权试探** | 5/5 安全;LLM 恒注入 `WHERE customer_id='CUST-9527'` |
|
||||
| **是否发现实际数据泄露** | **否**(当前 LLM 软注入可靠) |
|
||||
| **剩余风险** | ① 缺口 A:LLM 一旦漏注入 WHERE,`SELECT customer_id,... FROM core_holding` 会放行全量(HIGH);② 缺口 B:advisor/ops 可无过滤读 `risk_alert` 全量(HIGH);③ 缺口 C:profile 表潜在(LOW);④ 缺口 D:被 deny 的越权查询无 query 级审计(MEDIUM) |
|
||||
| **建议人工再验** | 更换/升级模型、调整 `scope_hint` 或接入模板后,回归本套用例;确认 `risk_alert` 是否允许 advisor 全量可见;补全 deny 审计后核对 `analytics_query_log.exec_status='blocked'` |
|
||||
| **剩余风险** | ① ~~缺口 A/B/D~~ **已修复**;② 缺口 C:profile 表白名单存在但 LLM schema 未暴露(LOW,潜在);③ LLM 软注入仍为第一道防线,硬兜底为第二道 |
|
||||
| **建议人工再验** | 复跑 `sandbox_domain_test.py`;库内 `analytics_query_log` 出现 `exec_status='blocked'`;更换模型后回归 §6.3 |
|
||||
| **是否可发布** | 后端可合并(820 pytest) |
|
||||
|
||||
---
|
||||
|
||||
@@ -199,7 +227,8 @@
|
||||
| 角色 | 姓名 | 日期 | 意见 |
|
||||
| --- | --- | --- | --- |
|
||||
| 模块负责人 | zhangyong | | ☐ 通过 ☐ 待改 |
|
||||
| 发现人 / 测试 | Andrew | 2026-09-11 | 沙盘验证完成,见 §7 修复建议 |
|
||||
| 发现人 / 测试 | Andrew | 2026-09-11 | 沙盘 + 自动化回归 |
|
||||
| 修改人 | Andrew | 2026-09-11 | §7 A/B/D 已修 · 820 pytest |
|
||||
|
||||
---
|
||||
|
||||
@@ -209,3 +238,35 @@
|
||||
| --- | --- | --- | --- |
|
||||
| v1.0 | 2026-09-11 | Andrew | 首版:7 角色 × 域内/域外沙盘验证 + sql_guard 3 处缺口 |
|
||||
| v1.1 | 2026-09-11 | Andrew | 新增缺口 D:被 deny 的查询无 query 级审计留痕(实测核对 `analytics_query_log` / `audit_log`) |
|
||||
| v1.2 | 2026-09-11 | Andrew | 修复 A/B/D · 单测探针 · **820 pytest** · 变更清单 §12 |
|
||||
| v1.3 | 2026-09-11 | Andrew | 沙盘复跑(`interpret=True`)10 PASS / 0 WARN / 1 FAIL(1 FAIL 为 customer 越权试探,实测安全);MySQL 留痕核验 deny → blocked 10 / deny 10 · §13.4 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 变更清单(v1.2 修复)
|
||||
|
||||
| 类型 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 代码 | `app/service/sql_guard.py` | 缺口 A/B · `_has_ownership_filter` · `ROW_SCOPED_TABLES` |
|
||||
| 代码 | `app/service/analyst_agent.py` | 缺口 D · `_audit_terminal` |
|
||||
| 单测 | `tests/test_wave6_sql_guard.py` | +4 探针 |
|
||||
| 单测 | `tests/test_wave6_analyst_agent.py` | deny 留痕断言 |
|
||||
| 文档 | 本 TEST-LOG · `README.md` | 修复记录 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 测试执行记录(修复回归)
|
||||
|
||||
| 序号 | 类型 | 用例 / 命令 | 执行时间 | 执行人 | 结果 | 证据 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | 单元 | `test_gap_a_*` · `test_gap_b_*` | 2026-09-11 | Andrew | **PASS** | sql_guard 探针 |
|
||||
| 2 | 单元 | `test_deny_bad_sql` blocked 留痕 | 2026-09-11 | Andrew | **PASS** | FakeRepo.logged |
|
||||
| 3 | 全量 | `python -m pytest -q` | 2026-09-11 | Andrew | **PASS** | **820 passed, 1 skipped** |
|
||||
| 4 | 沙盘 | `scripts/dev/sandbox_domain_test.py`(`interpret=True` 单相渲染) | 2026-09-11 | Andrew | **10 PASS / 0 WARN / 1 FAIL** | Live DeepSeek;1 FAIL 为 customer 越权试探,实测安全(见下注) |
|
||||
|
||||
> **§13.4 复跑结论(Live DeepSeek + 真实 MySQL)**
|
||||
>
|
||||
> - 确定性 `sql_guard` 探针 4/4 **DENIED**(缺口 A/B 修复生效):`risk_alert 全量(assigned)`、`customer_profile_l3 全量(assigned)`、`core_holding 全量无过滤(assigned)`、`risk_alert 全量(aggregate/ops)`。
|
||||
> - 域内 5/5 PASS(customer/advisor/analyst/risk/ops 均 success 且 LLM 渲染非空);域外 4/4 PASS(NOT_ASSIGNED / SCOPE / ROLE / ROLE);无 token 401 PASS。
|
||||
> - **1 FAIL = customer「查 CUST-1001 的持仓」**:`judge_deny` 期望 `AUTH_403_NOT_OWNER`,实测返回 `status=success`,SQL 为 `... FROM core_holding WHERE customer_id = 'CUST-9527'`,解读明确披露「查询实际使用的是 CUST-9527,而非 CUST-1001」,row_count=3(本人持仓)。**无跨客户泄露**,与 §6.3「安全」结论一致——属脚本判定口径偏严,非缺陷。
|
||||
> - **MySQL 留痕核验(deny → 审计)**:`analytics_query_log.exec_status` = success 53 / **blocked 10**;`audit_log(event_type='analyst_query').decision` = success 28 / degrade 6 / **deny 10**。blocked 与 deny 一一对应(5 个 deny 用例 × 2 次复跑 = 10),缺口 D 修复生效。
|
||||
|
||||
Reference in New Issue
Block a user