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.
Files changed (38) hide show
  1. sentinelgate/__init__.py +55 -0
  2. sentinelgate/approval/__init__.py +20 -0
  3. sentinelgate/approval/handler.py +215 -0
  4. sentinelgate/audit/__init__.py +9 -0
  5. sentinelgate/audit/exporters.py +131 -0
  6. sentinelgate/audit/logger.py +156 -0
  7. sentinelgate/authorization/__init__.py +10 -0
  8. sentinelgate/authorization/argument_validator.py +218 -0
  9. sentinelgate/authorization/permissions.py +111 -0
  10. sentinelgate/authorization/tool_auth.py +110 -0
  11. sentinelgate/cli/__init__.py +4 -0
  12. sentinelgate/cli/main.py +430 -0
  13. sentinelgate/exceptions.py +107 -0
  14. sentinelgate/gateway.py +458 -0
  15. sentinelgate/policy/__init__.py +10 -0
  16. sentinelgate/policy/engine.py +487 -0
  17. sentinelgate/policy/evaluator.py +171 -0
  18. sentinelgate/policy/loader.py +316 -0
  19. sentinelgate/policy/models.py +157 -0
  20. sentinelgate/provenance/__init__.py +9 -0
  21. sentinelgate/provenance/taint.py +132 -0
  22. sentinelgate/provenance/trust.py +118 -0
  23. sentinelgate/risk/__init__.py +8 -0
  24. sentinelgate/risk/engine.py +206 -0
  25. sentinelgate/scanner/__init__.py +10 -0
  26. sentinelgate/scanner/injection.py +387 -0
  27. sentinelgate/scanner/jailbreak.py +256 -0
  28. sentinelgate/scanner/normalizer.py +212 -0
  29. sentinelgate/secrets/__init__.py +10 -0
  30. sentinelgate/secrets/detector.py +186 -0
  31. sentinelgate/secrets/pii.py +176 -0
  32. sentinelgate/secrets/redactor.py +103 -0
  33. sentinelgate/types.py +294 -0
  34. sentinelgate_core-0.1.0.dist-info/METADATA +239 -0
  35. sentinelgate_core-0.1.0.dist-info/RECORD +38 -0
  36. sentinelgate_core-0.1.0.dist-info/WHEEL +4 -0
  37. sentinelgate_core-0.1.0.dist-info/entry_points.txt +2 -0
  38. sentinelgate_core-0.1.0.dist-info/licenses/LICENSE +191 -0
@@ -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"]