一、为什么需要行为审计?
前四讲都是「事前防御」——在攻击发生前阻断。但没有任何防御系统能做到 100% 拦截,总会有攻击穿透层层防线。这时需要事后检测:通过审计日志发现异常行为,及时止损。
行为审计要回答三个核心问题:
- 谁(哪个 Agent/用户)在什么时间 调用了什么工具
- 传入的参数 是什么,返回的数据量 有多大
- 行为模式 是否偏离了基线(突然大量导出、非工作时间操作等)
二、审计系统的三层架构
层级 | 功能 | 存储 |
|---|---|---|
采集层 | 拦截所有工具调用,记录原始请求/响应 | 消息队列(Kafka) |
存储层 | 结构化存储审计日志,支持快速检索 | Elasticsearch / ClickHouse |
分析层 | 实时异常检测 + 离线行为画像 | Flink / Spark |
三、Go 实现:嵌入式审计与异常检测引擎
下面实现一个可以直接嵌入 MCP Gateway 的审计系统,包含:
- 审计日志采集:拦截所有工具调用并记录
- 行为画像:为每个 Agent 建立行为基线
- 实时异常检测:基于规则的异常行为识别
package main import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "log" "math" "os" "sync" "time" ) // ---- 审计日志结构 ---- type AuditRecord struct { ID string `json:"id"` Timestamp time.Time `json:"timestamp"` AgentID string `json:"agent_id"` AgentName string `json:"agent_name"` SessionID string `json:"session_id"` ToolName string `json:"tool_name"` Args map[string]interface{} `json:"args"` ArgsHash string `json:"args_hash"` // 参数哈希(用于去重) ResponseSize int `json:"response_size"` DurationMs int64 `json:"duration_ms"` Success bool `json:"success"` ErrorMsg string `json:"error_msg,omitempty"` SourceIP string `json:"source_ip"` RiskScore float64 `json:"risk_score"` // 0~100 Tags []string `json:"tags"` } // ---- 行为画像 ---- type BehaviorProfile struct { AgentID string `json:"agent_id"` FirstSeen time.Time `json:"first_seen"` LastSeen time.Time `json:"last_seen"` TotalCalls int64 `json:"total_calls"` ToolFrequency map[string]int64 `json:"tool_frequency"` // 各工具调用次数 HourlyPattern [24]int64 `json:"hourly_pattern"` // 各小时调用分布 AvgDuration float64 `json:"avg_duration_ms"` AvgResponseSize float64 `json:"avg_response_size"` UniqueArgs map[string]int `json:"unique_args"` // 参数多样性 AnomalyCount int `json:"anomaly_count"` mu sync.RWMutex } // ---- 审计引擎 ---- type AuditEngine struct { mu sync.RWMutex records []*AuditRecord // 近期记录(内存环缓冲) maxRecords int // 最大内存记录数 profiles map[string]*BehaviorProfile // Agent 行为画像 // 异常检测规则 rules []AnomalyRule // 审计日志写入器 writers []AuditWriter } type AuditWriter interface { Write(record *AuditRecord) error Close() error } // 文件写入器 type FileAuditWriter struct { file *os.File enc *json.Encoder } func NewFileAuditWriter(path string) (*FileAuditWriter, error) { f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, err } return &FileAuditWriter{file: f, enc: json.NewEncoder(f)}, nil } func (w *FileAuditWriter) Write(record *AuditRecord) error { return w.enc.Encode(record) } func (w *FileAuditWriter) Close() error { return w.file.Close() } // ---- 异常检测规则 ---- type AnomalyRule interface { Name() string Evaluate(record *AuditRecord, profile *BehaviorProfile) (bool, string) // is_anomaly, reason } // 规则1:非工作时间调用 type OffHoursRule struct { WorkStart int // 工作开始时间(小时) WorkEnd int // 工作结束时间(小时) } func (r *OffHoursRule) Name() string { return "off_hours_access" } func (r *OffHoursRule) Evaluate(record *AuditRecord, profile *BehaviorProfile) (bool, string) { hour := record.Timestamp.Hour() isOffHours := hour < r.WorkStart || hour >= r.WorkEnd if isOffHours { return true, fmt.Sprintf("非工作时间调用 (当前 %d:00,工作时间 %d:00-%d:00)", hour, r.WorkStart, r.WorkEnd) } return false, "" } // 规则2:高频调用 type HighFrequencyRule struct { Threshold int // 阈值次数 Window time.Duration // 时间窗口 } func (r *HighFrequencyRule) Name() string { return "high_frequency" } func (r *HighFrequencyRule) Evaluate(record *AuditRecord, profile *BehaviorProfile) (bool, string) { // 这里简化为检查总调用频率,实际应滑动窗口计数 if profile.TotalCalls > 0 { elapsed := record.Timestamp.Sub(profile.FirstSeen) rate := float64(profile.TotalCalls) / elapsed.Seconds() * 60 // 每分钟调用数 if rate > float64(r.Threshold) { return true, fmt.Sprintf("调用频率过高: %.1f 次/分钟 (阈值: %d)", rate, r.Threshold) } } return false, "" } // 规则3:数据量异常 type DataExfiltrationRule struct { ThresholdBytes int } func (r *DataExfiltrationRule) Name() string { return "data_exfiltration" } func (r *DataExfiltrationRule) Evaluate(record *AuditRecord, profile *BehaviorProfile) (bool, string) { if record.ResponseSize > r.ThresholdBytes { return true, fmt.Sprintf("返回数据量过大: %d bytes (阈值: %d)", record.ResponseSize, r.ThresholdBytes) } // 累计数据量检查 if profile.AvgResponseSize > 0 && float64(record.ResponseSize) > profile.AvgResponseSize*3 { return true, fmt.Sprintf("返回数据量突增: %d bytes (平均: %.0f)", record.ResponseSize, profile.AvgResponseSize) } return false, "" } // 规则4:从未见过的工具 type UnknownToolRule struct{} func (r *UnknownToolRule) Name() string { return "unknown_tool" } func (r *UnknownToolRule) Evaluate(record *AuditRecord, profile *BehaviorProfile) (bool, string) { if profile.ToolFrequency[record.ToolName] == 0 && profile.TotalCalls > 10 { return true, fmt.Sprintf("首次调用工具: %s", record.ToolName) } return false, "" } // 规则5:连续失败 type ConsecutiveFailureRule struct { Threshold int } func (r *ConsecutiveFailureRule) Name() string { return "consecutive_failures" } func (r *ConsecutiveFailureRule) Evaluate(record *AuditRecord, profile *BehaviorProfile) (bool, string) { // 简化实现:检查最近记录中的失败比例 return false, "" // 需要滑动窗口实现 } // ---- 初始化审计引擎 ---- func NewAuditEngine(maxRecords int) *AuditEngine { engine := &AuditEngine{ records: make([]*AuditRecord, 0, maxRecords), maxRecords: maxRecords, profiles: make(map[string]*BehaviorProfile), writers: make([]AuditWriter, 0), } // 注册默认规则 engine.rules = []AnomalyRule{ &OffHoursRule{WorkStart: 9, WorkEnd: 18}, &HighFrequencyRule{Threshold: 30, Window: time.Minute}, &DataExfiltrationRule{ThresholdBytes: 1024 * 100}, // 100KB &UnknownToolRule{}, } return engine } // 注册审计写入器 func (ae *AuditEngine) AddWriter(writer AuditWriter) { ae.writers = append(ae.writers, writer) } // 记录一次工具调用 func (ae *AuditEngine) Record( agentID, agentName, sessionID, toolName string, args map[string]interface{}, responseSize int, durationMs int64, success bool, errMsg string, sourceIP string, ) *AuditRecord { // 计算参数哈希 argsJSON, _ := json.Marshal(args) hash := sha256.Sum256(argsJSON) argsHash := hex.EncodeToString(hash[:]) record := &AuditRecord{ ID: generateID(), Timestamp: time.Now(), AgentID: agentID, AgentName: agentName, SessionID: sessionID, ToolName: toolName, Args: args, ArgsHash: argsHash, ResponseSize: responseSize, DurationMs: durationMs, Success: success, ErrorMsg: errMsg, SourceIP: sourceIP, } // 更新行为画像 ae.updateProfile(record) // 运行异常检测 ae.runAnomalyDetection(record) // 存储记录 ae.storeRecord(record) // 写入所有审计输出 for _, writer := range ae.writers { if err := writer.Write(record); err != nil { log.Printf("[AUDIT] 写入审计日志失败: %v", err) } } return record } // 更新行为画像 func (ae *AuditEngine) updateProfile(record *AuditRecord) { ae.mu.Lock() defer ae.mu.Unlock() profile, exists := ae.profiles[record.AgentID] if !exists { profile = &BehaviorProfile{ AgentID: record.AgentID, FirstSeen: record.Timestamp, ToolFrequency: make(map[string]int64), UniqueArgs: make(map[string]int), } ae.profiles[record.AgentID] = profile } profile.mu.Lock() defer profile.mu.Unlock() profile.LastSeen = record.Timestamp profile.TotalCalls++ profile.ToolFrequency[record.ToolName]++ profile.HourlyPattern[record.Timestamp.Hour()]++ // 滑动平均 n := float64(profile.TotalCalls) profile.AvgDuration = profile.AvgDuration*(n-1)/n + float64(record.DurationMs)/n profile.AvgResponseSize = profile.AvgResponseSize*(n-1)/n + float64(record.ResponseSize)/n // 参数多样性 profile.UniqueArgs[record.ArgsHash]++ } // 运行异常检测 func (ae *AuditEngine) runAnomalyDetection(record *AuditRecord) { ae.mu.RLock() profile := ae.profiles[record.AgentID] ae.mu.RUnlock() if profile == nil { return } riskScore := 0.0 var tags []string for _, rule := range ae.rules { isAnomaly, reason := rule.Evaluate(record, profile) if isAnomaly { tags = append(tags, rule.Name()) riskScore += 20.0 log.Printf("[ANOMALY] Agent=%s Rule=%s Reason=%s", record.AgentID, rule.Name(), reason) // 更新画像的异常计数 profile.mu.Lock() profile.AnomalyCount++ profile.mu.Unlock() } } // 累积异常分数 if riskScore > 50 { tags = append(tags, "high_risk") } record.RiskScore = math.Min(riskScore, 100) record.Tags = tags } // 存储记录(环形缓冲区) func (ae *AuditEngine) storeRecord(record *AuditRecord) { ae.mu.Lock() defer ae.mu.Unlock() if len(ae.records) >= ae.maxRecords { ae.records = ae.records[1:] } ae.records = append(ae.records, record) } // 查询 Agent 最近的审计记录 func (ae *AuditEngine) QueryRecentByAgent(agentID string, limit int) []*AuditRecord { ae.mu.RLock() defer ae.mu.RUnlock() var results []*AuditRecord for i := len(ae.records) - 1; i >= 0 && len(results) < limit; i-- { if ae.records[i].AgentID == agentID { results = append(results, ae.records[i]) } } return results } // 查询高风险记录 func (ae *AuditEngine) QueryHighRisk(minScore float64, limit int) []*AuditRecord { ae.mu.RLock() defer ae.mu.RUnlock() var results []*AuditRecord for i := len(ae.records) - 1; i >= 0 && len(results) < limit; i-- { if ae.records[i].RiskScore >= minScore { results = append(results, ae.records[i]) } } return results } // 获取 Agent 行为画像 func (ae *AuditEngine) GetProfile(agentID string) *BehaviorProfile { ae.mu.RLock() defer ae.mu.RUnlock() return ae.profiles[agentID] } // ---- 集成到 MCP Gateway 的审计中间件 ---- type AuditMiddleware struct { engine *AuditEngine } func NewAuditMiddleware(engine *AuditEngine) *AuditMiddleware { return &AuditMiddleware{engine: engine} } // 包装工具调用函数 func (am *AuditMiddleware) Wrap(agentID, agentName, sessionID, sourceIP string, toolName string, args map[string]interface{}, handler func() (interface{}, error)) (interface{}, error) { startTime := time.Now() // 执行实际调用 result, err := handler() duration := time.Since(startTime).Milliseconds() // 计算响应大小 var responseSize int if result != nil { respJSON, _ := json.Marshal(result) responseSize = len(respJSON) } // 记录审计 errMsg := "" success := err == nil if err != nil { errMsg = err.Error() } record := am.engine.Record( agentID, agentName, sessionID, toolName, args, responseSize, duration, success, errMsg, sourceIP, ) // 高风险操作触发告警 if record.RiskScore >= 80 { log.Printf("[ALERT] 高风险操作: Agent=%s Tool=%s Score=%.0f Tags=%v", agentID, toolName, record.RiskScore, record.Tags) // 这里可以触发 webhook、邮件、短信告警 } return result, err } // ---- 工具函数 ---- var idCounter int64 func generateID() string { idCounter++ ts := time.Now().UnixNano() return fmt.Sprintf("aud-%x-%d", ts, idCounter) } // ---- 演示 ---- func main() { // 初始化审计引擎 engine := NewAuditEngine(1000) // 添加文件写入器 writer, err := NewFileAuditWriter("audit.log") if err != nil { log.Fatalf("无法创建审计日志文件: %v", err) } defer writer.Close() engine.AddWriter(writer) middleware := NewAuditMiddleware(engine) // 模拟正常操作 fmt.Println("========== 正常操作 ==========") for i := 0; i < 5; i++ { _, _ = middleware.Wrap( "agent-cs-001", "客服小张", "sess-001", "10.0.0.100", "query_orders", map[string]interface{}{"user_id": "u_123", "status": "paid"}, func() (interface{}, error) { time.Sleep(time.Duration(50+i*10) * time.Millisecond) return map[string]interface{}{ "orders": []interface{}{ map[string]interface{}{"order_id": "ord_001", "amount": 299.00}, }, }, nil }, ) } // 模拟异常操作 fmt.Println("\n========== 异常操作 ==========") // 1. 非工作时间 engine.mu.Lock() // 伪造一个非工作时间的时间戳 offHourRecord := &AuditRecord{ ID: "test-off-hours", Timestamp: time.Date(2026, 9, 8, 23, 45, 0, 0, time.Local), AgentID: "agent-cs-001", ToolName: "export_all_users", Args: map[string]interface{}{"format": "csv", "include_all": true}, } engine.records = append(engine.records, offHourRecord) engine.mu.Unlock() // 2. 大规模数据导出 _, _ = middleware.Wrap( "agent-cs-001", "客服小张", "sess-001", "10.0.0.100", "export_orders", map[string]interface{}{"date_range": "2020-01-01~2026-09-08", "limit": 100000}, func() (interface{}, error) { time.Sleep(2000 * time.Millisecond) // 构造一个大的响应 largeResp := make([]interface{}, 1000) for i := 0; i < 1000; i++ { largeResp[i] = map[string]interface{}{ "order_id": fmt.Sprintf("ord_%06d", i), "user_name": fmt.Sprintf("用户%d", i), "phone": "13812345678", "address": "北京市朝阳区xxx小区", "amount": 9999.99, } } return largeResp, nil }, ) // 3. 从未调用过的工具 _, _ = middleware.Wrap( "agent-cs-001", "客服小张", "sess-001", "10.0.0.100", "delete_all_users", map[string]interface{}{"confirm": true}, func() (interface{}, error) { return nil, fmt.Errorf("权限不足") }, ) // 4. 高频调用 fmt.Println("\n========== 高频调用模拟 ==========") for i := 0; i < 40; i++ { _, _ = middleware.Wrap( "agent-cs-001", "客服小张", "sess-001", "10.0.0.100", "query_orders", map[string]interface{}{"user_id": fmt.Sprintf("u_%d", i)}, func() (interface{}, error) { time.Sleep(10 * time.Millisecond) return map[string]interface{}{"count": 1}, nil }, ) } // 查询高风险记录 fmt.Println("\n========== 高风险操作报告 ==========") highRisk := engine.QueryHighRisk(50, 10) for _, rec := range highRisk { fmt.Printf("风险评分: %.0f | Agent: %s | 工具: %s | 标签: %v\n", rec.RiskScore, rec.AgentID, rec.ToolName, rec.Tags) if len(rec.Args) > 0 { argsJSON, _ := json.Marshal(rec.Args) fmt.Printf(" 参数: %s\n", string(argsJSON)) } } // 查询 Agent 画像 fmt.Println("\n========== Agent 行为画像 ==========") profile := engine.GetProfile("agent-cs-001") if profile != nil { fmt.Printf("Agent: %s\n", profile.AgentID) fmt.Printf("首次活跃: %s\n", profile.FirstSeen.Format(time.RFC3339)) fmt.Printf("总调用次数: %d\n", profile.TotalCalls) fmt.Printf("平均耗时: %.0fms\n", profile.AvgDuration) fmt.Printf("平均响应大小: %.0f bytes\n", profile.AvgResponseSize) fmt.Printf("异常次数: %d\n", profile.AnomalyCount) fmt.Printf("工具调用分布:\n") for tool, count := range profile.ToolFrequency { fmt.Printf(" %s: %d 次\n", tool, count) } fmt.Printf("时段分布 (高峰时段):\n") maxHour := 0 maxCount := int64(0) for hour, count := range profile.HourlyPattern { if count > maxCount { maxCount = count maxHour = hour } } fmt.Printf(" 最活跃时段: %d:00 (%d 次调用)\n", maxHour, maxCount) } }四、异常检测规则详解
规则 | 检测目标 | 典型场景 |
|---|---|---|
非工作时间 | 凌晨批量操作 | 攻击者在无人值守时窃取数据 |
高频调用 | API 滥用/爬虫 | 短时间内大量调用同一接口 |
数据量异常 | 数据泄露 | 单次返回远超正常量的数据 |
陌生工具 | 权限探测 | Agent 尝试调用从未用过的工具 |
连续失败 | 暴力破解/配置错误 | 多次调用同一工具均失败 |
参数突变 | 注入攻击 | 参数模式与历史行为不一致 |
新 IP/设备 | 凭证盗用 | 从陌生 IP 发起调用 |
五、安全分层(L5)
本讲在 L1-L4 基础上叠加行为审计层:
层级 | 防御手段 | 本讲新增 |
|---|---|---|
L1 | Prompt 注入检测 | 输入过滤 |
L2 | 输出验证与对齐 | 参数校验、业务规则 |
L3 | 工具权限与最小特权 | RBAC/ABAC 策略引擎 |
L4 | 数据脱敏与隔离 | 字段级脱敏 |
L5a | 审计日志采集 | ✅ 全量记录工具调用 |
L5b | 行为画像 | ✅ Agent 行为基线建模 |
L5c | 实时异常检测 | ✅ 多规则异常评分 |
L5d | 风险告警 | ✅ 高风险操作即时通知 |
六、延伸阅读
- MITRE ATLAS:针对 AI 系统的攻击矩阵,行为审计的威胁建模参考
- Elastic Security SIEM:安全信息和事件管理系统,可用于审计日志分析
- Flink CEP(复杂事件处理):流式处理中的模式匹配,用于检测时序异常
- Uber's Schemaless Audit Logging:Uber 的审计日志系统设计实践
七、下一讲预告
第6讲:速率限制与熔断保护——防止 Agent 打垮下游系统
行为审计是被动检测,而速率限制是主动防御。当 Agent 出现异常行为时,我们不能等到审计日志写完再反应——必须在第一时间限流。下一讲实现令牌桶限流、并发控制、熔断器和降级策略,保护下游系统不被异常 Agent 打垮。
🧰开发之余的小工具推荐
处理 Base64、JSON 格式化、JWT 解析、Crontab 计算、PDF 合并压缩这些碎片需求,我常用一个纯前端本地工具箱:zz365.top。所有计算在浏览器完成,文件不上服务器,关页即清。免费、无登录、无广告,适合开发者当常驻标签页。