omnimancer-cli 0.1.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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,511 @@
1
+ """Audit logger for tracking and monitoring agent security events."""
2
+
3
+ import hashlib
4
+ import json
5
+ import logging
6
+ import threading
7
+ from dataclasses import asdict, dataclass
8
+ from datetime import datetime, timezone
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from queue import Empty, Queue
12
+ from typing import Any, Dict, List, Optional
13
+
14
+
15
+ class AuditEventType(Enum):
16
+ """Types of security audit events."""
17
+
18
+ PERMISSION_CHECK = "permission_check"
19
+ PERMISSION_DENIED = "permission_denied"
20
+ COMMAND_EXECUTED = "command_executed"
21
+ COMMAND_BLOCKED = "command_blocked"
22
+ FILE_ACCESS = "file_access"
23
+ FILE_ACCESS_DENIED = "file_access_denied"
24
+ APPROVAL_REQUESTED = "approval_requested"
25
+ APPROVAL_GRANTED = "approval_granted"
26
+ APPROVAL_DENIED = "approval_denied"
27
+ SANDBOX_CREATED = "sandbox_created"
28
+ SANDBOX_VIOLATION = "sandbox_violation"
29
+ SECURITY_ALERT = "security_alert"
30
+ SYSTEM_EVENT = "system_event"
31
+
32
+
33
+ class AuditLevel(Enum):
34
+ """Audit logging levels."""
35
+
36
+ DEBUG = "debug"
37
+ INFO = "info"
38
+ WARNING = "warning"
39
+ ERROR = "error"
40
+ CRITICAL = "critical"
41
+
42
+
43
+ @dataclass
44
+ class AuditEvent:
45
+ """Represents a security audit event."""
46
+
47
+ timestamp: datetime
48
+ event_type: AuditEventType
49
+ level: AuditLevel
50
+ message: str
51
+ source: str = "security_manager"
52
+ user_id: Optional[str] = None
53
+ session_id: Optional[str] = None
54
+ operation_id: Optional[str] = None
55
+ metadata: Dict[str, Any] = None
56
+
57
+ def __post_init__(self):
58
+ if self.metadata is None:
59
+ self.metadata = {}
60
+
61
+ # Add event hash for integrity checking
62
+ self.metadata["event_hash"] = self._calculate_hash()
63
+
64
+ def _calculate_hash(self) -> str:
65
+ """Calculate hash for event integrity."""
66
+ # Create hashable content (excluding the hash itself)
67
+ content = {
68
+ "timestamp": self.timestamp.isoformat(),
69
+ "event_type": self.event_type.value,
70
+ "level": self.level.value,
71
+ "message": self.message,
72
+ "source": self.source,
73
+ "user_id": self.user_id,
74
+ "session_id": self.session_id,
75
+ "operation_id": self.operation_id,
76
+ }
77
+
78
+ # Include metadata except for event_hash
79
+ if self.metadata:
80
+ filtered_metadata = {
81
+ k: v for k, v in self.metadata.items() if k != "event_hash"
82
+ }
83
+ content["metadata"] = filtered_metadata
84
+
85
+ content_str = json.dumps(content, sort_keys=True, default=str)
86
+ return hashlib.sha256(content_str.encode()).hexdigest()[:16]
87
+
88
+ def to_dict(self) -> Dict[str, Any]:
89
+ """Convert event to dictionary."""
90
+ data = asdict(self)
91
+ data["timestamp"] = self.timestamp.isoformat()
92
+ data["event_type"] = self.event_type.value
93
+ data["level"] = self.level.value
94
+ return data
95
+
96
+ def to_json(self) -> str:
97
+ """Convert event to JSON string."""
98
+ return json.dumps(self.to_dict(), default=str)
99
+
100
+
101
+ class AuditLogger:
102
+ """Handles security audit logging and monitoring."""
103
+
104
+ def __init__(
105
+ self,
106
+ log_file: Optional[str] = None,
107
+ log_level: AuditLevel = AuditLevel.INFO,
108
+ max_file_size: int = 10 * 1024 * 1024, # 10MB
109
+ backup_count: int = 5,
110
+ enable_console: bool = True,
111
+ enable_async: bool = True,
112
+ ):
113
+
114
+ self.log_level = log_level
115
+ self.max_file_size = max_file_size
116
+ self.backup_count = backup_count
117
+ self.enable_console = enable_console
118
+ self.enable_async = enable_async
119
+
120
+ # Set up log file path
121
+ if log_file:
122
+ self.log_file = Path(log_file)
123
+ else:
124
+ log_dir = Path.home() / ".omnimancer" / "logs"
125
+ log_dir.mkdir(parents=True, exist_ok=True)
126
+ self.log_file = log_dir / "security_audit.log"
127
+
128
+ # Ensure log directory exists
129
+ self.log_file.parent.mkdir(parents=True, exist_ok=True)
130
+
131
+ # Set up Python logger
132
+ self.logger = self._setup_logger()
133
+
134
+ # Async logging setup
135
+ if self.enable_async:
136
+ self.log_queue = Queue()
137
+ self.log_thread = threading.Thread(
138
+ target=self._async_log_worker, daemon=True
139
+ )
140
+ self.log_thread.start()
141
+ else:
142
+ self.log_queue = None
143
+ self.log_thread = None
144
+
145
+ # Event statistics
146
+ self.event_counts = {event_type: 0 for event_type in AuditEventType}
147
+ self.level_counts = {level: 0 for level in AuditLevel}
148
+ self.start_time = datetime.now(timezone.utc)
149
+
150
+ # Log audit logger initialization
151
+ self.log_event(
152
+ AuditEventType.SYSTEM_EVENT,
153
+ AuditLevel.INFO,
154
+ "Security audit logger initialized",
155
+ metadata={
156
+ "log_file": str(self.log_file),
157
+ "log_level": self.log_level.value,
158
+ "async_enabled": self.enable_async,
159
+ },
160
+ )
161
+
162
+ def _setup_logger(self) -> logging.Logger:
163
+ """Set up Python logger for audit events."""
164
+
165
+ logger = logging.getLogger("omnimancer.security.audit")
166
+ logger.setLevel(logging.DEBUG)
167
+
168
+ # Clear existing handlers
169
+ logger.handlers.clear()
170
+
171
+ # File handler with rotation
172
+ from logging.handlers import RotatingFileHandler
173
+
174
+ file_handler = RotatingFileHandler(
175
+ self.log_file,
176
+ maxBytes=self.max_file_size,
177
+ backupCount=self.backup_count,
178
+ )
179
+
180
+ # Console handler
181
+ if self.enable_console:
182
+ console_handler = logging.StreamHandler()
183
+ console_handler.setLevel(
184
+ logging.WARNING
185
+ ) # Only warnings and above to console
186
+ console_formatter = logging.Formatter(
187
+ "%(asctime)s - SECURITY - %(levelname)s - %(message)s"
188
+ )
189
+ console_handler.setFormatter(console_formatter)
190
+ logger.addHandler(console_handler)
191
+
192
+ # Detailed file formatter
193
+ file_formatter = logging.Formatter(
194
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
195
+ )
196
+ file_handler.setFormatter(file_formatter)
197
+ logger.addHandler(file_handler)
198
+
199
+ return logger
200
+
201
+ def _async_log_worker(self) -> None:
202
+ """Worker thread for async logging."""
203
+
204
+ while True:
205
+ try:
206
+ event = self.log_queue.get(timeout=1.0)
207
+ if event is None: # Shutdown signal
208
+ break
209
+ self._write_event_sync(event)
210
+ self.log_queue.task_done()
211
+ except Empty:
212
+ continue
213
+ except Exception as e:
214
+ # Fallback to stderr for logging errors
215
+ print(f"Audit logging error: {e}", file=__import__("sys").stderr)
216
+
217
+ def log_event(
218
+ self,
219
+ event_type: AuditEventType,
220
+ level: AuditLevel,
221
+ message: str,
222
+ source: str = "security_manager",
223
+ user_id: Optional[str] = None,
224
+ session_id: Optional[str] = None,
225
+ operation_id: Optional[str] = None,
226
+ metadata: Optional[Dict[str, Any]] = None,
227
+ ) -> None:
228
+ """Log a security audit event."""
229
+
230
+ # Check if event should be logged based on level
231
+ if not self._should_log_level(level):
232
+ return
233
+
234
+ # Create audit event
235
+ event = AuditEvent(
236
+ timestamp=datetime.now(timezone.utc),
237
+ event_type=event_type,
238
+ level=level,
239
+ message=message,
240
+ source=source,
241
+ user_id=user_id,
242
+ session_id=session_id,
243
+ operation_id=operation_id,
244
+ metadata=metadata or {},
245
+ )
246
+
247
+ # Update statistics
248
+ self.event_counts[event_type] += 1
249
+ self.level_counts[level] += 1
250
+
251
+ # Log the event
252
+ if self.enable_async and self.log_queue:
253
+ self.log_queue.put(event)
254
+ else:
255
+ self._write_event_sync(event)
256
+
257
+ def _should_log_level(self, level: AuditLevel) -> bool:
258
+ """Check if event level should be logged."""
259
+
260
+ level_order = [
261
+ AuditLevel.DEBUG,
262
+ AuditLevel.INFO,
263
+ AuditLevel.WARNING,
264
+ AuditLevel.ERROR,
265
+ AuditLevel.CRITICAL,
266
+ ]
267
+
268
+ return level_order.index(level) >= level_order.index(self.log_level)
269
+
270
+ def _write_event_sync(self, event: AuditEvent) -> None:
271
+ """Write event to log file synchronously."""
272
+
273
+ try:
274
+ # Write to file as JSON
275
+ with open(self.log_file, "a", encoding="utf-8") as f:
276
+ f.write(event.to_json() + "\n")
277
+
278
+ # Also log to Python logger
279
+ python_level = self._audit_level_to_python_level(event.level)
280
+ log_message = f"[{event.event_type.value.upper()}] {event.message}"
281
+ if event.metadata:
282
+ log_message += f" | Metadata: {json.dumps(event.metadata, default=str)}"
283
+
284
+ self.logger.log(python_level, log_message)
285
+
286
+ except Exception as e:
287
+ # Fallback logging
288
+ print(
289
+ f"Failed to write audit event: {e}",
290
+ file=__import__("sys").stderr,
291
+ )
292
+ print(f"Event: {event.to_json()}", file=__import__("sys").stderr)
293
+
294
+ def _audit_level_to_python_level(self, level: AuditLevel) -> int:
295
+ """Convert audit level to Python logging level."""
296
+
297
+ mapping = {
298
+ AuditLevel.DEBUG: logging.DEBUG,
299
+ AuditLevel.INFO: logging.INFO,
300
+ AuditLevel.WARNING: logging.WARNING,
301
+ AuditLevel.ERROR: logging.ERROR,
302
+ AuditLevel.CRITICAL: logging.CRITICAL,
303
+ }
304
+
305
+ return mapping.get(level, logging.INFO)
306
+
307
+ def log_permission_check(
308
+ self,
309
+ operation: str,
310
+ path: Optional[str] = None,
311
+ allowed: bool = True,
312
+ reason: Optional[str] = None,
313
+ **kwargs,
314
+ ) -> None:
315
+ """Log a permission check event."""
316
+
317
+ event_type = (
318
+ AuditEventType.PERMISSION_CHECK
319
+ if allowed
320
+ else AuditEventType.PERMISSION_DENIED
321
+ )
322
+ level = AuditLevel.INFO if allowed else AuditLevel.WARNING
323
+
324
+ message = f"Permission {'granted' if allowed else 'denied'} for {operation}"
325
+ if path:
326
+ message += f" on {path}"
327
+
328
+ metadata = {
329
+ "operation": operation,
330
+ "path": path,
331
+ "allowed": allowed,
332
+ "reason": reason,
333
+ **kwargs,
334
+ }
335
+
336
+ self.log_event(event_type, level, message, metadata=metadata)
337
+
338
+ def log_command_execution(
339
+ self,
340
+ command: str,
341
+ success: bool = True,
342
+ exit_code: Optional[int] = None,
343
+ output: Optional[str] = None,
344
+ sandbox_id: Optional[str] = None,
345
+ **kwargs,
346
+ ) -> None:
347
+ """Log a command execution event."""
348
+
349
+ event_type = (
350
+ AuditEventType.COMMAND_EXECUTED
351
+ if success
352
+ else AuditEventType.COMMAND_BLOCKED
353
+ )
354
+ level = AuditLevel.INFO if success else AuditLevel.WARNING
355
+
356
+ message = f"Command {'executed' if success else 'blocked'}: {command}"
357
+
358
+ metadata = {
359
+ "command": command,
360
+ "success": success,
361
+ "exit_code": exit_code,
362
+ "sandbox_id": sandbox_id,
363
+ **kwargs,
364
+ }
365
+
366
+ # Don't log full output for security, just indicate if present
367
+ if output:
368
+ metadata["output_length"] = len(output)
369
+ metadata["output_preview"] = output[:100] if len(output) > 100 else output
370
+
371
+ self.log_event(event_type, level, message, metadata=metadata)
372
+
373
+ def log_file_access(
374
+ self,
375
+ path: str,
376
+ operation: str,
377
+ allowed: bool = True,
378
+ file_size: Optional[int] = None,
379
+ **kwargs,
380
+ ) -> None:
381
+ """Log a file access event."""
382
+
383
+ event_type = (
384
+ AuditEventType.FILE_ACCESS if allowed else AuditEventType.FILE_ACCESS_DENIED
385
+ )
386
+ level = AuditLevel.INFO if allowed else AuditLevel.WARNING
387
+
388
+ message = f"File {operation} {'allowed' if allowed else 'denied'}: {path}"
389
+
390
+ metadata = {
391
+ "path": path,
392
+ "operation": operation,
393
+ "allowed": allowed,
394
+ "file_size": file_size,
395
+ **kwargs,
396
+ }
397
+
398
+ self.log_event(event_type, level, message, metadata=metadata)
399
+
400
+ def log_security_alert(
401
+ self,
402
+ alert_type: str,
403
+ description: str,
404
+ severity: AuditLevel = AuditLevel.WARNING,
405
+ **kwargs,
406
+ ) -> None:
407
+ """Log a security alert."""
408
+
409
+ message = f"Security alert: {alert_type} - {description}"
410
+
411
+ metadata = {
412
+ "alert_type": alert_type,
413
+ "description": description,
414
+ **kwargs,
415
+ }
416
+
417
+ self.log_event(
418
+ AuditEventType.SECURITY_ALERT, severity, message, metadata=metadata
419
+ )
420
+
421
+ def get_recent_events(
422
+ self,
423
+ count: int = 100,
424
+ event_type: Optional[AuditEventType] = None,
425
+ level: Optional[AuditLevel] = None,
426
+ ) -> List[AuditEvent]:
427
+ """Get recent audit events from the log file."""
428
+
429
+ events = []
430
+
431
+ try:
432
+ with open(self.log_file, "r", encoding="utf-8") as f:
433
+ lines = f.readlines()
434
+
435
+ # Process lines in reverse order (most recent first)
436
+ for line in reversed(
437
+ lines[-count * 2 :]
438
+ ): # Read more lines to account for filtering
439
+ try:
440
+ event_data = json.loads(line.strip())
441
+
442
+ # Recreate AuditEvent object
443
+ event = AuditEvent(
444
+ timestamp=datetime.fromisoformat(event_data["timestamp"]),
445
+ event_type=AuditEventType(event_data["event_type"]),
446
+ level=AuditLevel(event_data["level"]),
447
+ message=event_data["message"],
448
+ source=event_data.get("source", "unknown"),
449
+ user_id=event_data.get("user_id"),
450
+ session_id=event_data.get("session_id"),
451
+ operation_id=event_data.get("operation_id"),
452
+ metadata=event_data.get("metadata", {}),
453
+ )
454
+
455
+ # Apply filters
456
+ if event_type and event.event_type != event_type:
457
+ continue
458
+ if level and event.level != level:
459
+ continue
460
+
461
+ events.append(event)
462
+
463
+ if len(events) >= count:
464
+ break
465
+
466
+ except (json.JSONDecodeError, KeyError, ValueError):
467
+ continue
468
+
469
+ except (FileNotFoundError, PermissionError):
470
+ pass
471
+
472
+ return events
473
+
474
+ def get_statistics(self) -> Dict[str, Any]:
475
+ """Get audit logging statistics."""
476
+
477
+ uptime = datetime.now(timezone.utc) - self.start_time
478
+
479
+ return {
480
+ "uptime_seconds": int(uptime.total_seconds()),
481
+ "log_file": str(self.log_file),
482
+ "log_level": self.log_level.value,
483
+ "async_enabled": self.enable_async,
484
+ "total_events": sum(self.event_counts.values()),
485
+ "event_type_counts": {k.value: v for k, v in self.event_counts.items()},
486
+ "level_counts": {k.value: v for k, v in self.level_counts.items()},
487
+ "log_file_size": (
488
+ self.log_file.stat().st_size if self.log_file.exists() else 0
489
+ ),
490
+ }
491
+
492
+ def shutdown(self) -> None:
493
+ """Shutdown the audit logger gracefully."""
494
+
495
+ self.log_event(
496
+ AuditEventType.SYSTEM_EVENT,
497
+ AuditLevel.INFO,
498
+ "Security audit logger shutting down",
499
+ )
500
+
501
+ if self.enable_async and self.log_queue and self.log_thread:
502
+ # Signal shutdown and wait for thread
503
+ self.log_queue.put(None)
504
+ self.log_thread.join(timeout=5.0)
505
+
506
+ def __del__(self):
507
+ """Destructor to ensure proper cleanup."""
508
+ try:
509
+ self.shutdown()
510
+ except:
511
+ pass