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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|