sentinelgate-core 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.
- sentinelgate/__init__.py +55 -0
- sentinelgate/approval/__init__.py +20 -0
- sentinelgate/approval/handler.py +215 -0
- sentinelgate/audit/__init__.py +9 -0
- sentinelgate/audit/exporters.py +131 -0
- sentinelgate/audit/logger.py +156 -0
- sentinelgate/authorization/__init__.py +10 -0
- sentinelgate/authorization/argument_validator.py +218 -0
- sentinelgate/authorization/permissions.py +111 -0
- sentinelgate/authorization/tool_auth.py +110 -0
- sentinelgate/cli/__init__.py +4 -0
- sentinelgate/cli/main.py +430 -0
- sentinelgate/exceptions.py +107 -0
- sentinelgate/gateway.py +458 -0
- sentinelgate/policy/__init__.py +10 -0
- sentinelgate/policy/engine.py +487 -0
- sentinelgate/policy/evaluator.py +171 -0
- sentinelgate/policy/loader.py +316 -0
- sentinelgate/policy/models.py +157 -0
- sentinelgate/provenance/__init__.py +9 -0
- sentinelgate/provenance/taint.py +132 -0
- sentinelgate/provenance/trust.py +118 -0
- sentinelgate/risk/__init__.py +8 -0
- sentinelgate/risk/engine.py +206 -0
- sentinelgate/scanner/__init__.py +10 -0
- sentinelgate/scanner/injection.py +387 -0
- sentinelgate/scanner/jailbreak.py +256 -0
- sentinelgate/scanner/normalizer.py +212 -0
- sentinelgate/secrets/__init__.py +10 -0
- sentinelgate/secrets/detector.py +186 -0
- sentinelgate/secrets/pii.py +176 -0
- sentinelgate/secrets/redactor.py +103 -0
- sentinelgate/types.py +294 -0
- sentinelgate_core-0.1.0.dist-info/METADATA +239 -0
- sentinelgate_core-0.1.0.dist-info/RECORD +38 -0
- sentinelgate_core-0.1.0.dist-info/WHEEL +4 -0
- sentinelgate_core-0.1.0.dist-info/entry_points.txt +2 -0
- sentinelgate_core-0.1.0.dist-info/licenses/LICENSE +191 -0
sentinelgate/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
SentinelGate - Security enforcement layer for agentic AI applications.
|
|
6
|
+
|
|
7
|
+
Your agent decides. SentinelGate authorizes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from sentinelgate.exceptions import (
|
|
13
|
+
ApprovalDenied,
|
|
14
|
+
ApprovalRequired,
|
|
15
|
+
AuthorizationError,
|
|
16
|
+
PolicyError,
|
|
17
|
+
PolicyLoadError,
|
|
18
|
+
PolicyValidationError,
|
|
19
|
+
SecurityViolation,
|
|
20
|
+
SentinelGateError,
|
|
21
|
+
)
|
|
22
|
+
from sentinelgate.gateway import SecurityGateway
|
|
23
|
+
from sentinelgate.types import (
|
|
24
|
+
DecisionAction,
|
|
25
|
+
PolicyViolation,
|
|
26
|
+
RiskLevel,
|
|
27
|
+
SecurityDecision,
|
|
28
|
+
SecurityEvent,
|
|
29
|
+
ToolRequest,
|
|
30
|
+
TrustLevel,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Main entry point
|
|
37
|
+
"SecurityGateway",
|
|
38
|
+
# Types
|
|
39
|
+
"DecisionAction",
|
|
40
|
+
"PolicyViolation",
|
|
41
|
+
"RiskLevel",
|
|
42
|
+
"SecurityDecision",
|
|
43
|
+
"SecurityEvent",
|
|
44
|
+
"ToolRequest",
|
|
45
|
+
"TrustLevel",
|
|
46
|
+
# Exceptions
|
|
47
|
+
"ApprovalDenied",
|
|
48
|
+
"ApprovalRequired",
|
|
49
|
+
"AuthorizationError",
|
|
50
|
+
"PolicyError",
|
|
51
|
+
"PolicyLoadError",
|
|
52
|
+
"PolicyValidationError",
|
|
53
|
+
"SecurityViolation",
|
|
54
|
+
"SentinelGateError",
|
|
55
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Human-in-the-loop approval handlers."""
|
|
5
|
+
|
|
6
|
+
from sentinelgate.approval.handler import (
|
|
7
|
+
ApprovalHandler,
|
|
8
|
+
AutoApproveHandler,
|
|
9
|
+
AutoDenyHandler,
|
|
10
|
+
CallbackApprovalHandler,
|
|
11
|
+
ConsoleApprovalHandler,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ApprovalHandler",
|
|
16
|
+
"AutoApproveHandler",
|
|
17
|
+
"AutoDenyHandler",
|
|
18
|
+
"CallbackApprovalHandler",
|
|
19
|
+
"ConsoleApprovalHandler",
|
|
20
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Pluggable human-in-the-loop approval handlers.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from sentinelgate.approval import ConsoleApprovalHandler, CallbackApprovalHandler
|
|
8
|
+
|
|
9
|
+
# Interactive CLI approval
|
|
10
|
+
handler = ConsoleApprovalHandler()
|
|
11
|
+
|
|
12
|
+
# Custom callback approval
|
|
13
|
+
handler = CallbackApprovalHandler(my_approval_function)
|
|
14
|
+
|
|
15
|
+
# Auto-approve for development (logs a warning)
|
|
16
|
+
handler = AutoApproveHandler()
|
|
17
|
+
|
|
18
|
+
# Auto-deny for testing
|
|
19
|
+
handler = AutoDenyHandler()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
import warnings
|
|
26
|
+
from collections.abc import Callable
|
|
27
|
+
|
|
28
|
+
from sentinelgate.types import ApprovalResult, SecurityEvent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ApprovalHandler:
|
|
32
|
+
"""Base class for approval handlers.
|
|
33
|
+
|
|
34
|
+
Subclass this to implement custom approval workflows
|
|
35
|
+
(Slack, email, webhook, dashboard, etc.).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def request_approval(self, event: SecurityEvent) -> ApprovalResult:
|
|
39
|
+
"""Request human approval for a security event.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
event: The security event requiring approval.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
An ApprovalResult indicating whether the action was approved.
|
|
46
|
+
"""
|
|
47
|
+
raise NotImplementedError("Subclasses must implement request_approval()")
|
|
48
|
+
|
|
49
|
+
def _format_event_summary(self, event: SecurityEvent) -> str:
|
|
50
|
+
"""Format a security event into a human-readable summary."""
|
|
51
|
+
lines = [
|
|
52
|
+
"--- SentinelGate Approval Request ---",
|
|
53
|
+
f"Agent: {event.actor}",
|
|
54
|
+
f"Action: {event.action}",
|
|
55
|
+
f"Resource: {event.resource}",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if event.arguments:
|
|
59
|
+
lines.append("Arguments:")
|
|
60
|
+
for key, value in event.arguments.items():
|
|
61
|
+
val_str = str(value)
|
|
62
|
+
if len(val_str) > 100:
|
|
63
|
+
val_str = val_str[:100] + "..."
|
|
64
|
+
lines.append(f" {key}: {val_str}")
|
|
65
|
+
|
|
66
|
+
if event.decision:
|
|
67
|
+
lines.append(f"Risk Score: {event.decision.risk_score:.1f}")
|
|
68
|
+
lines.append(f"Risk Level: {event.decision.risk_level.value}")
|
|
69
|
+
if event.decision.reason:
|
|
70
|
+
lines.append(f"Reason: {event.decision.reason}")
|
|
71
|
+
|
|
72
|
+
lines.append("-" * 40)
|
|
73
|
+
return "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ConsoleApprovalHandler(ApprovalHandler):
|
|
77
|
+
"""Interactive CLI approval handler.
|
|
78
|
+
|
|
79
|
+
Displays the security event details and prompts for
|
|
80
|
+
approval via stdin.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def request_approval(self, event: SecurityEvent) -> ApprovalResult:
|
|
84
|
+
"""Prompt for approval via the console.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
event: The security event requiring approval.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
An ApprovalResult based on user input.
|
|
91
|
+
"""
|
|
92
|
+
summary = self._format_event_summary(event)
|
|
93
|
+
print("\n" + summary, file=sys.stderr)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
response = input("Approve this action? [y/N]: ").strip().lower()
|
|
97
|
+
except (EOFError, KeyboardInterrupt):
|
|
98
|
+
print("\nApproval denied (no input).", file=sys.stderr)
|
|
99
|
+
return ApprovalResult(
|
|
100
|
+
approved=False,
|
|
101
|
+
approver="console",
|
|
102
|
+
reason="No input received",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
approved = response in ("y", "yes")
|
|
106
|
+
|
|
107
|
+
reason = ""
|
|
108
|
+
if not approved and response not in ("n", "no", ""):
|
|
109
|
+
reason = response
|
|
110
|
+
|
|
111
|
+
return ApprovalResult(
|
|
112
|
+
approved=approved,
|
|
113
|
+
approver="console",
|
|
114
|
+
reason=reason or ("Approved by user" if approved else "Denied by user"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CallbackApprovalHandler(ApprovalHandler):
|
|
119
|
+
"""Approval handler that delegates to a callback function.
|
|
120
|
+
|
|
121
|
+
The callback receives the SecurityEvent and must return
|
|
122
|
+
an ApprovalResult or a bool.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, callback: Callable[[SecurityEvent], ApprovalResult | bool]) -> None:
|
|
126
|
+
"""Initialize with a callback function.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
callback: A function that takes a SecurityEvent and returns
|
|
130
|
+
either an ApprovalResult or a bool.
|
|
131
|
+
"""
|
|
132
|
+
if not callable(callback):
|
|
133
|
+
raise TypeError("callback must be callable")
|
|
134
|
+
self._callback = callback
|
|
135
|
+
|
|
136
|
+
def request_approval(self, event: SecurityEvent) -> ApprovalResult:
|
|
137
|
+
"""Delegate approval to the callback.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
event: The security event requiring approval.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
An ApprovalResult.
|
|
144
|
+
"""
|
|
145
|
+
result = self._callback(event)
|
|
146
|
+
|
|
147
|
+
if isinstance(result, ApprovalResult):
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
if isinstance(result, bool):
|
|
151
|
+
return ApprovalResult(
|
|
152
|
+
approved=result,
|
|
153
|
+
approver="callback",
|
|
154
|
+
reason="Approved by callback" if result else "Denied by callback",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Try to interpret as truthy/falsy
|
|
158
|
+
return ApprovalResult(
|
|
159
|
+
approved=bool(result),
|
|
160
|
+
approver="callback",
|
|
161
|
+
reason=f"Callback returned: {type(result).__name__}",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class AutoApproveHandler(ApprovalHandler):
|
|
166
|
+
"""Auto-approve handler for development environments.
|
|
167
|
+
|
|
168
|
+
Automatically approves all requests with a warning.
|
|
169
|
+
Do not use in production.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(self) -> None:
|
|
173
|
+
warnings.warn(
|
|
174
|
+
"AutoApproveHandler is enabled. All approval requests will be "
|
|
175
|
+
"automatically approved. Do not use this in production.",
|
|
176
|
+
UserWarning,
|
|
177
|
+
stacklevel=2,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def request_approval(self, event: SecurityEvent) -> ApprovalResult:
|
|
181
|
+
"""Auto-approve the request.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
event: The security event (ignored).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
An approved ApprovalResult.
|
|
188
|
+
"""
|
|
189
|
+
return ApprovalResult(
|
|
190
|
+
approved=True,
|
|
191
|
+
approver="auto_approve",
|
|
192
|
+
reason="Automatically approved (development mode)",
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class AutoDenyHandler(ApprovalHandler):
|
|
197
|
+
"""Auto-deny handler for testing environments.
|
|
198
|
+
|
|
199
|
+
Automatically denies all approval requests.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def request_approval(self, event: SecurityEvent) -> ApprovalResult:
|
|
203
|
+
"""Auto-deny the request.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
event: The security event (ignored).
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
A denied ApprovalResult.
|
|
210
|
+
"""
|
|
211
|
+
return ApprovalResult(
|
|
212
|
+
approved=False,
|
|
213
|
+
approver="auto_deny",
|
|
214
|
+
reason="Automatically denied (testing mode)",
|
|
215
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Audit and security logging."""
|
|
5
|
+
|
|
6
|
+
from sentinelgate.audit.exporters import CallbackExporter, ConsoleExporter, JSONFileExporter
|
|
7
|
+
from sentinelgate.audit.logger import AuditLogger
|
|
8
|
+
|
|
9
|
+
__all__ = ["AuditLogger", "ConsoleExporter", "JSONFileExporter", "CallbackExporter"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Audit event exporters."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TextIO
|
|
14
|
+
|
|
15
|
+
from sentinelgate.types import SecurityEvent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseExporter:
|
|
19
|
+
"""Base class for audit event exporters."""
|
|
20
|
+
|
|
21
|
+
def export(self, event: SecurityEvent) -> None:
|
|
22
|
+
"""Export a security event.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
event: The security event to export.
|
|
26
|
+
"""
|
|
27
|
+
raise NotImplementedError("Subclasses must implement export()")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConsoleExporter(BaseExporter):
|
|
31
|
+
"""Exports security events to the console with formatting."""
|
|
32
|
+
|
|
33
|
+
# Terminal colors (ANSI codes)
|
|
34
|
+
COLORS = {
|
|
35
|
+
"allow": "\033[92m", # green
|
|
36
|
+
"deny": "\033[91m", # red
|
|
37
|
+
"review": "\033[93m", # yellow
|
|
38
|
+
"redact": "\033[94m", # blue
|
|
39
|
+
"reset": "\033[0m",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
stream: TextIO | None = None,
|
|
45
|
+
use_color: bool = True,
|
|
46
|
+
verbose: bool = False,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize the console exporter.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
stream: Output stream. Defaults to stderr.
|
|
52
|
+
use_color: Whether to use ANSI colors.
|
|
53
|
+
verbose: Whether to include full event details.
|
|
54
|
+
"""
|
|
55
|
+
self._stream = stream or sys.stderr
|
|
56
|
+
self._use_color = use_color and hasattr(self._stream, "isatty") and self._stream.isatty()
|
|
57
|
+
self._verbose = verbose
|
|
58
|
+
|
|
59
|
+
def export(self, event: SecurityEvent) -> None:
|
|
60
|
+
"""Export a security event to the console."""
|
|
61
|
+
action_str = "UNKNOWN"
|
|
62
|
+
if event.decision:
|
|
63
|
+
action_str = event.decision.action.value.upper()
|
|
64
|
+
|
|
65
|
+
# Format the line
|
|
66
|
+
if self._use_color:
|
|
67
|
+
color = self.COLORS.get(action_str.lower(), self.COLORS["reset"])
|
|
68
|
+
reset = self.COLORS["reset"]
|
|
69
|
+
else:
|
|
70
|
+
color = ""
|
|
71
|
+
reset = ""
|
|
72
|
+
|
|
73
|
+
line = f"[SentinelGate] {color}{action_str.ljust(6)}{reset} | {event.actor} | {event.action} | {event.resource}"
|
|
74
|
+
|
|
75
|
+
if event.decision and event.decision.reason:
|
|
76
|
+
line += f" | {event.decision.reason}"
|
|
77
|
+
|
|
78
|
+
print(line, file=self._stream)
|
|
79
|
+
|
|
80
|
+
if self._verbose and event.arguments:
|
|
81
|
+
args_str = json.dumps(event.arguments, default=str)
|
|
82
|
+
if len(args_str) > 200:
|
|
83
|
+
args_str = args_str[:200] + "..."
|
|
84
|
+
print(f" Arguments: {args_str}", file=self._stream)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class JSONFileExporter(BaseExporter):
|
|
88
|
+
"""Exports security events to a JSON Lines file."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, filepath: str, flush_every: int = 1) -> None:
|
|
91
|
+
"""Initialize the JSON file exporter.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
filepath: Path to the output file.
|
|
95
|
+
flush_every: Flush after this many events.
|
|
96
|
+
"""
|
|
97
|
+
self._filepath = Path(filepath)
|
|
98
|
+
self._flush_every = flush_every
|
|
99
|
+
self._count = 0
|
|
100
|
+
self._lock = threading.Lock()
|
|
101
|
+
|
|
102
|
+
# Ensure parent directory exists
|
|
103
|
+
self._filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
def export(self, event: SecurityEvent) -> None:
|
|
106
|
+
"""Export a security event to the JSON lines file."""
|
|
107
|
+
with self._lock, open(self._filepath, "a", encoding="utf-8") as f:
|
|
108
|
+
json_line = json.dumps(event.to_dict(), default=str)
|
|
109
|
+
f.write(json_line + "\n")
|
|
110
|
+
|
|
111
|
+
self._count += 1
|
|
112
|
+
if self._count % self._flush_every == 0:
|
|
113
|
+
f.flush()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class CallbackExporter(BaseExporter):
|
|
117
|
+
"""Exports security events via a callback function."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, callback: Callable[[SecurityEvent], None]) -> None:
|
|
120
|
+
"""Initialize with a callback function.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
callback: Function to call for each event.
|
|
124
|
+
"""
|
|
125
|
+
if not callable(callback):
|
|
126
|
+
raise TypeError("callback must be callable")
|
|
127
|
+
self._callback = callback
|
|
128
|
+
|
|
129
|
+
def export(self, event: SecurityEvent) -> None:
|
|
130
|
+
"""Export a security event via the callback."""
|
|
131
|
+
self._callback(event)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Structured audit logger for security events."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import contextlib
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from sentinelgate.audit.exporters import BaseExporter
|
|
14
|
+
from sentinelgate.types import SecurityDecision, SecurityEvent
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuditLogger:
|
|
18
|
+
"""Thread-safe structured audit logger.
|
|
19
|
+
|
|
20
|
+
Records all security decisions and events for compliance,
|
|
21
|
+
debugging, and forensic analysis.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
exporters: list[BaseExporter] | None = None,
|
|
27
|
+
max_buffer_size: int = 1000,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize the audit logger.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
exporters: List of exporters for writing events.
|
|
33
|
+
Defaults to ConsoleExporter if none provided.
|
|
34
|
+
max_buffer_size: Maximum events to keep in memory.
|
|
35
|
+
"""
|
|
36
|
+
self._exporters = exporters or []
|
|
37
|
+
self._buffer: list[SecurityEvent] = []
|
|
38
|
+
self._max_buffer_size = max_buffer_size
|
|
39
|
+
self._lock = threading.Lock()
|
|
40
|
+
|
|
41
|
+
def log_event(self, event: SecurityEvent) -> None:
|
|
42
|
+
"""Log a security event.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
event: The SecurityEvent to record.
|
|
46
|
+
"""
|
|
47
|
+
with self._lock:
|
|
48
|
+
self._buffer.append(event)
|
|
49
|
+
if len(self._buffer) > self._max_buffer_size:
|
|
50
|
+
self._buffer.pop(0)
|
|
51
|
+
|
|
52
|
+
# Export to all configured exporters
|
|
53
|
+
for exporter in self._exporters:
|
|
54
|
+
with contextlib.suppress(Exception):
|
|
55
|
+
exporter.export(event)
|
|
56
|
+
|
|
57
|
+
def log_decision(
|
|
58
|
+
self,
|
|
59
|
+
actor: str,
|
|
60
|
+
action: str,
|
|
61
|
+
resource: str,
|
|
62
|
+
decision: SecurityDecision,
|
|
63
|
+
arguments: dict[str, Any] | None = None,
|
|
64
|
+
context: dict[str, Any] | None = None,
|
|
65
|
+
) -> SecurityEvent:
|
|
66
|
+
"""Log a security decision as an event.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
actor: The agent making the request.
|
|
70
|
+
action: The action being performed.
|
|
71
|
+
resource: The resource being accessed.
|
|
72
|
+
decision: The security decision.
|
|
73
|
+
arguments: Optional tool arguments.
|
|
74
|
+
context: Optional additional context.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The created SecurityEvent.
|
|
78
|
+
"""
|
|
79
|
+
event = SecurityEvent(
|
|
80
|
+
actor=actor,
|
|
81
|
+
action=action,
|
|
82
|
+
resource=resource,
|
|
83
|
+
arguments=arguments or {},
|
|
84
|
+
decision=decision,
|
|
85
|
+
context=context or {},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.log_event(event)
|
|
89
|
+
return event
|
|
90
|
+
|
|
91
|
+
def get_events(
|
|
92
|
+
self,
|
|
93
|
+
limit: int = 100,
|
|
94
|
+
actor: str | None = None,
|
|
95
|
+
action: str | None = None,
|
|
96
|
+
allowed_only: bool | None = None,
|
|
97
|
+
) -> list[SecurityEvent]:
|
|
98
|
+
"""Retrieve logged events with optional filtering.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
limit: Maximum number of events to return.
|
|
102
|
+
actor: Filter by actor name.
|
|
103
|
+
action: Filter by action.
|
|
104
|
+
allowed_only: Filter by decision (True=allowed, False=denied).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
List of matching SecurityEvent objects.
|
|
108
|
+
"""
|
|
109
|
+
with self._lock:
|
|
110
|
+
events = list(self._buffer)
|
|
111
|
+
|
|
112
|
+
# Apply filters
|
|
113
|
+
if actor:
|
|
114
|
+
events = [e for e in events if e.actor == actor]
|
|
115
|
+
if action:
|
|
116
|
+
events = [e for e in events if e.action == action]
|
|
117
|
+
if allowed_only is not None:
|
|
118
|
+
events = [
|
|
119
|
+
e for e in events
|
|
120
|
+
if e.decision is not None and e.decision.allowed == allowed_only
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Return most recent events
|
|
124
|
+
return events[-limit:]
|
|
125
|
+
|
|
126
|
+
def get_violations(self, limit: int = 100) -> list[SecurityEvent]:
|
|
127
|
+
"""Get events that were denied or had violations.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
limit: Maximum number of events to return.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of denied/violated SecurityEvent objects.
|
|
134
|
+
"""
|
|
135
|
+
return self.get_events(limit=limit, allowed_only=False)
|
|
136
|
+
|
|
137
|
+
def export_json(self, limit: int = 100) -> str:
|
|
138
|
+
"""Export events as a JSON string.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
limit: Maximum number of events to export.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
JSON string of events.
|
|
145
|
+
"""
|
|
146
|
+
events = self.get_events(limit=limit)
|
|
147
|
+
return json.dumps(
|
|
148
|
+
[e.to_dict() for e in events],
|
|
149
|
+
indent=2,
|
|
150
|
+
default=str,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def clear(self) -> None:
|
|
154
|
+
"""Clear all buffered events."""
|
|
155
|
+
with self._lock:
|
|
156
|
+
self._buffer.clear()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright 2024 SentinelGate Contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
|
|
4
|
+
"""Tool authorization components."""
|
|
5
|
+
|
|
6
|
+
from sentinelgate.authorization.argument_validator import ArgumentValidator
|
|
7
|
+
from sentinelgate.authorization.permissions import PermissionResolver
|
|
8
|
+
from sentinelgate.authorization.tool_auth import ToolAuthorizer
|
|
9
|
+
|
|
10
|
+
__all__ = ["ToolAuthorizer", "ArgumentValidator", "PermissionResolver"]
|