proxilion 0.0.1__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 (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,460 @@
1
+ """
2
+ IDOR (Insecure Direct Object Reference) protection for Proxilion.
3
+
4
+ This module provides protection against IDOR attacks, where attackers
5
+ attempt to access resources by manipulating object IDs in tool calls.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+ import threading
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass, field
15
+ from typing import Any
16
+
17
+ from proxilion.exceptions import IDORViolationError
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class ResourceScope:
24
+ """Defines the scope of resources a user can access."""
25
+ allowed_ids: set[str] = field(default_factory=set)
26
+ allowed_patterns: list[str] = field(default_factory=list)
27
+ scope_loader: Callable[[str], set[str]] | None = None
28
+
29
+
30
+ @dataclass
31
+ class IDPattern:
32
+ """Pattern for identifying object IDs in parameters."""
33
+ parameter_name: str
34
+ resource_type: str
35
+ pattern: str = r".*" # Regex to validate ID format
36
+ extractor: Callable[[Any], list[str]] | None = None
37
+
38
+
39
+ class IDORProtector:
40
+ """
41
+ Protects against Insecure Direct Object Reference attacks.
42
+
43
+ IDOR attacks occur when a user manipulates object IDs to access
44
+ resources they shouldn't have access to. This class validates
45
+ that object IDs in tool call arguments are within the user's
46
+ authorized scope.
47
+
48
+ Features:
49
+ - Register allowed resource scopes per user
50
+ - Define patterns to extract object IDs from arguments
51
+ - Validate access before tool execution
52
+ - Support for dynamic scope loading
53
+
54
+ Example:
55
+ >>> protector = IDORProtector()
56
+ >>>
57
+ >>> # Register user's allowed documents
58
+ >>> protector.register_scope(
59
+ ... user_id="user_123",
60
+ ... resource_type="document",
61
+ ... allowed_ids={"doc_1", "doc_2", "doc_3"},
62
+ ... )
63
+ >>>
64
+ >>> # Define where IDs appear in arguments
65
+ >>> protector.register_id_pattern(
66
+ ... parameter_name="document_id",
67
+ ... resource_type="document",
68
+ ... )
69
+ >>>
70
+ >>> # Validate access
71
+ >>> protector.validate_access("user_123", "document", "doc_1") # True
72
+ >>> protector.validate_access("user_123", "document", "doc_999") # False
73
+ """
74
+
75
+ # Common ID patterns for auto-detection
76
+ UUID_PATTERN = re.compile(
77
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
78
+ re.IGNORECASE,
79
+ )
80
+ NUMERIC_PATTERN = re.compile(r"^\d+$")
81
+ ALPHANUMERIC_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")
82
+
83
+ def __init__(self) -> None:
84
+ """Initialize the IDOR protector."""
85
+ self._scopes: dict[str, dict[str, ResourceScope]] = {}
86
+ self._patterns: dict[str, IDPattern] = {}
87
+ self._resource_patterns: dict[str, list[IDPattern]] = {}
88
+ self._lock = threading.RLock()
89
+
90
+ # Global scope loaders by resource type
91
+ self._scope_loaders: dict[str, Callable[[str], set[str]]] = {}
92
+
93
+ def register_scope(
94
+ self,
95
+ user_id: str,
96
+ resource_type: str,
97
+ allowed_ids: set[str] | None = None,
98
+ allowed_patterns: list[str] | None = None,
99
+ scope_loader: Callable[[str], set[str]] | None = None,
100
+ ) -> None:
101
+ """
102
+ Register a resource scope for a user.
103
+
104
+ Args:
105
+ user_id: The user's ID.
106
+ resource_type: Type of resource (e.g., "document", "account").
107
+ allowed_ids: Set of allowed object IDs.
108
+ allowed_patterns: Regex patterns for allowed IDs.
109
+ scope_loader: Function to dynamically load allowed IDs.
110
+
111
+ Example:
112
+ >>> protector.register_scope(
113
+ ... user_id="user_123",
114
+ ... resource_type="document",
115
+ ... allowed_ids={"doc_1", "doc_2"},
116
+ ... )
117
+ """
118
+ with self._lock:
119
+ if user_id not in self._scopes:
120
+ self._scopes[user_id] = {}
121
+
122
+ self._scopes[user_id][resource_type] = ResourceScope(
123
+ allowed_ids=allowed_ids or set(),
124
+ allowed_patterns=allowed_patterns or [],
125
+ scope_loader=scope_loader,
126
+ )
127
+
128
+ logger.debug(
129
+ f"Registered scope for user={user_id}, "
130
+ f"resource_type={resource_type}, ids={len(allowed_ids or set())}"
131
+ )
132
+
133
+ def register_scope_loader(
134
+ self,
135
+ resource_type: str,
136
+ loader: Callable[[str], set[str]],
137
+ ) -> None:
138
+ """
139
+ Register a global scope loader for a resource type.
140
+
141
+ The loader is called with a user_id and should return
142
+ the set of allowed IDs for that user.
143
+
144
+ Args:
145
+ resource_type: The resource type.
146
+ loader: Function that takes user_id and returns allowed IDs.
147
+
148
+ Example:
149
+ >>> def load_user_documents(user_id: str) -> set[str]:
150
+ ... return db.get_user_document_ids(user_id)
151
+ >>>
152
+ >>> protector.register_scope_loader("document", load_user_documents)
153
+ """
154
+ with self._lock:
155
+ self._scope_loaders[resource_type] = loader
156
+ logger.debug(f"Registered scope loader for resource_type={resource_type}")
157
+
158
+ def register_id_pattern(
159
+ self,
160
+ parameter_name: str,
161
+ resource_type: str,
162
+ pattern: str = r".*",
163
+ extractor: Callable[[Any], list[str]] | None = None,
164
+ ) -> None:
165
+ """
166
+ Register a pattern for extracting object IDs from arguments.
167
+
168
+ Args:
169
+ parameter_name: The parameter name that contains the ID.
170
+ resource_type: The type of resource the ID refers to.
171
+ pattern: Regex pattern to validate the ID format.
172
+ extractor: Custom function to extract IDs from the parameter value.
173
+
174
+ Example:
175
+ >>> # Simple ID parameter
176
+ >>> protector.register_id_pattern("document_id", "document")
177
+ >>>
178
+ >>> # Parameter with list of IDs
179
+ >>> protector.register_id_pattern(
180
+ ... "document_ids",
181
+ ... "document",
182
+ ... extractor=lambda v: v if isinstance(v, list) else [v],
183
+ ... )
184
+ """
185
+ with self._lock:
186
+ id_pattern = IDPattern(
187
+ parameter_name=parameter_name,
188
+ resource_type=resource_type,
189
+ pattern=pattern,
190
+ extractor=extractor,
191
+ )
192
+
193
+ self._patterns[parameter_name] = id_pattern
194
+
195
+ if resource_type not in self._resource_patterns:
196
+ self._resource_patterns[resource_type] = []
197
+ self._resource_patterns[resource_type].append(id_pattern)
198
+
199
+ logger.debug(
200
+ f"Registered ID pattern: {parameter_name} -> {resource_type}"
201
+ )
202
+
203
+ def validate_access(
204
+ self,
205
+ user_id: str,
206
+ resource_type: str,
207
+ object_id: str,
208
+ ) -> bool:
209
+ """
210
+ Validate that a user can access a specific object.
211
+
212
+ Args:
213
+ user_id: The user's ID.
214
+ resource_type: The type of resource.
215
+ object_id: The object ID being accessed.
216
+
217
+ Returns:
218
+ True if access is allowed, False otherwise.
219
+ """
220
+ with self._lock:
221
+ # Get user's scope for this resource type
222
+ user_scopes = self._scopes.get(user_id, {})
223
+ scope = user_scopes.get(resource_type)
224
+
225
+ # Try global scope loader if no user-specific scope
226
+ if scope is None and resource_type in self._scope_loaders:
227
+ loader = self._scope_loaders[resource_type]
228
+ try:
229
+ allowed_ids = loader(user_id)
230
+ return object_id in allowed_ids
231
+ except Exception as e:
232
+ logger.error(f"Scope loader failed: {e}")
233
+ return False
234
+
235
+ if scope is None:
236
+ # No scope defined - default deny
237
+ logger.debug(
238
+ f"No scope for user={user_id}, resource_type={resource_type}"
239
+ )
240
+ return False
241
+
242
+ # Check allowed IDs
243
+ if object_id in scope.allowed_ids:
244
+ return True
245
+
246
+ # Check patterns
247
+ for pattern in scope.allowed_patterns:
248
+ if re.match(pattern, object_id):
249
+ return True
250
+
251
+ # Try scope loader
252
+ if scope.scope_loader:
253
+ try:
254
+ dynamic_ids = scope.scope_loader(user_id)
255
+ if object_id in dynamic_ids:
256
+ return True
257
+ except Exception as e:
258
+ logger.error(f"Dynamic scope loader failed: {e}")
259
+
260
+ return False
261
+
262
+ def validate_arguments(
263
+ self,
264
+ user_id: str,
265
+ arguments: dict[str, Any],
266
+ ) -> list[tuple[str, str, str]]:
267
+ """
268
+ Validate all object IDs in tool arguments.
269
+
270
+ Scans the arguments for registered ID patterns and validates
271
+ each found ID against the user's scope.
272
+
273
+ Args:
274
+ user_id: The user's ID.
275
+ arguments: The tool call arguments.
276
+
277
+ Returns:
278
+ List of (parameter_name, resource_type, object_id) tuples
279
+ for IDs that failed validation.
280
+ """
281
+ violations: list[tuple[str, str, str]] = []
282
+
283
+ with self._lock:
284
+ for param_name, value in arguments.items():
285
+ pattern = self._patterns.get(param_name)
286
+ if pattern is None:
287
+ continue
288
+
289
+ # Extract IDs from the value
290
+ ids = self._extract_ids(value, pattern)
291
+
292
+ # Validate each ID
293
+ for object_id in ids:
294
+ if not self.validate_access(
295
+ user_id, pattern.resource_type, object_id
296
+ ):
297
+ violations.append(
298
+ (param_name, pattern.resource_type, object_id)
299
+ )
300
+
301
+ return violations
302
+
303
+ def check_arguments(
304
+ self,
305
+ user_id: str,
306
+ arguments: dict[str, Any],
307
+ ) -> None:
308
+ """
309
+ Check arguments and raise if any IDOR violations found.
310
+
311
+ Args:
312
+ user_id: The user's ID.
313
+ arguments: The tool call arguments.
314
+
315
+ Raises:
316
+ IDORViolationError: If any object ID is not in user's scope.
317
+ """
318
+ violations = self.validate_arguments(user_id, arguments)
319
+
320
+ if violations:
321
+ # Report first violation
322
+ param_name, resource_type, object_id = violations[0]
323
+ raise IDORViolationError(
324
+ user_id=user_id,
325
+ resource_type=resource_type,
326
+ object_id=object_id,
327
+ )
328
+
329
+ def _extract_ids(
330
+ self,
331
+ value: Any,
332
+ pattern: IDPattern,
333
+ ) -> list[str]:
334
+ """Extract object IDs from a parameter value."""
335
+ if pattern.extractor:
336
+ try:
337
+ return pattern.extractor(value)
338
+ except Exception:
339
+ return []
340
+
341
+ # Default extraction logic
342
+ if isinstance(value, str):
343
+ return [value]
344
+ elif isinstance(value, list):
345
+ return [str(v) for v in value if v is not None]
346
+ elif isinstance(value, dict):
347
+ # Look for common ID field names
348
+ for key in ("id", "ID", "_id", "object_id"):
349
+ if key in value:
350
+ return [str(value[key])]
351
+ return []
352
+ else:
353
+ return [str(value)] if value is not None else []
354
+
355
+ def auto_detect_ids(
356
+ self,
357
+ arguments: dict[str, Any],
358
+ ) -> dict[str, str]:
359
+ """
360
+ Auto-detect potential object IDs in arguments.
361
+
362
+ Scans arguments for values that look like object IDs
363
+ (UUIDs, numeric IDs, etc.).
364
+
365
+ Args:
366
+ arguments: The tool call arguments.
367
+
368
+ Returns:
369
+ Dictionary mapping parameter names to detected ID types.
370
+ """
371
+ detected: dict[str, str] = {}
372
+
373
+ for param_name, value in arguments.items():
374
+ if not isinstance(value, str):
375
+ continue
376
+
377
+ # Skip known non-ID parameters
378
+ if param_name in ("query", "content", "message", "text"):
379
+ continue
380
+
381
+ # Check for common ID patterns
382
+ if self.UUID_PATTERN.match(value):
383
+ detected[param_name] = "uuid"
384
+ elif self.NUMERIC_PATTERN.match(value) and len(value) <= 20:
385
+ detected[param_name] = "numeric"
386
+ elif (
387
+ self.ALPHANUMERIC_PATTERN.match(value) and
388
+ len(value) <= 50 and
389
+ any(c.isdigit() for c in value)
390
+ ):
391
+ detected[param_name] = "alphanumeric"
392
+
393
+ return detected
394
+
395
+ def clear_scope(
396
+ self,
397
+ user_id: str,
398
+ resource_type: str | None = None,
399
+ ) -> None:
400
+ """
401
+ Clear scope for a user.
402
+
403
+ Args:
404
+ user_id: The user's ID.
405
+ resource_type: Specific resource type to clear, or None for all.
406
+ """
407
+ with self._lock:
408
+ if user_id not in self._scopes:
409
+ return
410
+
411
+ if resource_type:
412
+ self._scopes[user_id].pop(resource_type, None)
413
+ else:
414
+ del self._scopes[user_id]
415
+
416
+ def add_to_scope(
417
+ self,
418
+ user_id: str,
419
+ resource_type: str,
420
+ object_ids: set[str],
421
+ ) -> None:
422
+ """
423
+ Add object IDs to a user's scope.
424
+
425
+ Args:
426
+ user_id: The user's ID.
427
+ resource_type: The resource type.
428
+ object_ids: IDs to add.
429
+ """
430
+ with self._lock:
431
+ if user_id not in self._scopes:
432
+ self._scopes[user_id] = {}
433
+
434
+ if resource_type not in self._scopes[user_id]:
435
+ self._scopes[user_id][resource_type] = ResourceScope()
436
+
437
+ self._scopes[user_id][resource_type].allowed_ids.update(object_ids)
438
+
439
+ def remove_from_scope(
440
+ self,
441
+ user_id: str,
442
+ resource_type: str,
443
+ object_ids: set[str],
444
+ ) -> None:
445
+ """
446
+ Remove object IDs from a user's scope.
447
+
448
+ Args:
449
+ user_id: The user's ID.
450
+ resource_type: The resource type.
451
+ object_ids: IDs to remove.
452
+ """
453
+ with self._lock:
454
+ if user_id not in self._scopes:
455
+ return
456
+
457
+ if resource_type not in self._scopes[user_id]:
458
+ return
459
+
460
+ self._scopes[user_id][resource_type].allowed_ids -= object_ids