【Claude】日志审计与合规追踪配置 — 已解决
【Claude】日志审计与合规追踪配置 — 已解决
适用版本:Claude Code v1.0.x 及以上
受影响场景:企业审计、合规追踪、操作日志、安全监控、数据泄露防护
阅读时长:约 25 分钟
目录
- 问题现象
- 原理深挖:Claude Code 日志体系
- 根因分析:审计缺失的五大根因
- 多方案解决:从日志到合规
- 验证回归:审计配置验证
- 避坑最佳实践
- 附录:审计配置速查表
1. 问题现象
1.1 典型问题表现
问题一:无法追踪 Claude Code 的操作历史
# Claude Code 修改了文件,但不知道什么时候改了什么 git diff # src/config.py 被修改了,但不知道是 Claude Code 还是人工修改 # 没有操作日志问题二:企业审计要求记录所有 AI 操作
合规要求: - 记录所有 Claude Code 的文件读写操作 - 记录所有命令执行 - 记录所有 API 调用的 Token 消耗 - 日志保留 90 天 - 支持审计查询问题三:无法检测敏感数据泄露
# Claude Code 可能读取了 .env 文件或密钥 # 但没有日志记录哪些敏感文件被访问 # 无法在事后检测数据泄露问题四:成本无法按项目/用户追踪
# 多个项目使用同一个 API Key # 月底账单 $500,但不知道哪个项目花了多少 # 缺少按项目/会话的成本追踪问题五:CI 中的操作无审计
# CI 环境中使用 Claude Code claude -p --dangerously-skip-permissions "修复 bug" # 没有记录 Claude 做了什么操作 # 如果 Claude 修改了不该改的文件,无法追踪2. 原理深挖:Claude Code 日志体系
2.1 日志层级
┌─────────────────────────────────────────────────────┐ │ Claude Code 日志层级 │ ├─────────────────────────────────────────────────────┤ │ │ │ Layer 1: 会话日志 (内置) │ │ ├── ~/.claude/projects/<project>/sessions/ │ │ ├── JSONL 格式,记录每轮对话 │ │ ├── 包含: 消息内容、工具调用、Token 消耗 │ │ └── 自动生成,无需配置 │ │ │ │ Layer 2: Verbose 日志 (--verbose) │ │ ├── 实时输出到 stderr │ │ ├── 包含: API 请求/响应、工具调用详情 │ │ └── 用于调试 │ │ │ │ Layer 3: Hook 日志 (自定义) │ │ ├── PreToolUse / PostToolUse hooks │ │ ├── 记录每次工具调用的详细信息 │ │ └── 需要手动配置 │ │ │ │ Layer 4: API 使用日志 (Anthropic Console) │ │ ├── console.anthropic.com │ │ ├── 记录 API 调用次数、Token 消耗、成本 │ │ └── 按组织/API Key 维度统计 │ │ │ │ Layer 5: 系统级日志 (OS) │ │ ├── shell history (~/.zsh_history) │ │ ├── 文件系统审计 (auditd/FSEvents) │ │ └── 需要操作系统级配置 │ │ │ └─────────────────────────────────────────────────────┘2.2 会话日志格式
// ~/.claude/projects/<project-hash>/sessions/<session-id>.jsonl {"type":"user","message":"修复 auth.py 的 bug","timestamp":"2025-01-15T10:00:00Z"} {"type":"assistant","message":"我来检查 auth.py...","timestamp":"2025-01-15T10:00:02Z","model":"claude-sonnet-4-20250514","usage":{"input":1500,"output":200}} {"type":"tool_use","tool":"Read","input":{"file_path":"src/auth.py"},"timestamp":"2025-01-15T10:00:03Z"} {"type":"tool_result","tool":"Read","output":"<file content>","timestamp":"2025-01-15T10:00:03Z"} {"type":"tool_use","tool":"Edit","input":{"file_path":"src/auth.py","old_str":"...","new_str":"..."},"timestamp":"2025-01-15T10:00:10Z"} {"type":"tool_result","tool":"Edit","output":"成功","timestamp":"2025-01-15T10:00:10Z"} {"type":"assistant","message":"已修复 bug","timestamp":"2025-01-15T10:00:15Z","usage":{"input":2000,"output":150}}2.3 Hook 审计机制
Claude Code Hook 审计流程: Claude 要执行操作 (如 Edit) ↓ PreToolUse Hook 被触发 → 记录: 时间、工具名、输入参数 → 可以阻止操作 (返回 deny) ↓ 操作执行 ↓ PostToolUse Hook 被触发 → 记录: 执行结果、耗时、状态 ↓ 审计日志写入 配置位置: .claude/settings.json → hooks2.4 合规审计需求矩阵
| 合规标准 | 审计要求 | Claude Code 对应 |
|---|---|---|
| SOC 2 | 操作日志、访问控制 | Hook 日志 + 权限配置 |
| GDPR | 数据访问记录、删除权 | 文件读取日志 |
| HIPAA | PHI 访问审计 | 敏感文件访问 Hook |
| ISO 27001 | 安全事件记录 | 安全操作日志 |
| 企业内部 | 成本追踪、操作审计 | Token 日志 + Hook |
3. 根因分析:审计缺失的五大根因
3.1 根因一:未配置 Hook
Claude Code 默认不记录操作日志,需要通过 Hook 配置审计日志。
3.2 根因二:会话日志不集中
会话日志分散在多个项目目录中,难以集中查询和分析。
3.3 根因三:缺少成本追踪
没有按项目、用户、会话维度追踪 Token 消耗和成本。
3.4 根因四:敏感文件访问无告警
Claude Code 读取.env、密钥等文件时没有实时告警机制。
3.5 根因五:日志保留策略缺失
会话日志和 Hook 日志没有自动清理或归档策略,可能无限增长。
4. 多方案解决:从日志到合规
4.1 方案一:Hook 审计系统
// .claude/settings.json — 审计 Hook 配置 { "hooks": { "PreToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "python3 .claude/hooks/audit-pre-tool.py" } ] } ], "PostToolUse": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "python3 .claude/hooks/audit-post-tool.py" } ] } ], "Stop": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "python3 .claude/hooks/audit-session-end.py" } ] } ] } }#!/usr/bin/env python3 # .claude/hooks/audit-pre-tool.py — 工具调用前审计 import json import sys import os from datetime import datetime from pathlib import Path # 审计日志目录 AUDIT_DIR = Path(os.environ.get("CLAUDE_AUDIT_DIR", ".claude/audit")) AUDIT_DIR.mkdir(parents=True, exist_ok=True) # 敏感文件模式 SENSITIVE_PATTERNS = [ ".env", "id_rsa", "id_ed25519", "credentials", "secret", "token", "apikey", "api_key", "password", "private_key", ".pem", ".key" ] def is_sensitive(filepath): """检查是否是敏感文件""" filepath_lower = str(filepath).lower() for pattern in SENSITIVE_PATTERNS: if pattern in filepath_lower: return True return False def log_audit(event_type, tool_name, input_data, alert=False): """写入审计日志""" log_entry = { "timestamp": datetime.utcnow().isoformat() + "Z", "event": event_type, "tool": tool_name, "input": input_data, "project": os.getcwd(), "session": os.environ.get("CLAUDE_SESSION_ID", "unknown"), "user": os.environ.get("USER", "unknown"), "alert": alert } # 按日期分文件 date_str = datetime.utcnow().strftime("%Y-%m-%d") log_file = AUDIT_DIR / f"audit-{date_str}.jsonl" with open(log_file, "a") as f: f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") # 敏感操作实时告警 if alert: alert_file = AUDIT_DIR / "alerts.jsonl" with open(alert_file, "a") as f: f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") print(f"⚠ AUDIT ALERT: 敏感文件访问 {input_data}", file=sys.stderr) # 读取 Hook 输入 try: hook_input = json.load(sys.stdin) tool_name = hook_input.get("tool_name", "unknown") tool_input = hook_input.get("tool_input", {}) # 检查敏感文件 alert = False if tool_name in ["Read", "Write", "Edit"]: filepath = tool_input.get("file_path", "") if is_sensitive(filepath): alert = True # Bash 命令审计 if tool_name == "Bash": command = tool_input.get("command", "") # 检查危险命令 dangerous = ["rm -rf", "sudo", "curl.*|.*sh", "wget.*|.*sh"] import re for pattern in dangerous: if re.search(pattern, command): alert = True break log_audit("pre_tool_use", tool_name, tool_input, alert) except Exception as e: # 审计日志不应阻止操作 print(f"Audit error: {e}", file=sys.stderr) # 不阻止操作 (exit 0) sys.exit(0)#!/usr/bin/env python3 # .claude/hooks/audit-post-tool.py — 工具调用后审计 import json import sys import os from datetime import datetime from pathlib import Path AUDIT_DIR = Path(os.environ.get("CLAUDE_AUDIT_DIR", ".claude/audit")) AUDIT_DIR.mkdir(parents=True, exist_ok=True) def log_audit(event_type, tool_name, input_data, output_data, duration=None): """写入工具调用后审计日志""" log_entry = { "timestamp": datetime.utcnow().isoformat() + "Z", "event": event_type, "tool": tool_name, "input": input_data, "output_summary": str(output_data)[:500] if output_data else None, "duration_ms": duration, "project": os.getcwd(), "session": os.environ.get("CLAUDE_SESSION_ID", "unknown"), "status": "success" } date_str = datetime.utcnow().strftime("%Y-%m-%d") log_file = AUDIT_DIR / f"audit-{date_str}.jsonl" with open(log_file, "a") as f: f.write(json.dumps(log_entry, ensure_ascii=False) + "\n") try: hook_input = json.load(sys.stdin) tool_name = hook_input.get("tool_name", "unknown") tool_input = hook_input.get("tool_input", {}) tool_output = hook_input.get("tool_output", {}) log_audit("post_tool_use", tool_name, tool_input, tool_output) except Exception as e: print(f"Audit error: {e}", file=sys.stderr) sys.exit(0)4.2 方案二:成本追踪系统
#!/usr/bin/env python3 # .claude/hooks/cost-tracker.py — Token 成本追踪 import json import sys import os from datetime import datetime from pathlib import Path COST_LOG_DIR = Path(".claude/audit/costs") COST_LOG_DIR.mkdir(parents=True, exist_ok=True) # 模型定价 (每百万 Token) PRICING = { "claude-opus-4-20250514": {"input": 15.0, "output": 75.0, "cache_read": 1.5, "cache_write": 18.75}, "claude-sonnet-4-20250514": {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75}, "claude-haiku-4-20250422": {"input": 0.25, "output": 1.25, "cache_read": 0.025, "cache_write": 0.3125}, } def calculate_cost(model, usage): """计算 API 调用成本""" pricing = PRICING.get(model, PRICING["claude-sonnet-4-20250514"]) input_tokens = usage.get("input_tokens", 0) output_tokens = usage.get("output_tokens", 0) cache_read = usage.get("cache_read_input_tokens", 0) cache_write = usage.get("cache_creation_input_tokens", 0) cost = ( input_tokens * pricing["input"] / 1_000_000 + output_tokens * pricing["output"] / 1_000_000 + cache_read * pricing["cache_read"] / 1_000_000 + cache_write * pricing["cache_write"] / 1_000_000 ) return round(cost, 6) def log_cost(model, usage): """记录成本""" cost = calculate_cost(model, usage) entry = { "timestamp": datetime.utcnow().isoformat() + "Z", "model": model, "input_tokens": usage.get("input_tokens", 0), "output_tokens": usage.get("output_tokens", 0), "cache_read_tokens": usage.get("cache_read_input_tokens", 0), "cache_write_tokens": usage.get("cache_creation_input_tokens", 0), "cost_usd": cost, "project": os.path.basename(os.getcwd()),  "session": os.environ.get("CLAUDE_SESSION_ID", "unknown") } date_str = datetime.utcnow().strftime("%Y-%m-%d") log_file = COST_LOG_DIR / f"cost-{date_str}.jsonl" with open(log_file, "a") as f: f.write(json.dumps(entry) + "\n") # Hook 入口 try: hook_input = json.load(sys.stdin) # 从 Hook 输入中提取使用信息 # PostToolUse 或 Stop hook 中可能包含 usage if "usage" in hook_input: model = hook_input.get("model", "claude-sonnet-4-20250514") log_cost(model, hook_input["usage"]) except: pass sys.exit(0)4.3 方案三:审计日志分析工具
#!/usr/bin/env python3 """ 审计日志分析工具 查询和分析 Claude Code 操作日志 """ import json import os import sys from pathlib import Path from datetime import datetime, timedelta from collections import defaultdict AUDIT_DIR = Path(".claude/audit") def load_audit_logs(days=7): """加载最近 N 天的审计日志""" logs = [] for i in range(days): date = (datetime.utcnow() - timedelta(days=i)).strftime("%Y-%m-%d") log_file = AUDIT_DIR / f"audit-{date}.jsonl" if log_file.exists(): with open(log_file) as f: for line in f: try: logs.append(json.loads(line)) except json.JSONDecodeError: continue return logs def analyze_operations(logs): """分析操作统计""" tool_counts = defaultdict(int) file_access = defaultdict(int) alerts = [] for log in logs: tool = log.get("tool", "unknown") tool_counts[tool] += 1 if tool in ["Read", "Write", "Edit"]: filepath = log.get("input", {}).get("file_path", "unknown") file_access[filepath] += 1 if log.get("alert"): alerts.append(log) return tool_counts, file_access, alerts def generate_report(days=7): """生成审计报告""" logs = load_audit_logs(days) if not logs: print("无审计日志") return tool_counts, file_access, alerts = analyze_operations(logs) print(f"=== Claude Code 审计报告 ({days} 天) ===") print(f"日志条目: {len(logs)}") print(f"时间范围: {logs[0]['timestamp'][:10]} ~ {logs[-1]['timestamp'][:10]}") print(f"\n--- 工具调用统计 ---") for tool, count in sorted(tool_counts.items(), key=lambda x: -x[1]): print(f" {tool}: {count} 次") print(f"\n--- 文件访问 Top 10 ---") for filepath, count in sorted(file_access.items(), key=lambda x: -x[1])[:10]: print(f" {count}x {filepath}") print(f"\n--- 敏感操作告警 ({len(alerts)}) ---") for alert in alerts[-10:]: # 最近 10 条 print(f" [{alert['timestamp']}] {alert['tool']}: {alert.get('input', {})}") # 成本分析 cost_logs = load_cost_logs(days) if cost_logs: total_cost = sum(l.get("cost_usd", 0) for l in cost_logs) total_input = sum(l.get("input_tokens", 0) for l in cost_logs) total_output = sum(l.get("output_tokens", 0) for l in cost_logs) print(f"\n--- 成本统计 ---") print(f" 总成本: ${total_cost:.4f}") print(f" 输入 Token: {total_input:,}") print(f" 输出 Token: {total_output:,}") print(f" API 调用: {len(cost_logs)} 次") def load_cost_logs(days=7): """加载成本日志""" logs = [] cost_dir = AUDIT_DIR / "costs" for i in range(days): date = (datetime.utcnow() - timedelta(days=i)).strftime("%Y-%m-%d") log_file = cost_dir / f"cost-{date}.jsonl" if log_file.exists(): with open(log_file) as f: for line in f: try: logs.append(json.loads(line)) except: continue return logs def query_sensitive_access(): """查询敏感文件访问记录""" logs = load_audit_logs(30) sensitive = [l for l in logs if l.get("alert")] print(f"\n=== 敏感文件访问记录 (30 天) ===") print(f"总计: {len(sensitive)} 次") for log in sensitive: timestamp = log.get("timestamp", "") tool = log.get("tool", "") user = log.get("user", "") input_data = log.get("input", {}) if tool in ["Read", "Write", "Edit"]: filepath = input_data.get("file_path", "") print(f" [{timestamp}] {user} {tool} {filepath}") elif tool == "Bash": command = input_data.get("command", "") print(f" [{timestamp}] {user} Bash: {command[:100]}") # 使用 if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "sensitive": query_sensitive_access() else: generate_report(days=int(sys.argv[1]) if len(sys.argv) > 1 and sys.argv[1].isdigit() else 7)4.4 方案四:日志集中化
#!/usr/bin/env python3 """ 日志集中化:将分散的会话日志和审计日志汇总 """ import json import os import shutil from pathlib import Path from datetime import datetime class LogCentralizer: """日志集中管理器""" def __init__(self, central_dir=None): self.central_dir = Path(central_dir or os.environ.get( "CLAUDE_LOG_CENTER", os.path.expanduser("~/.claude/audit-central") )) self.central_dir.mkdir(parents=True, exist_ok=True) def collect_session_logs(self): """收集所有项目的会话日志""" projects_dir = Path.home() / ".claude" / "projects" if not projects_dir.exists(): return collected = 0 for project_dir in projects_dir.iterdir(): if not project_dir.is_dir(): continue project_name = project_dir.name sessions_dir = project_dir / "sessions" if not sessions_dir.exists(): continue # 目标目录 dest_dir = self.central_dir / "sessions" / project_name dest_dir.mkdir(parents=True, exist_ok=True) # 复制会话日志 for session_file in sessions_dir.glob("*.jsonl"): dest_file = dest_dir / session_file.name # 不覆盖已收集的 if not dest_file.exists(): shutil.copy2(session_file, dest_file) collected += 1 print(f"✓ 收集了 {collected} 个会话日志") def collect_audit_logs(self): """收集项目级审计日志""" # 遍历所有项目目录 for audit_dir in Path(".").glob("*/.claude/audit"): project_name = audit_dir.parent.parent.name dest_dir = self.central_dir / "audit" / project_name dest_dir.mkdir(parents=True, exist_ok=True) for log_file in audit_dir.glob("*.jsonl"): dest_file = dest_dir / log_file.name if not dest_file.exists(): shutil.copy2(log_file, dest_file) def cleanup_old_logs(self, retention_days=90): """清理过期日志""" cutoff = datetime.utcnow().timestamp() - (retention_days * 86400) removed = 0 for log_file in self.central_dir.rglob("*.jsonl"): if log_file.stat().st_mtime < cutoff: log_file.unlink() removed += 1 print(f"✓ 清理了 {removed} 个过期日志 (>{retention_days} 天)") # 使用 centralizer = LogCentralizer() centralizer.collect_session_logs() centralizer.collect_audit_logs() centralizer.cleanup_old_logs(retention_days=90)4.5 方案五:实时告警系统
#!/usr/bin/env python3 # .claude/hooks/alert-system.py — 实时安全告警 import json import sys import os import smtplib from email.mime.text import MIMEText from datetime import datetime # 告警规则 ALERT_RULES = { # 敏感文件读取 "sensitive_read": { "patterns": [".env", "id_rsa", "credentials", "secret", "apikey"], "severity": "HIGH", "message": "敏感文件被读取" }, # 危险命令 "dangerous_command": { "patterns": ["rm -rf", "sudo ", "chmod 777", "curl.*|.*sh"], "severity": "CRITICAL", "message": "执行危险命令" }, # 大量文件操作 "mass_operation": { "threshold": 50, # 单次会话操作超过 50 次 "severity": "MEDIUM", "message": "大量文件操作" }, # 网络请求 "network_access": { "patterns": ["curl", "wget", "WebFetch"], "severity": "MEDIUM", "message": "网络访问" } } def send_alert(severity, message, details): """发送告警""" timestamp = datetime.utcnow().isoformat() # 控制台输出 print(f"[{severity}] {timestamp} - {message}", file=sys.stderr) print(f" 详情: {json.dumps(details, ensure_ascii=False)[:200]}", file=sys.stderr) # 写入告警文件 alert_file = Path(".claude/audit/alerts-realtime.jsonl") alert_file.parent.mkdir(parents=True, exist_ok=True) with open(alert_file, "a") as f: f.write(json.dumps({ "timestamp": timestamp, "severity": severity, "message": message, "details": details }, ensure_ascii=False) + "\n") # 高严重度发邮件 (可选) if severity == "CRITICAL" and os.environ.get("ALERT_EMAIL"): try: send_email(severity, message, details) except: pass # 告警不应阻止操作 def send_email(severity, message, details): """发送邮件告警""" smtp_host = os.environ.get("SMTP_HOST", "localhost") smtp_port = int(os.environ.get("SMTP_PORT", 25)) from_addr = os.environ.get("ALERT_FROM", "claude-audit@company.com") to_addr = os.environ.get("ALERT_EMAIL") subject = f"[Claude Code {severity}] {message}" body = f""" 时间: {datetime.utcnow().isoformat()} 严重度: {severity} 消息: {message} 详情: {json.dumps(details, ensure_ascii=False, indent=2)} 项目: {os.getcwd()} 用户: {os.environ.get('USER', 'unknown')} """ msg = MIMEText(body) msg["Subject"] = subject msg["From"] = from_addr msg["To"] = to_addr with smtplib.SMTP(smtp_host, smtp_port) as server: server.sendmail(from_addr, [to_addr], msg.as_string()) # Hook 入口 try: hook_input = json.load(sys.stdin) tool_name = hook_input.get("tool_name", "") tool_input = hook_input.get("tool_input", {}) # 检查敏感文件 if tool_name in ["Read", "Write", "Edit"]: filepath = str(tool_input.get("file_path", "")).lower() for pattern in ALERT_RULES["sensitive_read"]["patterns"]: if pattern in filepath: send_alert( ALERT_RULES["sensitive_read"]["severity"], ALERT_RULES["sensitive_read"]["message"], {"tool": tool_name, "file": tool_input.get("file_path")} ) break # 检查危险命令 if tool_name == "Bash": command = tool_input.get("command", "") import re for pattern in ALERT_RULES["dangerous_command"]["patterns"]: if re.search(pattern, command): send_alert( ALERT_RULES["dangerous_command"]["severity"], ALERT_RULES["dangerous_command"]["message"], {"command": command} ) break # 检查网络访问 for pattern in ALERT_RULES["network_access"]["patterns"]: if pattern in command: send_alert( ALERT_RULES["network_access"]["severity"], ALERT_RULES["network_access"]["message"], {"command": command} ) break except Exception as e: print(f"Alert error: {e}", file=sys.stderr) sys.exit(0)4.6 方案六:日志保留策略
#!/bin/bash # log-retention.sh — 日志保留和归档策略 # 配置 RETENTION_DAYS=${CLAUDE_LOG_RETENTION:-90} ARCHIVE_DIR=${CLAUDE_LOG_ARCHIVE:-".claude/audit/archive"} AUDIT_DIR=".claude/audit" echo "=== 日志保留策略 ===" echo "保留: ${RETENTION_DAYS} 天" echo "归档目录: ${ARCHIVE_DIR}" # 创建归档目录 mkdir -p "$ARCHIVE_DIR" # 归档超过保留期的日志 CURRENT_DATE=$(date +%Y%m%d) CUTOFF_DATE=$(date -v-${RETENTION_DAYS}d +%Y-%m-%d 2>/dev/null || date -d "-${RETENTION_DAYS} days" +%Y-%m-%d) echo "归档 ${CUTOFF_DATE} 之前的日志..." ARCHIVED=0 for log_file in "$AUDIT_DIR"/audit-*.jsonl; do [ -f "$log_file" ] || continue # 从文件名提取日期 FILE_DATE=$(basename "$log_file" | sed 's/audit-\(.*\)\.jsonl/\1/') if [[ "$FILE_DATE" < "$CUTOFF_DATE" ]]; then # 压缩并移到归档 gzip "$log_file" mv "${log_file}.gz" "$ARCHIVE_DIR/" ARCHIVED=$((ARCHIVED + 1)) fi done echo "✓ 归档了 ${ARCHIVED} 个日志文件" # 清理超过 1 年的归档 echo "" echo "清理超过 1 年的归档..." YEAR_AGO=$(date -v-365d +%Y-%m-%d 2>/dev/null || date -d "-365 days" +%Y-%m-%d) PURGED=0 for archive_file in "$ARCHIVE_DIR"/*.gz; do [ -f "$archive_file" ] || continue FILE_DATE=$(basename "$archive_file" | sed 's/audit-\(.*\)\.jsonl\.gz/\1/') if [[ "$FILE_DATE" < "$YEAR_AGO" ]]; then rm "$archive_file" PURGED=$((PURGED + 1)) fi done echo "✓ 清理了 ${PURGED} 个过期归档" # 日志大小报告 echo "" echo "=== 日志大小 ===" du -sh "$AUDIT_DIR" 2>/dev/null du -sh "$ARCHIVE_DIR" 2>/dev/null echo "" echo "文件数量:" find "$AUDIT_DIR" -name "*.jsonl" 2>/dev/null | wc -l | xargs echo " 活跃日志:" find "$ARCHIVE_DIR" -name "*.gz" 2>/dev/null | wc -l | xargs echo " 归档日志:"5. 验证回归:审计配置验证
5.1 审计验证脚本
#!/bin/bash # verify-audit.sh — 验证审计配置 echo "=== 审计配置验证 ===" # 1. 检查 Hook 配置 echo "Hook 配置:" if [ -f ".claude/settings.json" ]; then python3 -c " import json with open('.claude/settings.json') as f: data = json.load(f) hooks = data.get('hooks', {}) for event, hook_list in hooks.items(): for h in hook_list: for hook in h.get('hooks', []): print(f' ✓ {event}: {hook.get(\"command\", \"\")}') if not hooks: print(' ✗ 未配置审计 Hook') " 2>/dev/null fi # 2. 检查审计日志目录 echo "" echo "审计日志:" if [ -d ".claude/audit" ]; then LOG_COUNT=$(find .claude/audit -name "*.jsonl" | wc -l) LOG_SIZE=$(du -sh .claude/audit 2>/dev/null | cut -f1) echo " ✓ 目录存在: $LOG_COUNT 个日志, $LOG_SIZE" else echo " ✗ 审计目录不存在" fi # 3. 检查告警文件 echo "" echo "告警记录:" if [ -f ".claude/audit/alerts.jsonl" ]; then ALERT_COUNT=$(wc -l < .claude/audit/alerts.jsonl) echo " ✓ $ALERT_COUNT 条告警" else echo " - 无告警记录" fi # 4. 检查成本日志 echo "" echo "成本日志:" if [ -d ".claude/audit/costs" ]; then COST_FILES=$(find .claude/audit/costs -name "*.jsonl" | wc -l) echo " ✓ $COST_FILES 个成本日志文件" else echo " ✗ 无成本日志" fi echo "" echo "=== 验证完成 ==="5.2 验证清单
| # | 验证项 | 预期 | 方法 |
|---|---|---|---|
| 1 | Hook 配置 | PreToolUse/PostToolUse | settings.json 检查 |
| 2 | 审计日志生成 | 有日志文件 | 执行操作后检查 |
| 3 | 敏感文件告警 | 触发告警 | 读取 .env 测试 |
| 4 | 成本追踪 | 有成本记录 | 检查 costs/ 目录 |
| 5 | 日志格式 | 合法 JSONL | json.load 验证 |
| 6 | 日志保留 | 自动归档 | retention 脚本 |
| 7 | 集中化 | 日志汇总 | centralizer 工具 |
| 8 | 查询功能 | 可查询 | audit-analyzer |
6. 避坑最佳实践
6.1 审计配置原则
原则 1: Hook 审计 — 用 PreToolUse/PostToolUse 记录所有操作 原则 2: 敏感检测 — 自动检测 .env/密钥/危险命令 原则 3: 成本追踪 — 按项目/会话记录 Token 消耗 原则 4: 集中管理 — 日志汇总到统一目录 原则 5: 保留策略 — 90 天活跃 + 1 年归档 原则 6: 实时告警 — 高危操作即时通知 原则 7: 不阻塞 — 审计日志不应阻止操作 原则 8: 定期审查 — 定期分析审计报告6.2 常见陷阱
| # | 陷阱 | 后果 | 解决 |
|---|---|---|---|
| 1 | 无 Hook | 无操作日志 | 配置审计 Hook |
| 2 | 日志不集中 | 难以查询 | 用 centralizer |
| 3 | 无成本追踪 | 不知花费 | cost-tracker Hook |
| 4 | 无敏感检测 | 数据泄露 | 敏感文件模式匹配 |
| 5 | 日志无限增长 | 磁盘满 | 保留策略 |
| 6 | 审计阻塞操作 | Claude 卡住 | Hook exit(0) |
| 7 | 无告警 | 不知风险 | 实时告警系统 |
| 8 | CI 无审计 | 操作不可追 | CI 中也配 Hook |
7. 附录:审计配置速查表
7.1 Hook 事件
| 事件 | 触发时机 | 用途 |
|---|---|---|
| PreToolUse | 工具调用前 | 记录操作、安全检查 |
| PostToolUse | 工具调用后 | 记录结果、成本统计 |
| Stop | 会话结束 | 会话总结、成本汇总 |
| Notification | 通知事件 | 实时告警 |
7.2 审计日志字段
| 字段 | 说明 | 示例 |
|---|---|---|
| timestamp | 时间戳 | 2025-01-15T10:00:00Z |
| event | 事件类型 | pre_tool_use |
| tool | 工具名 | Read/Write/Edit/Bash |
| input | 输入参数 | {"file_path": "src/app.py"} |
| project | 项目路径 | /home/user/myproject |
| session | 会话 ID | abc-123-def |
| user | 用户 | zhubo |
| alert | 是否告警 | true/false |
7.3 保留策略推荐
| 日志类型 | 活跃保留 | 归档保留 | 格式 |
|---|---|---|---|
| 操作日志 | 90 天 | 1 年 | JSONL |
| 成本日志 | 90 天 | 2 年 | JSONL |
| 告警日志 | 90 天 | 2 年 | JSONL |
| 会话日志 | 30 天 | 6 月 | JSONL |
结语
日志审计与合规追踪是企业级 Claude Code 使用的必备配置。通过 Hook 审计系统、成本追踪、敏感文件检测、实时告警、日志集中化和保留策略,可以满足 SOC 2、GDPR、企业内部等合规审计要求。
核心要点回顾:
- Hook 审计:用 PreToolUse/PostToolUse Hook 记录所有工具调用
- 敏感检测:自动检测
.env、密钥、危险命令的访问 - 成本追踪:按项目、会话、模型维度记录 Token 消耗和成本
- 集中管理:用 LogCentralizer 汇总分散的日志
- 实时告警:高危操作即时告警(邮件/控制台)
- 保留策略:90 天活跃 + 1 年归档 + 自动清理
- 不阻塞:审计 Hook 始终 exit(0),不影响 Claude 操作
- 定期审查:用审计分析工具生成定期报告
