genxai-framework 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.
- cli/__init__.py +3 -0
- cli/commands/__init__.py +6 -0
- cli/commands/approval.py +85 -0
- cli/commands/audit.py +127 -0
- cli/commands/metrics.py +25 -0
- cli/commands/tool.py +389 -0
- cli/main.py +32 -0
- genxai/__init__.py +81 -0
- genxai/api/__init__.py +5 -0
- genxai/api/app.py +21 -0
- genxai/config/__init__.py +5 -0
- genxai/config/settings.py +37 -0
- genxai/connectors/__init__.py +19 -0
- genxai/connectors/base.py +122 -0
- genxai/connectors/kafka.py +92 -0
- genxai/connectors/postgres_cdc.py +95 -0
- genxai/connectors/registry.py +44 -0
- genxai/connectors/sqs.py +94 -0
- genxai/connectors/webhook.py +73 -0
- genxai/core/__init__.py +37 -0
- genxai/core/agent/__init__.py +32 -0
- genxai/core/agent/base.py +206 -0
- genxai/core/agent/config_io.py +59 -0
- genxai/core/agent/registry.py +98 -0
- genxai/core/agent/runtime.py +970 -0
- genxai/core/communication/__init__.py +6 -0
- genxai/core/communication/collaboration.py +44 -0
- genxai/core/communication/message_bus.py +192 -0
- genxai/core/communication/protocols.py +35 -0
- genxai/core/execution/__init__.py +22 -0
- genxai/core/execution/metadata.py +181 -0
- genxai/core/execution/queue.py +201 -0
- genxai/core/graph/__init__.py +30 -0
- genxai/core/graph/checkpoints.py +77 -0
- genxai/core/graph/edges.py +131 -0
- genxai/core/graph/engine.py +813 -0
- genxai/core/graph/executor.py +516 -0
- genxai/core/graph/nodes.py +161 -0
- genxai/core/graph/trigger_runner.py +40 -0
- genxai/core/memory/__init__.py +19 -0
- genxai/core/memory/base.py +72 -0
- genxai/core/memory/embedding.py +327 -0
- genxai/core/memory/episodic.py +448 -0
- genxai/core/memory/long_term.py +467 -0
- genxai/core/memory/manager.py +543 -0
- genxai/core/memory/persistence.py +297 -0
- genxai/core/memory/procedural.py +461 -0
- genxai/core/memory/semantic.py +526 -0
- genxai/core/memory/shared.py +62 -0
- genxai/core/memory/short_term.py +303 -0
- genxai/core/memory/vector_store.py +508 -0
- genxai/core/memory/working.py +211 -0
- genxai/core/state/__init__.py +6 -0
- genxai/core/state/manager.py +293 -0
- genxai/core/state/schema.py +115 -0
- genxai/llm/__init__.py +14 -0
- genxai/llm/base.py +150 -0
- genxai/llm/factory.py +329 -0
- genxai/llm/providers/__init__.py +1 -0
- genxai/llm/providers/anthropic.py +249 -0
- genxai/llm/providers/cohere.py +274 -0
- genxai/llm/providers/google.py +334 -0
- genxai/llm/providers/ollama.py +147 -0
- genxai/llm/providers/openai.py +257 -0
- genxai/llm/routing.py +83 -0
- genxai/observability/__init__.py +6 -0
- genxai/observability/logging.py +327 -0
- genxai/observability/metrics.py +494 -0
- genxai/observability/tracing.py +372 -0
- genxai/performance/__init__.py +39 -0
- genxai/performance/cache.py +256 -0
- genxai/performance/pooling.py +289 -0
- genxai/security/audit.py +304 -0
- genxai/security/auth.py +315 -0
- genxai/security/cost_control.py +528 -0
- genxai/security/default_policies.py +44 -0
- genxai/security/jwt.py +142 -0
- genxai/security/oauth.py +226 -0
- genxai/security/pii.py +366 -0
- genxai/security/policy_engine.py +82 -0
- genxai/security/rate_limit.py +341 -0
- genxai/security/rbac.py +247 -0
- genxai/security/validation.py +218 -0
- genxai/tools/__init__.py +21 -0
- genxai/tools/base.py +383 -0
- genxai/tools/builtin/__init__.py +131 -0
- genxai/tools/builtin/communication/__init__.py +15 -0
- genxai/tools/builtin/communication/email_sender.py +159 -0
- genxai/tools/builtin/communication/notification_manager.py +167 -0
- genxai/tools/builtin/communication/slack_notifier.py +118 -0
- genxai/tools/builtin/communication/sms_sender.py +118 -0
- genxai/tools/builtin/communication/webhook_caller.py +136 -0
- genxai/tools/builtin/computation/__init__.py +15 -0
- genxai/tools/builtin/computation/calculator.py +101 -0
- genxai/tools/builtin/computation/code_executor.py +183 -0
- genxai/tools/builtin/computation/data_validator.py +259 -0
- genxai/tools/builtin/computation/hash_generator.py +129 -0
- genxai/tools/builtin/computation/regex_matcher.py +201 -0
- genxai/tools/builtin/data/__init__.py +15 -0
- genxai/tools/builtin/data/csv_processor.py +213 -0
- genxai/tools/builtin/data/data_transformer.py +299 -0
- genxai/tools/builtin/data/json_processor.py +233 -0
- genxai/tools/builtin/data/text_analyzer.py +288 -0
- genxai/tools/builtin/data/xml_processor.py +175 -0
- genxai/tools/builtin/database/__init__.py +15 -0
- genxai/tools/builtin/database/database_inspector.py +157 -0
- genxai/tools/builtin/database/mongodb_query.py +196 -0
- genxai/tools/builtin/database/redis_cache.py +167 -0
- genxai/tools/builtin/database/sql_query.py +145 -0
- genxai/tools/builtin/database/vector_search.py +163 -0
- genxai/tools/builtin/file/__init__.py +17 -0
- genxai/tools/builtin/file/directory_scanner.py +214 -0
- genxai/tools/builtin/file/file_compressor.py +237 -0
- genxai/tools/builtin/file/file_reader.py +102 -0
- genxai/tools/builtin/file/file_writer.py +122 -0
- genxai/tools/builtin/file/image_processor.py +186 -0
- genxai/tools/builtin/file/pdf_parser.py +144 -0
- genxai/tools/builtin/test/__init__.py +15 -0
- genxai/tools/builtin/test/async_simulator.py +62 -0
- genxai/tools/builtin/test/data_transformer.py +99 -0
- genxai/tools/builtin/test/error_generator.py +82 -0
- genxai/tools/builtin/test/simple_math.py +94 -0
- genxai/tools/builtin/test/string_processor.py +72 -0
- genxai/tools/builtin/web/__init__.py +15 -0
- genxai/tools/builtin/web/api_caller.py +161 -0
- genxai/tools/builtin/web/html_parser.py +330 -0
- genxai/tools/builtin/web/http_client.py +187 -0
- genxai/tools/builtin/web/url_validator.py +162 -0
- genxai/tools/builtin/web/web_scraper.py +170 -0
- genxai/tools/custom/my_test_tool_2.py +9 -0
- genxai/tools/dynamic.py +105 -0
- genxai/tools/mcp_server.py +167 -0
- genxai/tools/persistence/__init__.py +6 -0
- genxai/tools/persistence/models.py +55 -0
- genxai/tools/persistence/service.py +322 -0
- genxai/tools/registry.py +227 -0
- genxai/tools/security/__init__.py +11 -0
- genxai/tools/security/limits.py +214 -0
- genxai/tools/security/policy.py +20 -0
- genxai/tools/security/sandbox.py +248 -0
- genxai/tools/templates.py +435 -0
- genxai/triggers/__init__.py +19 -0
- genxai/triggers/base.py +104 -0
- genxai/triggers/file_watcher.py +75 -0
- genxai/triggers/queue.py +68 -0
- genxai/triggers/registry.py +82 -0
- genxai/triggers/schedule.py +66 -0
- genxai/triggers/webhook.py +68 -0
- genxai/utils/__init__.py +1 -0
- genxai/utils/tokens.py +295 -0
- genxai_framework-0.1.0.dist-info/METADATA +495 -0
- genxai_framework-0.1.0.dist-info/RECORD +156 -0
- genxai_framework-0.1.0.dist-info/WHEEL +5 -0
- genxai_framework-0.1.0.dist-info/entry_points.txt +2 -0
- genxai_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
- genxai_framework-0.1.0.dist-info/top_level.txt +2 -0
genxai/security/oauth.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""OAuth 2.0 integration for GenXAI."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OAuthProvider(ABC):
|
|
10
|
+
"""Base OAuth 2.0 provider."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
|
|
13
|
+
"""Initialize OAuth provider.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
client_id: OAuth client ID
|
|
17
|
+
client_secret: OAuth client secret
|
|
18
|
+
redirect_uri: Redirect URI after authentication
|
|
19
|
+
"""
|
|
20
|
+
self.client_id = client_id
|
|
21
|
+
self.client_secret = client_secret
|
|
22
|
+
self.redirect_uri = redirect_uri
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get_authorization_url(self, state: Optional[str] = None) -> str:
|
|
26
|
+
"""Get OAuth authorization URL.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
state: Optional state parameter
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Authorization URL
|
|
33
|
+
"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def exchange_code(self, code: str) -> Dict[str, Any]:
|
|
38
|
+
"""Exchange authorization code for tokens.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
code: Authorization code
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Token response with access_token, refresh_token, etc.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def get_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
50
|
+
"""Get user information.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
access_token: Access token
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
User information
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class GoogleOAuthProvider(OAuthProvider):
|
|
62
|
+
"""Google OAuth 2.0 provider."""
|
|
63
|
+
|
|
64
|
+
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
65
|
+
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
66
|
+
USER_INFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo"
|
|
67
|
+
|
|
68
|
+
def get_authorization_url(self, state: Optional[str] = None) -> str:
|
|
69
|
+
"""Get Google OAuth authorization URL."""
|
|
70
|
+
params = {
|
|
71
|
+
"client_id": self.client_id,
|
|
72
|
+
"redirect_uri": self.redirect_uri,
|
|
73
|
+
"response_type": "code",
|
|
74
|
+
"scope": "openid email profile",
|
|
75
|
+
"access_type": "offline",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if state:
|
|
79
|
+
params["state"] = state
|
|
80
|
+
|
|
81
|
+
return f"{self.AUTH_URL}?{urlencode(params)}"
|
|
82
|
+
|
|
83
|
+
def exchange_code(self, code: str) -> Dict[str, Any]:
|
|
84
|
+
"""Exchange authorization code for tokens."""
|
|
85
|
+
data = {
|
|
86
|
+
"client_id": self.client_id,
|
|
87
|
+
"client_secret": self.client_secret,
|
|
88
|
+
"code": code,
|
|
89
|
+
"redirect_uri": self.redirect_uri,
|
|
90
|
+
"grant_type": "authorization_code",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
response = requests.post(self.TOKEN_URL, data=data)
|
|
94
|
+
response.raise_for_status()
|
|
95
|
+
|
|
96
|
+
return response.json()
|
|
97
|
+
|
|
98
|
+
def get_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
99
|
+
"""Get user information from Google."""
|
|
100
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
|
101
|
+
response = requests.get(self.USER_INFO_URL, headers=headers)
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
|
|
104
|
+
return response.json()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class GitHubOAuthProvider(OAuthProvider):
|
|
108
|
+
"""GitHub OAuth 2.0 provider."""
|
|
109
|
+
|
|
110
|
+
AUTH_URL = "https://github.com/login/oauth/authorize"
|
|
111
|
+
TOKEN_URL = "https://github.com/login/oauth/access_token"
|
|
112
|
+
USER_INFO_URL = "https://api.github.com/user"
|
|
113
|
+
|
|
114
|
+
def get_authorization_url(self, state: Optional[str] = None) -> str:
|
|
115
|
+
"""Get GitHub OAuth authorization URL."""
|
|
116
|
+
params = {
|
|
117
|
+
"client_id": self.client_id,
|
|
118
|
+
"redirect_uri": self.redirect_uri,
|
|
119
|
+
"scope": "read:user user:email",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if state:
|
|
123
|
+
params["state"] = state
|
|
124
|
+
|
|
125
|
+
return f"{self.AUTH_URL}?{urlencode(params)}"
|
|
126
|
+
|
|
127
|
+
def exchange_code(self, code: str) -> Dict[str, Any]:
|
|
128
|
+
"""Exchange authorization code for tokens."""
|
|
129
|
+
data = {
|
|
130
|
+
"client_id": self.client_id,
|
|
131
|
+
"client_secret": self.client_secret,
|
|
132
|
+
"code": code,
|
|
133
|
+
"redirect_uri": self.redirect_uri,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
headers = {"Accept": "application/json"}
|
|
137
|
+
response = requests.post(self.TOKEN_URL, data=data, headers=headers)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
|
|
140
|
+
return response.json()
|
|
141
|
+
|
|
142
|
+
def get_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
143
|
+
"""Get user information from GitHub."""
|
|
144
|
+
headers = {
|
|
145
|
+
"Authorization": f"Bearer {access_token}",
|
|
146
|
+
"Accept": "application/json"
|
|
147
|
+
}
|
|
148
|
+
response = requests.get(self.USER_INFO_URL, headers=headers)
|
|
149
|
+
response.raise_for_status()
|
|
150
|
+
|
|
151
|
+
return response.json()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class MicrosoftOAuthProvider(OAuthProvider):
|
|
155
|
+
"""Microsoft OAuth 2.0 provider."""
|
|
156
|
+
|
|
157
|
+
AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
|
|
158
|
+
TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
|
159
|
+
USER_INFO_URL = "https://graph.microsoft.com/v1.0/me"
|
|
160
|
+
|
|
161
|
+
def get_authorization_url(self, state: Optional[str] = None) -> str:
|
|
162
|
+
"""Get Microsoft OAuth authorization URL."""
|
|
163
|
+
params = {
|
|
164
|
+
"client_id": self.client_id,
|
|
165
|
+
"redirect_uri": self.redirect_uri,
|
|
166
|
+
"response_type": "code",
|
|
167
|
+
"scope": "openid email profile User.Read",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if state:
|
|
171
|
+
params["state"] = state
|
|
172
|
+
|
|
173
|
+
return f"{self.AUTH_URL}?{urlencode(params)}"
|
|
174
|
+
|
|
175
|
+
def exchange_code(self, code: str) -> Dict[str, Any]:
|
|
176
|
+
"""Exchange authorization code for tokens."""
|
|
177
|
+
data = {
|
|
178
|
+
"client_id": self.client_id,
|
|
179
|
+
"client_secret": self.client_secret,
|
|
180
|
+
"code": code,
|
|
181
|
+
"redirect_uri": self.redirect_uri,
|
|
182
|
+
"grant_type": "authorization_code",
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
response = requests.post(self.TOKEN_URL, data=data)
|
|
186
|
+
response.raise_for_status()
|
|
187
|
+
|
|
188
|
+
return response.json()
|
|
189
|
+
|
|
190
|
+
def get_user_info(self, access_token: str) -> Dict[str, Any]:
|
|
191
|
+
"""Get user information from Microsoft."""
|
|
192
|
+
headers = {"Authorization": f"Bearer {access_token}"}
|
|
193
|
+
response = requests.get(self.USER_INFO_URL, headers=headers)
|
|
194
|
+
response.raise_for_status()
|
|
195
|
+
|
|
196
|
+
return response.json()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def create_oauth_provider(
|
|
200
|
+
provider: str,
|
|
201
|
+
client_id: str,
|
|
202
|
+
client_secret: str,
|
|
203
|
+
redirect_uri: str
|
|
204
|
+
) -> OAuthProvider:
|
|
205
|
+
"""Create OAuth provider instance.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
provider: Provider name (google, github, microsoft)
|
|
209
|
+
client_id: OAuth client ID
|
|
210
|
+
client_secret: OAuth client secret
|
|
211
|
+
redirect_uri: Redirect URI
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
OAuthProvider instance
|
|
215
|
+
"""
|
|
216
|
+
providers = {
|
|
217
|
+
"google": GoogleOAuthProvider,
|
|
218
|
+
"github": GitHubOAuthProvider,
|
|
219
|
+
"microsoft": MicrosoftOAuthProvider,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
provider_class = providers.get(provider.lower())
|
|
223
|
+
if not provider_class:
|
|
224
|
+
raise ValueError(f"Unknown OAuth provider: {provider}")
|
|
225
|
+
|
|
226
|
+
return provider_class(client_id, client_secret, redirect_uri)
|
genxai/security/pii.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""PII detection and redaction for GenXAI."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# PII patterns
|
|
10
|
+
PII_PATTERNS = {
|
|
11
|
+
"email": r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
|
|
12
|
+
"phone": r'\b(?:\+?1[-.]?)?\(?([0-9]{3})\)?[-.]?([0-9]{3})[-.]?([0-9]{4})\b',
|
|
13
|
+
"ssn": r'\b\d{3}-\d{2}-\d{4}\b',
|
|
14
|
+
"credit_card": r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b',
|
|
15
|
+
"ip_address": r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b',
|
|
16
|
+
"api_key": r'\b[A-Za-z0-9]{32,}\b',
|
|
17
|
+
"passport": r'\b[A-Z]{1,2}\d{6,9}\b',
|
|
18
|
+
"driver_license": r'\b[A-Z]{1,2}\d{5,8}\b',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class PIIMatch:
|
|
24
|
+
"""PII match result."""
|
|
25
|
+
pii_type: str
|
|
26
|
+
value: str
|
|
27
|
+
start: int
|
|
28
|
+
end: int
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PIIDetector:
|
|
32
|
+
"""Detect PII in text."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, patterns: Optional[Dict[str, str]] = None):
|
|
35
|
+
"""Initialize PII detector.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
patterns: Custom PII patterns (default: use built-in patterns)
|
|
39
|
+
"""
|
|
40
|
+
self.patterns = patterns or PII_PATTERNS
|
|
41
|
+
|
|
42
|
+
def detect(self, text: str) -> List[PIIMatch]:
|
|
43
|
+
"""Detect all PII in text.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
text: Text to scan
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of PII matches
|
|
50
|
+
"""
|
|
51
|
+
matches = []
|
|
52
|
+
|
|
53
|
+
for pii_type, pattern in self.patterns.items():
|
|
54
|
+
for match in re.finditer(pattern, text):
|
|
55
|
+
matches.append(PIIMatch(
|
|
56
|
+
pii_type=pii_type,
|
|
57
|
+
value=match.group(),
|
|
58
|
+
start=match.start(),
|
|
59
|
+
end=match.end()
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
# Sort by position
|
|
63
|
+
matches.sort(key=lambda x: x.start)
|
|
64
|
+
|
|
65
|
+
return matches
|
|
66
|
+
|
|
67
|
+
def detect_type(self, text: str, pii_type: str) -> List[PIIMatch]:
|
|
68
|
+
"""Detect specific PII type.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
text: Text to scan
|
|
72
|
+
pii_type: PII type to detect
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
List of PII matches
|
|
76
|
+
"""
|
|
77
|
+
if pii_type not in self.patterns:
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
pattern = self.patterns[pii_type]
|
|
81
|
+
matches = []
|
|
82
|
+
|
|
83
|
+
for match in re.finditer(pattern, text):
|
|
84
|
+
matches.append(PIIMatch(
|
|
85
|
+
pii_type=pii_type,
|
|
86
|
+
value=match.group(),
|
|
87
|
+
start=match.start(),
|
|
88
|
+
end=match.end()
|
|
89
|
+
))
|
|
90
|
+
|
|
91
|
+
return matches
|
|
92
|
+
|
|
93
|
+
def has_pii(self, text: str) -> bool:
|
|
94
|
+
"""Check if text contains PII.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
text: Text to check
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
True if PII detected
|
|
101
|
+
"""
|
|
102
|
+
for pattern in self.patterns.values():
|
|
103
|
+
if re.search(pattern, text):
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PIIRedactor:
|
|
110
|
+
"""Redact PII from text."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, detector: Optional[PIIDetector] = None):
|
|
113
|
+
"""Initialize PII redactor.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
detector: PII detector (default: create new detector)
|
|
117
|
+
"""
|
|
118
|
+
self.detector = detector or PIIDetector()
|
|
119
|
+
|
|
120
|
+
def redact(
|
|
121
|
+
self,
|
|
122
|
+
text: str,
|
|
123
|
+
replacement: str = "***REDACTED***",
|
|
124
|
+
pii_types: Optional[List[str]] = None
|
|
125
|
+
) -> str:
|
|
126
|
+
"""Redact all PII.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
text: Text to redact
|
|
130
|
+
replacement: Replacement string
|
|
131
|
+
pii_types: Specific PII types to redact (default: all)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Redacted text
|
|
135
|
+
"""
|
|
136
|
+
matches = self.detector.detect(text)
|
|
137
|
+
|
|
138
|
+
# Filter by PII types if specified
|
|
139
|
+
if pii_types:
|
|
140
|
+
matches = [m for m in matches if m.pii_type in pii_types]
|
|
141
|
+
|
|
142
|
+
# Redact from end to start to preserve positions
|
|
143
|
+
for match in reversed(matches):
|
|
144
|
+
text = text[:match.start] + replacement + text[match.end:]
|
|
145
|
+
|
|
146
|
+
return text
|
|
147
|
+
|
|
148
|
+
def mask(self, text: str, pii_types: Optional[List[str]] = None) -> str:
|
|
149
|
+
"""Mask PII (show last 4 characters).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
text: Text to mask
|
|
153
|
+
pii_types: Specific PII types to mask (default: all)
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Masked text
|
|
157
|
+
"""
|
|
158
|
+
matches = self.detector.detect(text)
|
|
159
|
+
|
|
160
|
+
# Filter by PII types if specified
|
|
161
|
+
if pii_types:
|
|
162
|
+
matches = [m for m in matches if m.pii_type in pii_types]
|
|
163
|
+
|
|
164
|
+
# Mask from end to start to preserve positions
|
|
165
|
+
for match in reversed(matches):
|
|
166
|
+
value = match.value
|
|
167
|
+
|
|
168
|
+
if match.pii_type == "email":
|
|
169
|
+
# email@example.com -> e***@example.com
|
|
170
|
+
parts = value.split('@')
|
|
171
|
+
if len(parts) == 2:
|
|
172
|
+
masked = parts[0][0] + '***@' + parts[1]
|
|
173
|
+
else:
|
|
174
|
+
masked = '***'
|
|
175
|
+
|
|
176
|
+
elif match.pii_type in ["phone", "ssn", "credit_card"]:
|
|
177
|
+
# Show last 4 digits
|
|
178
|
+
masked = '***-***-' + value[-4:]
|
|
179
|
+
|
|
180
|
+
elif match.pii_type == "ip_address":
|
|
181
|
+
# Show first octet
|
|
182
|
+
parts = value.split('.')
|
|
183
|
+
masked = parts[0] + '.***.***.***'
|
|
184
|
+
|
|
185
|
+
else:
|
|
186
|
+
# Default: show last 4 characters
|
|
187
|
+
if len(value) > 4:
|
|
188
|
+
masked = '***' + value[-4:]
|
|
189
|
+
else:
|
|
190
|
+
masked = '***'
|
|
191
|
+
|
|
192
|
+
text = text[:match.start] + masked + text[match.end:]
|
|
193
|
+
|
|
194
|
+
return text
|
|
195
|
+
|
|
196
|
+
def redact_dict(
|
|
197
|
+
self,
|
|
198
|
+
data: Dict[str, Any],
|
|
199
|
+
replacement: str = "***REDACTED***"
|
|
200
|
+
) -> Dict[str, Any]:
|
|
201
|
+
"""Redact PII from dictionary recursively.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
data: Dictionary to redact
|
|
205
|
+
replacement: Replacement string
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Redacted dictionary
|
|
209
|
+
"""
|
|
210
|
+
result = {}
|
|
211
|
+
|
|
212
|
+
for key, value in data.items():
|
|
213
|
+
if isinstance(value, str):
|
|
214
|
+
result[key] = self.redact(value, replacement)
|
|
215
|
+
elif isinstance(value, dict):
|
|
216
|
+
result[key] = self.redact_dict(value, replacement)
|
|
217
|
+
elif isinstance(value, list):
|
|
218
|
+
result[key] = [
|
|
219
|
+
self.redact(item, replacement) if isinstance(item, str)
|
|
220
|
+
else self.redact_dict(item, replacement) if isinstance(item, dict)
|
|
221
|
+
else item
|
|
222
|
+
for item in value
|
|
223
|
+
]
|
|
224
|
+
else:
|
|
225
|
+
result[key] = value
|
|
226
|
+
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class PIIAuditLogger:
|
|
231
|
+
"""Log PII access for compliance."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, log_file: str = "pii_audit.log"):
|
|
234
|
+
"""Initialize PII audit logger.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
log_file: Path to audit log file
|
|
238
|
+
"""
|
|
239
|
+
self.log_file = log_file
|
|
240
|
+
|
|
241
|
+
def log_access(
|
|
242
|
+
self,
|
|
243
|
+
user_id: str,
|
|
244
|
+
pii_type: str,
|
|
245
|
+
action: str,
|
|
246
|
+
context: Dict[str, Any]
|
|
247
|
+
):
|
|
248
|
+
"""Log PII access.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
user_id: User ID
|
|
252
|
+
pii_type: PII type accessed
|
|
253
|
+
action: Action performed (read, write, delete)
|
|
254
|
+
context: Additional context
|
|
255
|
+
"""
|
|
256
|
+
log_entry = {
|
|
257
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
258
|
+
"user_id": user_id,
|
|
259
|
+
"pii_type": pii_type,
|
|
260
|
+
"action": action,
|
|
261
|
+
"context": context
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
with open(self.log_file, 'a') as f:
|
|
265
|
+
import json
|
|
266
|
+
f.write(json.dumps(log_entry) + '\n')
|
|
267
|
+
|
|
268
|
+
def get_logs(
|
|
269
|
+
self,
|
|
270
|
+
user_id: Optional[str] = None,
|
|
271
|
+
pii_type: Optional[str] = None,
|
|
272
|
+
start_time: Optional[datetime] = None,
|
|
273
|
+
end_time: Optional[datetime] = None
|
|
274
|
+
) -> List[Dict[str, Any]]:
|
|
275
|
+
"""Get audit logs with filters.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
user_id: Filter by user ID
|
|
279
|
+
pii_type: Filter by PII type
|
|
280
|
+
start_time: Filter by start time
|
|
281
|
+
end_time: Filter by end time
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List of log entries
|
|
285
|
+
"""
|
|
286
|
+
import json
|
|
287
|
+
logs = []
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
with open(self.log_file, 'r') as f:
|
|
291
|
+
for line in f:
|
|
292
|
+
try:
|
|
293
|
+
entry = json.loads(line.strip())
|
|
294
|
+
|
|
295
|
+
# Apply filters
|
|
296
|
+
if user_id and entry.get("user_id") != user_id:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
if pii_type and entry.get("pii_type") != pii_type:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
timestamp = datetime.fromisoformat(entry["timestamp"])
|
|
303
|
+
|
|
304
|
+
if start_time and timestamp < start_time:
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
if end_time and timestamp > end_time:
|
|
308
|
+
continue
|
|
309
|
+
|
|
310
|
+
logs.append(entry)
|
|
311
|
+
|
|
312
|
+
except json.JSONDecodeError:
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
except FileNotFoundError:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
return logs
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# Global instances
|
|
322
|
+
_pii_detector = None
|
|
323
|
+
_pii_redactor = None
|
|
324
|
+
_pii_audit_logger = None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_pii_detector() -> PIIDetector:
|
|
328
|
+
"""Get global PII detector.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
PIIDetector instance
|
|
332
|
+
"""
|
|
333
|
+
global _pii_detector
|
|
334
|
+
|
|
335
|
+
if _pii_detector is None:
|
|
336
|
+
_pii_detector = PIIDetector()
|
|
337
|
+
|
|
338
|
+
return _pii_detector
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_pii_redactor() -> PIIRedactor:
|
|
342
|
+
"""Get global PII redactor.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
PIIRedactor instance
|
|
346
|
+
"""
|
|
347
|
+
global _pii_redactor
|
|
348
|
+
|
|
349
|
+
if _pii_redactor is None:
|
|
350
|
+
_pii_redactor = PIIRedactor()
|
|
351
|
+
|
|
352
|
+
return _pii_redactor
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def get_pii_audit_logger() -> PIIAuditLogger:
|
|
356
|
+
"""Get global PII audit logger.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
PIIAuditLogger instance
|
|
360
|
+
"""
|
|
361
|
+
global _pii_audit_logger
|
|
362
|
+
|
|
363
|
+
if _pii_audit_logger is None:
|
|
364
|
+
_pii_audit_logger = PIIAuditLogger()
|
|
365
|
+
|
|
366
|
+
return _pii_audit_logger
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Resource-level policy engine for GenXAI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Dict, Optional, Set
|
|
8
|
+
|
|
9
|
+
from genxai.security.rbac import Permission, User, PermissionDenied
|
|
10
|
+
from genxai.security.audit import get_approval_service
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourceType(str, Enum):
|
|
14
|
+
"""Resource types covered by policy engine."""
|
|
15
|
+
|
|
16
|
+
AGENT = "agent"
|
|
17
|
+
TOOL = "tool"
|
|
18
|
+
WORKFLOW = "workflow"
|
|
19
|
+
MEMORY = "memory"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class AccessRule:
|
|
24
|
+
"""ACL rule for a resource."""
|
|
25
|
+
|
|
26
|
+
permissions: Set[Permission]
|
|
27
|
+
allowed_users: Optional[Set[str]] = None
|
|
28
|
+
requires_approval: bool = False
|
|
29
|
+
approval_request_id: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PolicyEngine:
|
|
33
|
+
"""Simple ACL-based policy engine."""
|
|
34
|
+
|
|
35
|
+
def __init__(self) -> None:
|
|
36
|
+
self._rules: Dict[str, AccessRule] = {}
|
|
37
|
+
|
|
38
|
+
def add_rule(self, resource_id: str, rule: AccessRule) -> None:
|
|
39
|
+
self._rules[resource_id] = rule
|
|
40
|
+
|
|
41
|
+
def check(self, user: User, resource_id: str, permission: Permission) -> None:
|
|
42
|
+
rule = self._rules.get(resource_id)
|
|
43
|
+
if rule is None:
|
|
44
|
+
if not user.has_permission(permission):
|
|
45
|
+
raise PermissionDenied(
|
|
46
|
+
f"User {user.user_id} missing permission: {permission.value}"
|
|
47
|
+
)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if rule.requires_approval:
|
|
51
|
+
if not rule.approval_request_id:
|
|
52
|
+
approval = get_approval_service().submit(
|
|
53
|
+
f"{permission.value}",
|
|
54
|
+
resource_id,
|
|
55
|
+
user.user_id,
|
|
56
|
+
)
|
|
57
|
+
rule.approval_request_id = approval.request_id
|
|
58
|
+
approval = get_approval_service().get(rule.approval_request_id)
|
|
59
|
+
if not approval or approval.status != "approved":
|
|
60
|
+
raise PermissionDenied(
|
|
61
|
+
f"Approval required for resource {resource_id}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if rule.allowed_users and user.user_id not in rule.allowed_users:
|
|
65
|
+
raise PermissionDenied(
|
|
66
|
+
f"User {user.user_id} not allowed for resource {resource_id}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if permission not in rule.permissions:
|
|
70
|
+
raise PermissionDenied(
|
|
71
|
+
f"Permission {permission.value} denied for resource {resource_id}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_policy_engine: Optional[PolicyEngine] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_policy_engine() -> PolicyEngine:
|
|
79
|
+
global _policy_engine
|
|
80
|
+
if _policy_engine is None:
|
|
81
|
+
_policy_engine = PolicyEngine()
|
|
82
|
+
return _policy_engine
|