protegrity-ai-developer-python 1.2.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 (53) hide show
  1. appython/__init__.py +12 -0
  2. appython/protector.py +554 -0
  3. appython/service/auth_provider.py +273 -0
  4. appython/service/auth_token_provider.py +45 -0
  5. appython/service/config.py +209 -0
  6. appython/service/payload_builder.py +141 -0
  7. appython/service/request_handler.py +115 -0
  8. appython/service/response_handler.py +78 -0
  9. appython/stats/__init__.py +3 -0
  10. appython/stats/collector.py +90 -0
  11. appython/stats/writer.py +185 -0
  12. appython/utils/codec_helper.py +86 -0
  13. appython/utils/constants.py +246 -0
  14. appython/utils/exceptions.py +141 -0
  15. appython/utils/input_preprocessor.py +325 -0
  16. appython/utils/output_postprocessor.py +99 -0
  17. protegrity_ai_developer_python-1.2.1.dist-info/METADATA +428 -0
  18. protegrity_ai_developer_python-1.2.1.dist-info/RECORD +53 -0
  19. protegrity_ai_developer_python-1.2.1.dist-info/WHEEL +5 -0
  20. protegrity_ai_developer_python-1.2.1.dist-info/entry_points.txt +2 -0
  21. protegrity_ai_developer_python-1.2.1.dist-info/licenses/LICENSE +21 -0
  22. protegrity_ai_developer_python-1.2.1.dist-info/top_level.txt +3 -0
  23. protegrity_developer_python/__init__.py +4 -0
  24. protegrity_developer_python/scan.py +37 -0
  25. protegrity_developer_python/securefind.py +83 -0
  26. protegrity_developer_python/utils/ccn_processing.py +59 -0
  27. protegrity_developer_python/utils/config.py +60 -0
  28. protegrity_developer_python/utils/constants.py +123 -0
  29. protegrity_developer_python/utils/discover.py +49 -0
  30. protegrity_developer_python/utils/logger.py +23 -0
  31. protegrity_developer_python/utils/pii_processing.py +291 -0
  32. protegrity_developer_python/utils/protector.py +23 -0
  33. protegrity_developer_python/utils/semantic_guardrails.py +240 -0
  34. protegrity_developer_python/utils/transform.py +66 -0
  35. pty_migrate/__init__.py +1 -0
  36. pty_migrate/check_cmd.py +871 -0
  37. pty_migrate/cli.py +93 -0
  38. pty_migrate/config.py +127 -0
  39. pty_migrate/create_policy_cmd.py +795 -0
  40. pty_migrate/payloads/__init__.py +51 -0
  41. pty_migrate/payloads/alphabets.json +42 -0
  42. pty_migrate/payloads/dataelements.json +342 -0
  43. pty_migrate/payloads/datastores.json +7 -0
  44. pty_migrate/payloads/deploy_policy_ta.json +1 -0
  45. pty_migrate/payloads/masks.json +18 -0
  46. pty_migrate/payloads/members.json +62 -0
  47. pty_migrate/payloads/policies.json +13 -0
  48. pty_migrate/payloads/roles.json +32 -0
  49. pty_migrate/payloads/rules.json +1639 -0
  50. pty_migrate/payloads/sources.json +10 -0
  51. pty_migrate/payloads/trusted_apps.json +8 -0
  52. pty_migrate/ppc_client.py +371 -0
  53. pty_migrate/stats_cmd.py +87 -0
@@ -0,0 +1,10 @@
1
+ [
2
+ {
3
+ "name": "DevEditionSource",
4
+ "type": "FILE",
5
+ "connection": {
6
+ "userFile": "exampleusers.txt",
7
+ "groupFile": "examplegroups.txt"
8
+ }
9
+ }
10
+ ]
@@ -0,0 +1,8 @@
1
+ [
2
+ {
3
+ "name": "DevEditionTrustedApp",
4
+ "description": "Trusted application for Protegrity Developer Edition - all apps and users",
5
+ "applicationName": "*",
6
+ "applicationUser": "*"
7
+ }
8
+ ]
@@ -0,0 +1,371 @@
1
+ """PPC Admin API client for pty-migrate create-policy.
2
+
3
+ Provides full PIM v2 API coverage for creating the DE policy on a
4
+ Team Edition PPC cluster. Supports JWT auth with auto-refresh.
5
+ """
6
+
7
+ import logging
8
+ import time
9
+
10
+ import requests
11
+ import urllib3
12
+
13
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class PPCClient:
19
+ """Client for Protegrity PPC (PIM v2) Admin API with JWT auto-refresh."""
20
+
21
+ def __init__(self, host, user, password, port=443):
22
+ self._base_url = f"https://{host}:{port}/pty"
23
+ self._user = user
24
+ self._password = password
25
+ self._access_token = None
26
+ self._refresh_token = None
27
+ self._token_expiry = 0
28
+ self._refresh_expiry = 0
29
+ self._session = requests.Session()
30
+ self._session.verify = False
31
+ self._session.timeout = 30
32
+
33
+ # ──────────────────────────────────────────────────────────
34
+ # Authentication with auto-refresh
35
+ # ──────────────────────────────────────────────────────────
36
+
37
+ def authenticate(self):
38
+ """Obtain JWT tokens from PPC auth endpoint.
39
+
40
+ PPC has two login endpoints:
41
+ - /api/v1/auth/login/token: Returns token in 'pty_access_jwt_token' header.
42
+ This token is recognized by the gateway's ext auth SecurityPolicy.
43
+ - /pty/v1/auth/login/token: Returns token in JSON body. This token is NOT
44
+ recognized by the gateway ext auth (causes 401 on protected routes).
45
+
46
+ We try the header-based endpoint first, falling back to the body-based one.
47
+ """
48
+ # Try header-based auth first (gateway-compatible)
49
+ base = self._base_url[:-4] if self._base_url.endswith("/pty") else self._base_url
50
+ url = f"{base}/api/v1/auth/login/token"
51
+ resp = self._session.post(
52
+ url,
53
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
54
+ data={"loginname": self._user, "password": self._password},
55
+ )
56
+ if resp.status_code == 200:
57
+ token = resp.headers.get("pty_access_jwt_token", "").strip()
58
+ if token:
59
+ self._access_token = token
60
+ self._refresh_token = None # header-based auth doesn't return refresh token
61
+ self._token_expiry = time.time() + 300
62
+ self._session.headers["Authorization"] = f"Bearer {self._access_token}"
63
+ logger.debug("PPC authentication successful (header-based)")
64
+ return
65
+
66
+ # Fallback to JSON body-based auth
67
+ url = f"{self._base_url}/v1/auth/login/token"
68
+ resp = self._session.post(
69
+ url,
70
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
71
+ data={"loginname": self._user, "password": self._password},
72
+ )
73
+ if resp.status_code != 200:
74
+ raise RuntimeError(f"Authentication failed: {resp.status_code} {resp.text}")
75
+ data = resp.json().get("data", resp.json())
76
+ self._access_token = data.get("accessToken") or data.get("token") or data.get("access_token")
77
+ self._refresh_token = data.get("refreshToken")
78
+ if not self._access_token:
79
+ raise RuntimeError(f"No token in auth response: {list(data.keys())}")
80
+ self._token_expiry = time.time() + data.get("expiresIn", 300)
81
+ self._refresh_expiry = time.time() + data.get("refreshExpiresIn", 900)
82
+ self._session.headers["Authorization"] = f"Bearer {self._access_token}"
83
+ logger.debug("PPC authentication successful (body-based)")
84
+
85
+ def _ensure_auth(self):
86
+ """Refresh token if expired."""
87
+ now = time.time()
88
+ if self._access_token is None:
89
+ self.authenticate()
90
+ elif now >= (self._token_expiry - 30):
91
+ if self._refresh_token and now < (self._refresh_expiry - 10):
92
+ self._refresh()
93
+ else:
94
+ self.authenticate()
95
+
96
+ def _refresh(self):
97
+ """Refresh the access token."""
98
+ url = f"{self._base_url}/v1/auth/login/token/refresh"
99
+ resp = self._session.post(url, json={"refreshToken": self._refresh_token})
100
+ if resp.status_code != 200:
101
+ logger.debug("Token refresh failed, re-authenticating")
102
+ self.authenticate()
103
+ return
104
+ data = resp.json().get("data", resp.json())
105
+ self._access_token = data.get("accessToken") or data.get("token")
106
+ self._refresh_token = data.get("refreshToken", self._refresh_token)
107
+ self._token_expiry = time.time() + data.get("expiresIn", 300)
108
+ self._session.headers["Authorization"] = f"Bearer {self._access_token}"
109
+
110
+ # ──────────────────────────────────────────────────────────
111
+ # Generic HTTP helpers
112
+ # ──────────────────────────────────────────────────────────
113
+
114
+ def _post(self, path, payload):
115
+ """POST to PPC API with auto-refresh."""
116
+ self._ensure_auth()
117
+ url = f"{self._base_url}{path}"
118
+ return self._session.post(url, json=payload)
119
+
120
+ def _get(self, path):
121
+ """GET from PPC API with auto-refresh."""
122
+ self._ensure_auth()
123
+ url = f"{self._base_url}{path}"
124
+ return self._session.get(url)
125
+
126
+ def post_resource(self, endpoint, payload):
127
+ """POST a resource payload. Returns (created: bool, response).
128
+
129
+ Handles 409 as 'already exists'. Returns (False, resp) on 400/other
130
+ client errors without raising. Non-409 4xx failures are reported via
131
+ `_warn_failed_create` so every caller gets visibility for free.
132
+
133
+ Some PPC endpoints (notably role-member links) return 400 with a body
134
+ like 'must be unique' / 'already exists' instead of 409 for duplicate
135
+ creates. We treat those as the same idempotent skip as 409.
136
+ """
137
+ resp = self._post(f"/v2/pim/{endpoint}", payload)
138
+ if resp.status_code in (200, 201, 204):
139
+ return True, resp
140
+ if resp.status_code == 409:
141
+ return False, resp
142
+ if resp.status_code >= 500:
143
+ resp.raise_for_status()
144
+ if resp.status_code == 400 and self._is_already_exists(resp):
145
+ return False, resp
146
+ self._warn_failed_create(endpoint, payload, resp)
147
+ return False, resp
148
+
149
+ @staticmethod
150
+ def _is_already_exists(resp):
151
+ """Return True if a 400 response really means 'already exists'."""
152
+ try:
153
+ body = resp.text.lower()
154
+ except Exception:
155
+ return False
156
+ return (
157
+ "already exists" in body
158
+ or "must be unique" in body
159
+ or "duplicate" in body
160
+ )
161
+
162
+ @staticmethod
163
+ def _warn_failed_create(endpoint, payload, resp):
164
+ """Surface non-409 4xx failures from `post_resource` to the CLI."""
165
+ kind = endpoint.rstrip("/").split("/")[-1]
166
+ name = ""
167
+ if isinstance(payload, dict):
168
+ name = (
169
+ payload.get("name")
170
+ or payload.get("label")
171
+ or payload.get("username")
172
+ or ""
173
+ )
174
+ elif isinstance(payload, list) and payload and isinstance(payload[0], dict):
175
+ name = payload[0].get("name") or payload[0].get("username") or ""
176
+ try:
177
+ body = resp.text[:500]
178
+ except Exception:
179
+ body = ""
180
+ print(f" \u2717 Failed to create {kind} '{name}': HTTP {resp.status_code} {body}")
181
+
182
+ # ──────────────────────────────────────────────────────────
183
+ # PIM initialization
184
+ # ──────────────────────────────────────────────────────────
185
+
186
+ def is_pim_initialized(self):
187
+ """Check if PIM is initialized by querying datastores."""
188
+ try:
189
+ resp = self._get("/v2/pim/datastores")
190
+ return resp.status_code == 200
191
+ except Exception:
192
+ return False
193
+
194
+ def init_pim(self):
195
+ """Initialize PIM (first-time setup). Idempotent."""
196
+ if self.is_pim_initialized():
197
+ logger.debug("PIM already initialized")
198
+ return True
199
+ resp = self._post("/v2/pim/init", {})
200
+ if resp.status_code in (200, 201, 204, 409):
201
+ return True
202
+ resp.raise_for_status()
203
+ return True
204
+
205
+ # ──────────────────────────────────────────────────────────
206
+ # List resources (for delta computation)
207
+ # ──────────────────────────────────────────────────────────
208
+
209
+ def _parse_list_response(self, resp):
210
+ """Parse a list API response into a list of items."""
211
+ if resp.status_code != 200:
212
+ return []
213
+ data = resp.json()
214
+ if isinstance(data, list):
215
+ return data
216
+ if isinstance(data, dict):
217
+ return data.get("data", data.get("items", []))
218
+ return []
219
+
220
+ def list_datastores(self):
221
+ """Get existing datastores."""
222
+ return self._parse_list_response(self._get("/v2/pim/datastores"))
223
+
224
+ def list_sources(self):
225
+ """Get existing sources."""
226
+ return self._parse_list_response(self._get("/v2/pim/sources"))
227
+
228
+ def list_roles(self):
229
+ """Get existing roles."""
230
+ return self._parse_list_response(self._get("/v2/pim/roles"))
231
+
232
+ def list_alphabets(self):
233
+ """Get existing alphabets."""
234
+ return self._parse_list_response(self._get("/v2/pim/alphabets"))
235
+
236
+ def list_masks(self):
237
+ """Get existing masks."""
238
+ return self._parse_list_response(self._get("/v2/pim/masks"))
239
+
240
+ def list_data_elements(self):
241
+ """Get existing data elements."""
242
+ return self._parse_list_response(self._get("/v2/pim/dataelements"))
243
+
244
+ def list_applications(self):
245
+ """Get existing trusted applications."""
246
+ return self._parse_list_response(self._get("/v2/pim/applications"))
247
+
248
+ def list_policies(self):
249
+ """Get existing policies."""
250
+ return self._parse_list_response(self._get("/v2/pim/policies"))
251
+
252
+ def list_rules(self, policy_id="1"):
253
+ """Get existing rules for a policy."""
254
+ return self._parse_list_response(self._get(f"/v2/pim/policies/{policy_id}/rules"))
255
+
256
+ def list_role_members(self, role_id):
257
+ """Get members of a role."""
258
+ return self._parse_list_response(self._get(f"/v2/pim/roles/{role_id}/members"))
259
+
260
+ def list_export_keys(self, datastore_id):
261
+ """Get export keys registered for a datastore."""
262
+ return self._parse_list_response(
263
+ self._get(f"/v2/pim/datastores/{datastore_id}/export/keys")
264
+ )
265
+
266
+ # ──────────────────────────────────────────────────────────
267
+ # Create resources
268
+ # ──────────────────────────────────────────────────────────
269
+
270
+ def create_datastore(self, payload):
271
+ """Create a datastore."""
272
+ return self.post_resource("datastores", payload)
273
+
274
+ def create_source(self, payload):
275
+ """Create a source."""
276
+ return self.post_resource("sources", payload)
277
+
278
+ def create_role(self, payload):
279
+ """Create a role from full payload."""
280
+ return self.post_resource("roles", payload)
281
+
282
+ def create_alphabet(self, payload):
283
+ """Create an alphabet."""
284
+ return self.post_resource("alphabets", payload)
285
+
286
+ def create_mask(self, payload):
287
+ """Create a mask."""
288
+ return self.post_resource("masks", payload)
289
+
290
+ def create_data_element(self, payload):
291
+ """Create a data element from full payload."""
292
+ return self.post_resource("dataelements", payload)
293
+
294
+ def create_application(self, payload):
295
+ """Create a trusted application."""
296
+ return self.post_resource("applications", payload)
297
+
298
+ def create_policy(self, payload):
299
+ """Create a policy."""
300
+ return self.post_resource("policies", payload)
301
+
302
+ def create_rule(self, policy_id, payload):
303
+ """Create a rule for a policy."""
304
+ return self.post_resource(f"policies/{policy_id}/rules", payload)
305
+
306
+ def add_members(self, role_uid, members_payload):
307
+ """Add members to a role. members_payload is a list."""
308
+ return self.post_resource(f"roles/{role_uid}/members", members_payload)
309
+
310
+ def deploy(self, datastore_id="1", payload=None):
311
+ """Deploy policy to a datastore."""
312
+ if payload is None:
313
+ payload = {"policies": ["1"], "applications": ["1"]}
314
+ return self.post_resource(f"datastores/{datastore_id}/deploy", payload)
315
+
316
+ # ──────────────────────────────────────────────────────────
317
+ # User management (auth v1 API)
318
+ # ──────────────────────────────────────────────────────────
319
+
320
+ def list_users(self):
321
+ """List all users via auth API."""
322
+ resp = self._get("/v1/auth/users")
323
+ if resp.status_code != 200:
324
+ return []
325
+ data = resp.json()
326
+ if isinstance(data, list):
327
+ return data
328
+ if isinstance(data, dict):
329
+ return data.get("data", data.get("items", []))
330
+ return []
331
+
332
+ def user_exists(self, username):
333
+ """Check if a user exists."""
334
+ users = self.list_users()
335
+ return any(u.get("username") == username for u in users)
336
+
337
+ def create_user(self, username, password, roles=None):
338
+ """Create a user via auth API. Returns (created: bool, response)."""
339
+ if roles is None:
340
+ roles = []
341
+ payload = {
342
+ "username": username,
343
+ "password": password,
344
+ "roles": roles,
345
+ }
346
+ self._ensure_auth()
347
+ url = f"{self._base_url}/v1/auth/users"
348
+ resp = self._session.post(url, json=payload)
349
+ if resp.status_code in (200, 201):
350
+ return True, resp
351
+ if resp.status_code == 409:
352
+ return False, resp
353
+ if resp.status_code == 400 and "exist" in resp.text.lower():
354
+ return False, resp
355
+ return False, resp
356
+
357
+ def ensure_role_permissions(self, role_name, permissions):
358
+ """Update a role with required permissions via PUT /v1/auth/roles."""
359
+ self._ensure_auth()
360
+ payload = {"name": role_name, "permissions": permissions}
361
+ url = f"{self._base_url}/v1/auth/roles"
362
+ resp = self._session.put(url, json=payload)
363
+ return resp.status_code in (200, 201)
364
+
365
+ def re_authenticate(self, user, password):
366
+ """Switch identity by authenticating as a different user."""
367
+ self._user = user
368
+ self._password = password
369
+ self._access_token = None
370
+ self._refresh_token = None
371
+ self.authenticate()
@@ -0,0 +1,87 @@
1
+ """pty-migrate stats command — display usage statistics summary."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+
8
+
9
+ def _load_stats(stats_file=None):
10
+ """Load usage stats from file. CLI flag > env > config file > default."""
11
+ from pty_migrate.config import resolve
12
+ resolved = resolve(stats_file, "PTY_STATS_FILE", "stats_file",
13
+ str(Path.home() / ".protegrity" / "usage_stats.json"))
14
+ path = Path(resolved)
15
+
16
+ if not path.is_file():
17
+ return None, path
18
+ with open(path, "r") as f:
19
+ return json.load(f), path
20
+
21
+
22
+ def run_stats(args):
23
+ """Execute the stats command."""
24
+ data, path = _load_stats(args.stats_file)
25
+
26
+ if data is None:
27
+ print(f"No usage statistics found at: {path}")
28
+ print("Stats are collected automatically when using the SDK with Developer Edition.")
29
+ print("Ensure DEV_EDITION_* environment variables are set and perform some operations.")
30
+ return 1
31
+
32
+ if args.json:
33
+ print(json.dumps(data, indent=2))
34
+ return 0
35
+
36
+ # Formatted output
37
+ collected_since = data.get("collected_since", "unknown")
38
+ last_updated = data.get("last_updated", "unknown")
39
+
40
+ print(f"Usage Statistics (collected since {collected_since[:10]})")
41
+ print("─" * 55)
42
+ print(f"Last updated: {last_updated[:10]}")
43
+ print()
44
+
45
+ # Data elements
46
+ data_elements = data.get("data_elements", {})
47
+ if data_elements:
48
+ print(f"Data Elements Used ({len(data_elements)}):")
49
+ # Sort by protect_count descending
50
+ sorted_des = sorted(
51
+ data_elements.items(),
52
+ key=lambda x: x[1].get("protect_count", 0),
53
+ reverse=True,
54
+ )
55
+ for de_name, de_stats in sorted_des:
56
+ protect = de_stats.get("protect_count", 0)
57
+ unprotect = de_stats.get("unprotect_count", 0)
58
+ reprotect_src = de_stats.get("reprotect_source_count", 0)
59
+ reprotect_tgt = de_stats.get("reprotect_target_count", 0)
60
+ last_used = de_stats.get("last_used", "—")
61
+ line = f" {de_name:<16} protect: {protect:<6} unprotect: {unprotect:<6}"
62
+ if reprotect_src or reprotect_tgt:
63
+ line += f" reprotect: {reprotect_src + reprotect_tgt:<4}"
64
+ line += f" last: {last_used}"
65
+ print(line)
66
+ else:
67
+ print("No data elements recorded yet.")
68
+
69
+ print()
70
+
71
+ # Policy users
72
+ policy_users = data.get("policy_users", {})
73
+ if policy_users:
74
+ print(f"Policy Users ({len(policy_users)}):")
75
+ sorted_users = sorted(
76
+ policy_users.items(),
77
+ key=lambda x: x[1].get("session_count", 0),
78
+ reverse=True,
79
+ )
80
+ for user, user_stats in sorted_users:
81
+ sessions = user_stats.get("session_count", 0)
82
+ last_used = user_stats.get("last_used", "—")
83
+ print(f" {user:<16} sessions: {sessions:<6} last: {last_used}")
84
+ else:
85
+ print("No policy users recorded yet.")
86
+
87
+ return 0