zugashield 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
zugashield/__init__.py ADDED
@@ -0,0 +1,539 @@
1
+ """
2
+ ZugaShield - AI Agent Security System
3
+ =======================================
4
+
5
+ 7-layer defense system for AI agent threats:
6
+
7
+ Layer 1: Perimeter ─── HTTP middleware, request validation
8
+ Layer 2: Prompt Armor ── Injection detection, input sanitization
9
+ Layer 3: Tool Guard ──── Tool execution gating, parameter validation
10
+ Layer 4: Memory Sentinel ── Memory content validation, poison detection
11
+ Layer 5: Exfiltration Guard ── Output DLP, secret detection
12
+ Layer 6: Anomaly Detector ── Behavioral baselines, chain attack detection
13
+ Layer 7: Wallet Fortress ── Transaction approval, address validation
14
+
15
+ Usage:
16
+ from zugashield import ZugaShield
17
+
18
+ shield = ZugaShield()
19
+ decision = await shield.check_prompt("user message", context={})
20
+ if decision.is_blocked:
21
+ return "Blocked by ZugaShield"
22
+
23
+ Architecture:
24
+ - Modeled after uBlock Origin: curated threat catalog + layered defenses
25
+ - Zero required dependencies - works out of the box
26
+ - Configurable via environment variables (ZUGASHIELD_*)
27
+ - All layers run asynchronously with <15ms total fast-path overhead
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import logging
33
+ import re
34
+ from typing import Any, Dict, List, Optional
35
+
36
+ from zugashield.types import (
37
+ AnomalyScore,
38
+ MemoryTrust,
39
+ ShieldDecision,
40
+ ShieldVerdict,
41
+ ThreatCategory,
42
+ ThreatDetection,
43
+ ThreatLevel,
44
+ ToolPolicy,
45
+ allow_decision,
46
+ block_decision,
47
+ )
48
+ from zugashield.config import ShieldConfig
49
+ from zugashield.threat_catalog import ThreatCatalog
50
+ from zugashield.audit import ShieldAuditLogger
51
+
52
+ from zugashield.layers.perimeter import PerimeterLayer
53
+ from zugashield.layers.prompt_armor import PromptArmorLayer
54
+ from zugashield.layers.tool_guard import ToolGuardLayer
55
+ from zugashield.layers.memory_sentinel import MemorySentinelLayer
56
+ from zugashield.layers.exfiltration_guard import ExfiltrationGuardLayer
57
+ from zugashield.layers.anomaly_detector import AnomalyDetectorLayer
58
+ from zugashield.layers.wallet_fortress import WalletFortressLayer
59
+ from zugashield.layers.llm_judge import LLMJudgeLayer
60
+ from zugashield.multimodal import MultimodalScanner
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ # Approval provider interface for Human-in-the-Loop integration
65
+ _approval_provider = None
66
+
67
+
68
+ def set_approval_provider(provider: Any) -> None:
69
+ """Set an external approval provider for HIL integration."""
70
+ global _approval_provider
71
+ _approval_provider = provider
72
+
73
+
74
+ def get_approval_provider() -> Any:
75
+ """Get the current approval provider (or None)."""
76
+ return _approval_provider
77
+
78
+
79
+ class ZugaShield:
80
+ """
81
+ Main facade for the ZugaShield security system.
82
+
83
+ Provides a clean API for each integration point.
84
+ """
85
+
86
+ def __init__(self, config: Optional[ShieldConfig] = None) -> None:
87
+ self._config = config or ShieldConfig.from_env()
88
+ self._catalog = ThreatCatalog()
89
+ self._audit = ShieldAuditLogger()
90
+
91
+ # Initialize all layers
92
+ self.perimeter = PerimeterLayer(self._config, self._catalog)
93
+ self.prompt_armor = PromptArmorLayer(self._config, self._catalog)
94
+ self.tool_guard = ToolGuardLayer(self._config, self._catalog)
95
+ self.memory_sentinel = MemorySentinelLayer(self._config, self._catalog)
96
+ self.exfiltration_guard = ExfiltrationGuardLayer(self._config, self._catalog)
97
+ self.anomaly_detector = AnomalyDetectorLayer(self._config)
98
+ self.wallet_fortress = WalletFortressLayer(self._config, self._catalog)
99
+ self.multimodal = MultimodalScanner(self._config, self._catalog)
100
+ self.llm_judge = LLMJudgeLayer(self._config)
101
+
102
+ logger.info(
103
+ "[ZugaShield] Initialized: %d signatures loaded, %d layers active",
104
+ self._catalog._total_signatures,
105
+ sum(
106
+ [
107
+ self._config.perimeter_enabled,
108
+ self._config.prompt_armor_enabled,
109
+ self._config.tool_guard_enabled,
110
+ self._config.memory_sentinel_enabled,
111
+ self._config.exfiltration_guard_enabled,
112
+ self._config.anomaly_detector_enabled,
113
+ self._config.wallet_fortress_enabled,
114
+ ]
115
+ ),
116
+ )
117
+
118
+ @property
119
+ def enabled(self) -> bool:
120
+ return self._config.enabled
121
+
122
+ @property
123
+ def config(self) -> ShieldConfig:
124
+ return self._config
125
+
126
+ @property
127
+ def catalog(self) -> ThreatCatalog:
128
+ return self._catalog
129
+
130
+ @property
131
+ def audit(self) -> ShieldAuditLogger:
132
+ return self._audit
133
+
134
+ # =========================================================================
135
+ # Layer 1: Perimeter (HTTP middleware)
136
+ # =========================================================================
137
+
138
+ async def check_request(
139
+ self,
140
+ path: str,
141
+ method: str = "GET",
142
+ content_length: int = 0,
143
+ body: Optional[str] = None,
144
+ headers: Optional[Dict[str, str]] = None,
145
+ client_ip: str = "unknown",
146
+ ) -> ShieldDecision:
147
+ """Check an incoming HTTP request (Layer 1)."""
148
+ if not self._config.enabled:
149
+ return allow_decision("shield_disabled")
150
+
151
+ decision = await self.perimeter.check(
152
+ path=path,
153
+ method=method,
154
+ content_length=content_length,
155
+ body=body,
156
+ headers=headers,
157
+ client_ip=client_ip,
158
+ )
159
+ self._audit.log(decision, {"path": path, "method": method, "client_ip": client_ip})
160
+ self._feed_anomaly(decision)
161
+ return decision
162
+
163
+ # =========================================================================
164
+ # Layer 2: Prompt Armor (injection defense)
165
+ # =========================================================================
166
+
167
+ async def check_prompt(
168
+ self,
169
+ user_message: str,
170
+ context: Optional[Dict] = None,
171
+ ) -> ShieldDecision:
172
+ """Check user message for prompt injection (Layer 2)."""
173
+ if not self._config.enabled:
174
+ return allow_decision("shield_disabled")
175
+
176
+ decision = await self.prompt_armor.check(user_message, context)
177
+
178
+ # Optional LLM judge escalation for ambiguous cases
179
+ if self.llm_judge.should_escalate(decision):
180
+ decision = await self.llm_judge.judge(user_message, decision)
181
+
182
+ self._audit.log(decision, {"source": "prompt", **(context or {})})
183
+ self._feed_anomaly(decision)
184
+ return decision
185
+
186
+ # =========================================================================
187
+ # Layer 3: Tool Guard (execution gating)
188
+ # =========================================================================
189
+
190
+ async def check_tool_call(
191
+ self,
192
+ tool_name: str,
193
+ params: Dict[str, Any],
194
+ session_id: str = "default",
195
+ ) -> ShieldDecision:
196
+ """Check a tool call before execution (Layer 3)."""
197
+ if not self._config.enabled:
198
+ return allow_decision("shield_disabled")
199
+
200
+ decision = await self.tool_guard.check(tool_name, params, session_id)
201
+ self._audit.log(decision, {"tool": tool_name, "session_id": session_id})
202
+ self._feed_anomaly(decision, session_id)
203
+ return decision
204
+
205
+ # =========================================================================
206
+ # Layer 4: Memory Sentinel (write + read paths)
207
+ # =========================================================================
208
+
209
+ async def check_memory_write(
210
+ self,
211
+ content: str,
212
+ memory_type: str = "",
213
+ importance: str = "",
214
+ source: str = "unknown",
215
+ user_id: str = "default",
216
+ tags: Optional[List[str]] = None,
217
+ ) -> ShieldDecision:
218
+ """Check memory content before storage (Layer 4 write)."""
219
+ if not self._config.enabled:
220
+ return allow_decision("shield_disabled")
221
+
222
+ decision = await self.memory_sentinel.check_write(
223
+ content=content,
224
+ memory_type=memory_type,
225
+ importance=importance,
226
+ source=source,
227
+ user_id=user_id,
228
+ tags=tags,
229
+ )
230
+ self._audit.log(decision, {"source": source, "memory_type": memory_type})
231
+ self._feed_anomaly(decision)
232
+ return decision
233
+
234
+ async def check_memory_recall(
235
+ self,
236
+ memories: List[Dict[str, Any]],
237
+ ) -> ShieldDecision:
238
+ """Check recalled memories before prompt injection (Layer 4 read)."""
239
+ if not self._config.enabled:
240
+ return allow_decision("shield_disabled")
241
+
242
+ decision = await self.memory_sentinel.check_recall(memories)
243
+ self._audit.log(decision, {"memory_count": len(memories)})
244
+ return decision
245
+
246
+ # =========================================================================
247
+ # Layer 4b: RAG Document Pre-Ingestion Scanning
248
+ # =========================================================================
249
+
250
+ async def check_document(
251
+ self,
252
+ content: str,
253
+ source: str = "external",
254
+ document_type: str = "",
255
+ ) -> ShieldDecision:
256
+ """Check an external document before RAG ingestion (Layer 4)."""
257
+ if not self._config.enabled:
258
+ return allow_decision("shield_disabled")
259
+
260
+ decision = await self.memory_sentinel.check_document(
261
+ content=content,
262
+ source=source,
263
+ document_type=document_type,
264
+ )
265
+ self._audit.log(decision, {"source": source, "document_type": document_type})
266
+ self._feed_anomaly(decision)
267
+ return decision
268
+
269
+ # =========================================================================
270
+ # Layer 5: Exfiltration Guard (output DLP)
271
+ # =========================================================================
272
+
273
+ async def check_output(
274
+ self,
275
+ output: str,
276
+ context: Optional[Dict] = None,
277
+ ) -> ShieldDecision:
278
+ """Check LLM response or tool output for data leakage (Layer 5)."""
279
+ if not self._config.enabled:
280
+ return allow_decision("shield_disabled")
281
+
282
+ decision = await self.exfiltration_guard.check(output, context)
283
+ self._audit.log(decision, context)
284
+ self._feed_anomaly(decision)
285
+ return decision
286
+
287
+ # =========================================================================
288
+ # Multimodal: Image-Based Injection Detection
289
+ # =========================================================================
290
+
291
+ async def check_image(
292
+ self,
293
+ image_path: Optional[str] = None,
294
+ alt_text: Optional[str] = None,
295
+ ocr_text: Optional[str] = None,
296
+ metadata: Optional[Dict[str, Any]] = None,
297
+ ) -> ShieldDecision:
298
+ """Check an image for injection payloads (multimodal defense)."""
299
+ if not self._config.enabled:
300
+ return allow_decision("shield_disabled")
301
+
302
+ decision = await self.multimodal.check_image(
303
+ image_path=image_path,
304
+ alt_text=alt_text,
305
+ ocr_text=ocr_text,
306
+ metadata=metadata,
307
+ )
308
+ self._audit.log(decision, {"source": "image"})
309
+ self._feed_anomaly(decision)
310
+ return decision
311
+
312
+ # =========================================================================
313
+ # Layer 7: Wallet Fortress (crypto protection)
314
+ # =========================================================================
315
+
316
+ async def check_transaction(
317
+ self,
318
+ tx_type: str = "send",
319
+ to_address: str = "",
320
+ amount: float = 0.0,
321
+ amount_usd: float = 0.0,
322
+ contract_data: Optional[str] = None,
323
+ function_sig: Optional[str] = None,
324
+ ) -> ShieldDecision:
325
+ """Check a wallet transaction (Layer 7)."""
326
+ if not self._config.enabled:
327
+ return allow_decision("shield_disabled")
328
+
329
+ decision = await self.wallet_fortress.check(
330
+ tx_type=tx_type,
331
+ to_address=to_address,
332
+ amount=amount,
333
+ amount_usd=amount_usd,
334
+ contract_data=contract_data,
335
+ function_sig=function_sig,
336
+ )
337
+ self._audit.log(decision, {"tx_type": tx_type, "amount_usd": amount_usd})
338
+ self._feed_anomaly(decision)
339
+ return decision
340
+
341
+ # =========================================================================
342
+ # Cross-layer: Anomaly Detection (Layer 6)
343
+ # =========================================================================
344
+
345
+ def get_session_risk(self, session_id: str = "default") -> AnomalyScore:
346
+ """Get current anomaly score for a session."""
347
+ return self.anomaly_detector.get_session_score(session_id)
348
+
349
+ def get_audit_log(self, limit: int = 100, layer: Optional[str] = None) -> List[Dict]:
350
+ """Get recent audit events."""
351
+ return self._audit.get_recent(limit=limit, layer=layer)
352
+
353
+ def get_dashboard_data(self) -> Dict:
354
+ """Get aggregated data for the security dashboard."""
355
+ return {
356
+ "enabled": self._config.enabled,
357
+ "strict_mode": self._config.strict_mode,
358
+ "catalog": self._catalog.get_stats(),
359
+ "audit": self._audit.get_stats(),
360
+ "layers": {
361
+ "perimeter": self.perimeter.get_stats(),
362
+ "prompt_armor": self.prompt_armor.get_stats(),
363
+ "tool_guard": self.tool_guard.get_stats(),
364
+ "memory_sentinel": self.memory_sentinel.get_stats(),
365
+ "exfiltration_guard": self.exfiltration_guard.get_stats(),
366
+ "anomaly_detector": self.anomaly_detector.get_stats(),
367
+ "wallet_fortress": self.wallet_fortress.get_stats(),
368
+ },
369
+ }
370
+
371
+ # =========================================================================
372
+ # Tool Definition Scanning (MCP injection defense)
373
+ # =========================================================================
374
+
375
+ def scan_tool_definitions(
376
+ self,
377
+ tools: List[Dict[str, Any]],
378
+ ) -> List[Dict[str, Any]]:
379
+ """
380
+ Scan tool definitions for injection payloads before sending to LLM.
381
+
382
+ Addresses CVE-2025-53773: malicious instructions hidden in tool
383
+ descriptions, parameter descriptions, or tool metadata.
384
+ """
385
+ if not self._config.enabled or not self._config.prompt_armor_enabled:
386
+ return tools
387
+
388
+ clean_tools = []
389
+ injection_keywords = re.compile(
390
+ r"(?:ignore|override|bypass|disable)\s+(?:all\s+)?(?:previous|prior|safety|security|filter|restriction|instruction|rule|guideline)",
391
+ re.I,
392
+ )
393
+ hidden_instruction = re.compile(
394
+ r"(?:actually|really|instead|secretly|hidden)\s*[,:]?\s*(?:you\s+)?(?:should|must|will|need\s+to)\s+(?:ignore|override|execute|bypass|follow\s+these)",
395
+ re.I,
396
+ )
397
+ role_override = re.compile(
398
+ r"(?:you\s+are\s+now|act\s+as|pretend\s+to\s+be|switch\s+to)\s+(?:a\s+)?(?:unrestricted|unfiltered|jailbroken|evil|DAN)",
399
+ re.I,
400
+ )
401
+
402
+ for tool in tools:
403
+ flagged = False
404
+ tool_name = tool.get("name", "unknown")
405
+
406
+ texts_to_scan = []
407
+ if "description" in tool:
408
+ texts_to_scan.append(("description", tool["description"]))
409
+
410
+ schema = tool.get("input_schema", {})
411
+ for param_name, param_def in schema.get("properties", {}).items():
412
+ if "description" in param_def:
413
+ texts_to_scan.append((f"param:{param_name}", param_def["description"]))
414
+
415
+ for field_name, text in texts_to_scan:
416
+ for pattern, desc in [
417
+ (injection_keywords, "injection keyword"),
418
+ (hidden_instruction, "hidden instruction"),
419
+ (role_override, "role override"),
420
+ ]:
421
+ match = pattern.search(text)
422
+ if match:
423
+ flagged = True
424
+ threat = ThreatDetection(
425
+ category=ThreatCategory.TOOL_EXPLOITATION,
426
+ level=ThreatLevel.CRITICAL,
427
+ verdict=ShieldVerdict.BLOCK,
428
+ description=f"Injection in tool definition '{tool_name}' field '{field_name}': {desc}",
429
+ evidence=match.group(0)[:200],
430
+ layer="tool_definition_scanner",
431
+ confidence=0.88,
432
+ suggested_action=f"Remove poisoned tool '{tool_name}' from definitions",
433
+ signature_id="TDS-INJECT",
434
+ )
435
+ self._audit.log(
436
+ ShieldDecision(
437
+ verdict=ShieldVerdict.BLOCK,
438
+ threats_detected=[threat],
439
+ layer="tool_definition_scanner",
440
+ elapsed_ms=0.0,
441
+ ),
442
+ {"tool_name": tool_name, "field": field_name},
443
+ )
444
+ logger.warning(
445
+ "[ZugaShield] BLOCKED poisoned tool definition: %s (field=%s, match=%s)",
446
+ tool_name,
447
+ field_name,
448
+ match.group(0)[:80],
449
+ )
450
+ break
451
+ if flagged:
452
+ break
453
+
454
+ if not flagged:
455
+ clean_tools.append(tool)
456
+
457
+ removed = len(tools) - len(clean_tools)
458
+ if removed:
459
+ logger.warning(
460
+ "[ZugaShield] Removed %d poisoned tool definition(s) from %d total",
461
+ removed,
462
+ len(tools),
463
+ )
464
+
465
+ return clean_tools
466
+
467
+ # =========================================================================
468
+ # Internal helpers
469
+ # =========================================================================
470
+
471
+ def _feed_anomaly(self, decision: ShieldDecision, session_id: str = "default") -> None:
472
+ """Feed detection events to the anomaly detector for correlation."""
473
+ for threat in decision.threats_detected:
474
+ self.anomaly_detector.record_event(session_id, threat)
475
+
476
+ def get_version_state(self) -> Dict[str, Any]:
477
+ """Snapshot current shield configuration for versioning."""
478
+ return {
479
+ "enabled_layers": {
480
+ "perimeter": self._config.perimeter_enabled,
481
+ "prompt_armor": self._config.prompt_armor_enabled,
482
+ "tool_guard": self._config.tool_guard_enabled,
483
+ "memory_sentinel": self._config.memory_sentinel_enabled,
484
+ "exfiltration_guard": self._config.exfiltration_guard_enabled,
485
+ "anomaly_detector": self._config.anomaly_detector_enabled,
486
+ "wallet_fortress": self._config.wallet_fortress_enabled,
487
+ },
488
+ "config": {
489
+ "fail_open": self._config.fail_open if hasattr(self._config, "fail_open") else True,
490
+ },
491
+ }
492
+
493
+
494
+ # =============================================================================
495
+ # Singleton
496
+ # =============================================================================
497
+
498
+ _shield: Optional[ZugaShield] = None
499
+
500
+
501
+ def get_zugashield() -> ZugaShield:
502
+ """Get or create the singleton ZugaShield instance."""
503
+ global _shield
504
+ if _shield is None:
505
+ _shield = ZugaShield()
506
+ return _shield
507
+
508
+
509
+ def reset_zugashield() -> None:
510
+ """Reset the singleton (for testing)."""
511
+ global _shield
512
+ _shield = None
513
+
514
+
515
+ __all__ = [
516
+ # Facade
517
+ "ZugaShield",
518
+ "get_zugashield",
519
+ "reset_zugashield",
520
+ "set_approval_provider",
521
+ "get_approval_provider",
522
+ # Types
523
+ "AnomalyScore",
524
+ "MemoryTrust",
525
+ "ShieldDecision",
526
+ "ShieldVerdict",
527
+ "ThreatCategory",
528
+ "ThreatDetection",
529
+ "ThreatLevel",
530
+ "ToolPolicy",
531
+ "allow_decision",
532
+ "block_decision",
533
+ # Config
534
+ "ShieldConfig",
535
+ # Catalog
536
+ "ThreatCatalog",
537
+ # Audit
538
+ "ShieldAuditLogger",
539
+ ]
zugashield/audit.py ADDED
@@ -0,0 +1,152 @@
1
+ """
2
+ ZugaShield - Audit Logger
3
+ ===========================
4
+
5
+ Security event logging and forensics.
6
+ Records all shield decisions for:
7
+ - Real-time dashboard display
8
+ - Post-incident forensics
9
+ - False positive analysis
10
+ - Performance monitoring
11
+
12
+ Uses in-memory ring buffer with optional database persistence.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from collections import deque
19
+ from dataclasses import asdict, dataclass
20
+ from datetime import datetime
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ from zugashield.types import (
24
+ ShieldDecision,
25
+ ShieldVerdict,
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # Maximum events in memory
31
+ _MAX_EVENTS = 10000
32
+
33
+
34
+ @dataclass
35
+ class AuditEvent:
36
+ """A single audit log entry."""
37
+
38
+ timestamp: str
39
+ layer: str
40
+ verdict: str
41
+ threat_count: int
42
+ max_level: str
43
+ elapsed_ms: float
44
+ details: Dict[str, Any]
45
+
46
+ def to_dict(self) -> Dict:
47
+ return asdict(self)
48
+
49
+
50
+ class ShieldAuditLogger:
51
+ """
52
+ Audit logger for ZugaShield events.
53
+
54
+ Maintains an in-memory ring buffer of recent events
55
+ and provides query/filter capabilities for the dashboard.
56
+ """
57
+
58
+ def __init__(self, max_events: int = _MAX_EVENTS) -> None:
59
+ self._events: deque = deque(maxlen=max_events)
60
+ self._counters = {
61
+ "total_checks": 0,
62
+ "total_blocks": 0,
63
+ "total_challenges": 0,
64
+ "total_sanitizations": 0,
65
+ "total_allows": 0,
66
+ }
67
+ self._layer_stats: Dict[str, Dict[str, int]] = {}
68
+
69
+ def log(self, decision: ShieldDecision, context: Optional[Dict] = None) -> None:
70
+ """
71
+ Log a shield decision.
72
+
73
+ Args:
74
+ decision: The ShieldDecision from any layer
75
+ context: Optional additional context (session_id, user_id, etc.)
76
+ """
77
+ self._counters["total_checks"] += 1
78
+
79
+ if decision.verdict == ShieldVerdict.BLOCK:
80
+ self._counters["total_blocks"] += 1
81
+ elif decision.verdict == ShieldVerdict.QUARANTINE:
82
+ self._counters["total_blocks"] += 1
83
+ elif decision.verdict == ShieldVerdict.CHALLENGE:
84
+ self._counters["total_challenges"] += 1
85
+ elif decision.verdict == ShieldVerdict.SANITIZE:
86
+ self._counters["total_sanitizations"] += 1
87
+ else:
88
+ self._counters["total_allows"] += 1
89
+
90
+ # Track per-layer stats
91
+ layer = decision.layer
92
+ if layer not in self._layer_stats:
93
+ self._layer_stats[layer] = {"checks": 0, "blocks": 0, "threats": 0}
94
+ self._layer_stats[layer]["checks"] += 1
95
+ self._layer_stats[layer]["threats"] += decision.threat_count
96
+ if decision.is_blocked:
97
+ self._layer_stats[layer]["blocks"] += 1
98
+
99
+ # Only log non-allow events to the ring buffer (saves space)
100
+ if decision.verdict != ShieldVerdict.ALLOW:
101
+ details = {
102
+ "threats": [
103
+ {
104
+ "category": t.category.value,
105
+ "level": t.level.value,
106
+ "description": t.description,
107
+ "evidence": t.evidence[:100],
108
+ "confidence": t.confidence,
109
+ "signature_id": t.signature_id,
110
+ }
111
+ for t in decision.threats_detected
112
+ ],
113
+ }
114
+ if context:
115
+ details["context"] = context
116
+
117
+ event = AuditEvent(
118
+ timestamp=datetime.utcnow().isoformat() + "Z",
119
+ layer=layer,
120
+ verdict=decision.verdict.value,
121
+ threat_count=decision.threat_count,
122
+ max_level=decision.max_threat_level.value,
123
+ elapsed_ms=round(decision.elapsed_ms, 2),
124
+ details=details,
125
+ )
126
+ self._events.append(event)
127
+
128
+ # Log to standard logger for syslog/file collection
129
+ if decision.is_blocked:
130
+ logger.warning(
131
+ "[ShieldAudit] BLOCKED by %s: %s (threats=%d, %.1fms)",
132
+ layer,
133
+ decision.threats_detected[0].description if decision.threats_detected else "unknown",
134
+ decision.threat_count,
135
+ decision.elapsed_ms,
136
+ )
137
+
138
+ def get_recent(self, limit: int = 100, layer: Optional[str] = None) -> List[Dict]:
139
+ """Get recent audit events."""
140
+ events = list(self._events)
141
+ if layer:
142
+ events = [e for e in events if e.layer == layer]
143
+ return [e.to_dict() for e in events[-limit:]]
144
+
145
+ def get_stats(self) -> Dict:
146
+ """Get overall audit statistics."""
147
+ return {
148
+ "counters": dict(self._counters),
149
+ "layer_stats": dict(self._layer_stats),
150
+ "buffer_size": len(self._events),
151
+ "buffer_capacity": self._events.maxlen,
152
+ }