fix(security): P0 安全修复 — 审计日志 PII 脱敏 + AI Token 计量 + backup.sh 拼写 + CI audit

1. 审计日志 PII 脱敏: audit_service.rs 中 old_value/new_value 自动 mask
   patient/consultation/follow_up 等资源类型的 PII 字段(id_number/phone/name 等)
2. AI Token 计量: chat_handler.rs 从 Provider response 和 AgentOrchestrator 提取
   实际 input_tokens/output_tokens,替代硬编码 0
3. AI display_hints: 从 AgentOrchestrator 传递 display_hints 给前端 ChatResponse
4. backup.sh: PGDATABSE 拼写错误修复为 PGDATABASE
5. CI: npm audit 移除 || true,高危漏洞阻止合并
6. 新增六维度深度分析报告 docs/discussions/2026-05-28
This commit is contained in:
iven
2026-05-29 07:56:29 +08:00
parent ddf5c196e4
commit 03ead44385
5 changed files with 376 additions and 9 deletions

View File

@@ -9,6 +9,66 @@ use sha2::{Digest, Sha256};
use tracing;
use uuid::Uuid;
/// 审计日志中需要脱敏的 PII 字段名(小写匹配)
const PII_FIELDS: &[&str] = &[
"id_number",
"phone",
"emergency_contact_phone",
"emergency_contact_name",
"allergy_history",
"medical_history_summary",
"name",
"content",
];
/// 审计日志中需要脱敏的 resource_type 前缀
const PII_RESOURCE_TYPES: &[&str] = &[
"patient",
"consultation",
"follow_up",
"family_member",
"doctor_profile",
];
/// 对 JSON Value 中的 PII 字段进行脱敏
fn sanitize_audit_value(
value: &Option<serde_json::Value>,
resource_type: &str,
) -> Option<serde_json::Value> {
let needs_sanitization = PII_RESOURCE_TYPES
.iter()
.any(|prefix| resource_type.starts_with(prefix));
if !needs_sanitization {
return value.clone();
}
value.as_ref().map(sanitize_json_value)
}
fn sanitize_json_value(v: &serde_json::Value) -> serde_json::Value {
match v {
serde_json::Value::Object(map) => {
let sanitized: serde_json::Map<String, serde_json::Value> = map
.into_iter()
.map(|(k, v)| {
let key_lower = k.to_lowercase();
if PII_FIELDS.iter().any(|f| key_lower.contains(f)) {
(k.clone(), serde_json::Value::String("***".to_string()))
} else {
(k.clone(), sanitize_json_value(v))
}
})
.collect();
serde_json::Value::Object(sanitized)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(sanitize_json_value).collect())
}
other => other.clone(),
}
}
/// 持久化审计日志到 audit_logs 表。
///
/// 使用 fire-and-forget 模式:失败仅记录 warning 日志,不影响业务操作。
@@ -43,6 +103,10 @@ pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
// 计算当前记录的 record_hash
let record_hash = compute_record_hash(&log, prev_hash.as_deref());
// 脱敏处理:对 patient/consultation/follow_up 等资源类型的变更值中 PII 字段进行 mask
let sanitized_old = sanitize_audit_value(&log.old_value, &log.resource_type);
let sanitized_new = sanitize_audit_value(&log.new_value, &log.resource_type);
// 保存日志字段用于错误日志model 构建会 move String 字段)
let err_tenant_id = log.tenant_id;
let err_action = log.action.clone();
@@ -56,8 +120,8 @@ pub async fn record(mut log: AuditLog, db: &sea_orm::DatabaseConnection) {
action: Set(log.action),
resource_type: Set(log.resource_type),
resource_id: Set(log.resource_id),
old_value: Set(log.old_value),
new_value: Set(log.new_value),
old_value: Set(sanitized_old),
new_value: Set(sanitized_new),
ip_address: Set(log.ip_address),
user_agent: Set(log.user_agent),
created_at: Set(log.created_at),