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.
- appython/__init__.py +12 -0
- appython/protector.py +554 -0
- appython/service/auth_provider.py +273 -0
- appython/service/auth_token_provider.py +45 -0
- appython/service/config.py +209 -0
- appython/service/payload_builder.py +141 -0
- appython/service/request_handler.py +115 -0
- appython/service/response_handler.py +78 -0
- appython/stats/__init__.py +3 -0
- appython/stats/collector.py +90 -0
- appython/stats/writer.py +185 -0
- appython/utils/codec_helper.py +86 -0
- appython/utils/constants.py +246 -0
- appython/utils/exceptions.py +141 -0
- appython/utils/input_preprocessor.py +325 -0
- appython/utils/output_postprocessor.py +99 -0
- protegrity_ai_developer_python-1.2.1.dist-info/METADATA +428 -0
- protegrity_ai_developer_python-1.2.1.dist-info/RECORD +53 -0
- protegrity_ai_developer_python-1.2.1.dist-info/WHEEL +5 -0
- protegrity_ai_developer_python-1.2.1.dist-info/entry_points.txt +2 -0
- protegrity_ai_developer_python-1.2.1.dist-info/licenses/LICENSE +21 -0
- protegrity_ai_developer_python-1.2.1.dist-info/top_level.txt +3 -0
- protegrity_developer_python/__init__.py +4 -0
- protegrity_developer_python/scan.py +37 -0
- protegrity_developer_python/securefind.py +83 -0
- protegrity_developer_python/utils/ccn_processing.py +59 -0
- protegrity_developer_python/utils/config.py +60 -0
- protegrity_developer_python/utils/constants.py +123 -0
- protegrity_developer_python/utils/discover.py +49 -0
- protegrity_developer_python/utils/logger.py +23 -0
- protegrity_developer_python/utils/pii_processing.py +291 -0
- protegrity_developer_python/utils/protector.py +23 -0
- protegrity_developer_python/utils/semantic_guardrails.py +240 -0
- protegrity_developer_python/utils/transform.py +66 -0
- pty_migrate/__init__.py +1 -0
- pty_migrate/check_cmd.py +871 -0
- pty_migrate/cli.py +93 -0
- pty_migrate/config.py +127 -0
- pty_migrate/create_policy_cmd.py +795 -0
- pty_migrate/payloads/__init__.py +51 -0
- pty_migrate/payloads/alphabets.json +42 -0
- pty_migrate/payloads/dataelements.json +342 -0
- pty_migrate/payloads/datastores.json +7 -0
- pty_migrate/payloads/deploy_policy_ta.json +1 -0
- pty_migrate/payloads/masks.json +18 -0
- pty_migrate/payloads/members.json +62 -0
- pty_migrate/payloads/policies.json +13 -0
- pty_migrate/payloads/roles.json +32 -0
- pty_migrate/payloads/rules.json +1639 -0
- pty_migrate/payloads/sources.json +10 -0
- pty_migrate/payloads/trusted_apps.json +8 -0
- pty_migrate/ppc_client.py +371 -0
- pty_migrate/stats_cmd.py +87 -0
pty_migrate/check_cmd.py
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
"""pty-migrate check command — pre-flight migration readiness validation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# Default data elements from the Developer Edition README examples
|
|
9
|
+
_DEFAULT_DATA_ELEMENTS = ["name", "ssn", "email", "city", "phone"]
|
|
10
|
+
_DEFAULT_POLICY_USER = "superuser"
|
|
11
|
+
|
|
12
|
+
_PAYLOADS_DIR = Path(__file__).parent / "payloads"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _load_rules_index():
|
|
16
|
+
"""Build a lookup of expected permissions from the bundled DE payloads.
|
|
17
|
+
|
|
18
|
+
Returns a dict with:
|
|
19
|
+
user_to_role: {username: role_id_str}
|
|
20
|
+
de_name_to_id: {de_name: de_id_str}
|
|
21
|
+
rules: {(role_id_str, de_id_str): {"protect": bool, "unProtect": bool}}
|
|
22
|
+
Returns None if any payload is missing/unreadable.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
with open(_PAYLOADS_DIR / "members.json") as f:
|
|
26
|
+
members = json.load(f)
|
|
27
|
+
with open(_PAYLOADS_DIR / "dataelements.json") as f:
|
|
28
|
+
des = json.load(f)
|
|
29
|
+
with open(_PAYLOADS_DIR / "rules.json") as f:
|
|
30
|
+
rules_raw = json.load(f)
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
user_to_role = {}
|
|
35
|
+
for key, member_list in members.items():
|
|
36
|
+
# key like "roles/2/members"
|
|
37
|
+
parts = key.split("/")
|
|
38
|
+
if len(parts) >= 2:
|
|
39
|
+
rid = parts[1]
|
|
40
|
+
for m in member_list:
|
|
41
|
+
name = m.get("name")
|
|
42
|
+
if name:
|
|
43
|
+
user_to_role[name] = rid
|
|
44
|
+
|
|
45
|
+
de_name_to_id = {de.get("name"): str(i + 1) for i, de in enumerate(des) if de.get("name")}
|
|
46
|
+
|
|
47
|
+
rules = {}
|
|
48
|
+
for r in rules_raw:
|
|
49
|
+
role = str(r.get("role"))
|
|
50
|
+
de = str(r.get("dataElement"))
|
|
51
|
+
access = r.get("permission", {}).get("access", {})
|
|
52
|
+
rules[(role, de)] = {
|
|
53
|
+
"protect": bool(access.get("protect")),
|
|
54
|
+
"unProtect": bool(access.get("unProtect")),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {"user_to_role": user_to_role, "de_name_to_id": de_name_to_id, "rules": rules}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _expected_round_trip(rules_index, user, de_name):
|
|
61
|
+
"""Return True if (user, de) is expected to round-trip per bundled rules.
|
|
62
|
+
|
|
63
|
+
Returns None if user or DE is unknown to the bundled payloads."""
|
|
64
|
+
if not rules_index:
|
|
65
|
+
return None
|
|
66
|
+
rid = rules_index["user_to_role"].get(user)
|
|
67
|
+
did = rules_index["de_name_to_id"].get(de_name)
|
|
68
|
+
if not rid or not did:
|
|
69
|
+
return None
|
|
70
|
+
rule = rules_index["rules"].get((rid, did))
|
|
71
|
+
if not rule:
|
|
72
|
+
return False # no rule = no access
|
|
73
|
+
return rule["protect"] and rule["unProtect"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _expected_can_protect(rules_index, user, de_name):
|
|
77
|
+
"""Return True if (user, de) is expected to be able to protect per rules."""
|
|
78
|
+
if not rules_index:
|
|
79
|
+
return None
|
|
80
|
+
rid = rules_index["user_to_role"].get(user)
|
|
81
|
+
did = rules_index["de_name_to_id"].get(de_name)
|
|
82
|
+
if not rid or not did:
|
|
83
|
+
return None
|
|
84
|
+
rule = rules_index["rules"].get((rid, did))
|
|
85
|
+
if not rule:
|
|
86
|
+
return False
|
|
87
|
+
return rule["protect"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_stats(stats_file=None):
|
|
91
|
+
"""Load usage stats from file."""
|
|
92
|
+
from pty_migrate.config import resolve
|
|
93
|
+
resolved = resolve(stats_file, "PTY_STATS_FILE", "stats_file",
|
|
94
|
+
str(Path.home() / ".protegrity" / "usage_stats.json"))
|
|
95
|
+
path = Path(resolved)
|
|
96
|
+
if not path.is_file():
|
|
97
|
+
return None
|
|
98
|
+
with open(path, "r") as f:
|
|
99
|
+
return json.load(f)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _check_sdk_version():
|
|
103
|
+
"""Check Python SDK version >= 1.2.1.
|
|
104
|
+
|
|
105
|
+
Returns (status, detail) where status is one of:
|
|
106
|
+
"ok" — installed and >= 1.2.1
|
|
107
|
+
"old" — installed but < 1.2.1
|
|
108
|
+
"load_error" — package metadata says installed but `import appython` fails
|
|
109
|
+
(e.g. Windows: SDK uses fcntl which is unix-only)
|
|
110
|
+
"missing" — package not installed at all
|
|
111
|
+
"""
|
|
112
|
+
# Prefer package metadata so we can tell "not installed" apart from
|
|
113
|
+
# "installed but unimportable on this OS".
|
|
114
|
+
try:
|
|
115
|
+
from importlib.metadata import version as _pkg_version, PackageNotFoundError
|
|
116
|
+
except ImportError: # pragma: no cover (py<3.8)
|
|
117
|
+
_pkg_version = None
|
|
118
|
+
PackageNotFoundError = Exception # type: ignore
|
|
119
|
+
|
|
120
|
+
pkg_ver = None
|
|
121
|
+
if _pkg_version is not None:
|
|
122
|
+
try:
|
|
123
|
+
pkg_ver = _pkg_version("protegrity-ai-developer-python")
|
|
124
|
+
except PackageNotFoundError:
|
|
125
|
+
pkg_ver = None
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
from appython import Protector
|
|
129
|
+
protector = Protector()
|
|
130
|
+
version = protector.get_version()
|
|
131
|
+
parts = version.split(".")
|
|
132
|
+
major, minor = int(parts[0]), int(parts[1])
|
|
133
|
+
ok = (major > 1) or (major == 1 and minor >= 2)
|
|
134
|
+
return ("ok" if ok else "old"), version
|
|
135
|
+
except Exception as e:
|
|
136
|
+
if pkg_ver is not None:
|
|
137
|
+
return "load_error", f"{pkg_ver} installed but failed to load: {e}"
|
|
138
|
+
return "missing", "not installed"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_java_sdk_version():
|
|
142
|
+
"""Check Java SDK version >= 1.1.0 from local Maven repo."""
|
|
143
|
+
m2_path = Path.home() / ".m2" / "repository" / "com" / "protegrity" / "application-protector-java"
|
|
144
|
+
if not m2_path.is_dir():
|
|
145
|
+
return None, "not installed"
|
|
146
|
+
|
|
147
|
+
versions = []
|
|
148
|
+
for d in m2_path.iterdir():
|
|
149
|
+
if d.is_dir() and d.name[0].isdigit():
|
|
150
|
+
versions.append(d.name)
|
|
151
|
+
if not versions:
|
|
152
|
+
return None, "not installed"
|
|
153
|
+
|
|
154
|
+
# Find highest version
|
|
155
|
+
def version_tuple(v):
|
|
156
|
+
try:
|
|
157
|
+
return tuple(int(x) for x in v.split("."))
|
|
158
|
+
except ValueError:
|
|
159
|
+
return (0,)
|
|
160
|
+
|
|
161
|
+
versions.sort(key=version_tuple, reverse=True)
|
|
162
|
+
latest = versions[0]
|
|
163
|
+
parts = version_tuple(latest)
|
|
164
|
+
ok = parts >= (1, 1, 0)
|
|
165
|
+
return ok, latest
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _sdk_cfg():
|
|
169
|
+
"""Load SDK config (env > YAML > defaults). Returns {} on failure."""
|
|
170
|
+
try:
|
|
171
|
+
from appython.service.config import load_config
|
|
172
|
+
return load_config() or {}
|
|
173
|
+
except Exception:
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _check_te_host():
|
|
178
|
+
"""Check Cloud Protect host is configured (env PTY_CP_HOST or YAML protect_host)."""
|
|
179
|
+
host = _sdk_cfg().get("protect_host") or ""
|
|
180
|
+
return bool(host), host
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _check_auth_mode():
|
|
184
|
+
"""Check auth mode is configured (env PTY_AUTH_MODE or YAML auth_mode) with required credentials."""
|
|
185
|
+
cfg = _sdk_cfg()
|
|
186
|
+
mode = cfg.get("auth_mode") or ""
|
|
187
|
+
if not mode:
|
|
188
|
+
return False, "", "PTY_AUTH_MODE not set"
|
|
189
|
+
|
|
190
|
+
detail = ""
|
|
191
|
+
if mode == "aws_iam":
|
|
192
|
+
profile = os.getenv("AWS_PROFILE", "")
|
|
193
|
+
region = os.getenv("AWS_DEFAULT_REGION", os.getenv("AWS_REGION", ""))
|
|
194
|
+
if profile:
|
|
195
|
+
detail = f"profile: {profile}"
|
|
196
|
+
elif os.getenv("AWS_ACCESS_KEY_ID"):
|
|
197
|
+
detail = "env credentials"
|
|
198
|
+
if os.getenv("AWS_SESSION_TOKEN"):
|
|
199
|
+
detail += " (with session token)"
|
|
200
|
+
else:
|
|
201
|
+
return False, mode, "No AWS credentials found (set AWS_PROFILE or AWS_ACCESS_KEY_ID)"
|
|
202
|
+
elif mode == "bearer_token":
|
|
203
|
+
# Matches BearerTokenAuthProvider.initialize(): either a static token, or
|
|
204
|
+
# the OAuth2 client_credentials triplet (token endpoint + client id + secret).
|
|
205
|
+
if cfg.get("static_token"):
|
|
206
|
+
detail = "static token present"
|
|
207
|
+
elif cfg.get("token_endpoint") and cfg.get("client_id") and cfg.get("client_secret"):
|
|
208
|
+
detail = "oauth2 client_credentials configured"
|
|
209
|
+
else:
|
|
210
|
+
return (
|
|
211
|
+
False,
|
|
212
|
+
mode,
|
|
213
|
+
"set PTY_STATIC_TOKEN, or PTY_TOKEN_ENDPOINT + PTY_CLIENT_ID + PTY_CLIENT_SECRET",
|
|
214
|
+
)
|
|
215
|
+
elif mode == "mtls":
|
|
216
|
+
cert = cfg.get("client_cert") or ""
|
|
217
|
+
key = cfg.get("client_key") or ""
|
|
218
|
+
if not cert or not key:
|
|
219
|
+
return False, mode, "PTY_CLIENT_CERT and PTY_CLIENT_KEY required"
|
|
220
|
+
detail = f"cert: {cert}"
|
|
221
|
+
elif mode == "cognito":
|
|
222
|
+
detail = "Developer Edition mode (not TE)"
|
|
223
|
+
elif mode == "none":
|
|
224
|
+
detail = "no auth"
|
|
225
|
+
|
|
226
|
+
return True, mode, detail
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _shell_export(var, value):
|
|
230
|
+
"""Render a shell `set/export` line appropriate for the current platform."""
|
|
231
|
+
if os.name == "nt":
|
|
232
|
+
# PowerShell — what `pty-migrate` users on Windows almost always have
|
|
233
|
+
return f"$env:{var} = \"{value}\""
|
|
234
|
+
return f"export {var}={value}"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _auth_fix_hints(mode, detail, indent=" → ", continuation=None):
|
|
238
|
+
"""Return a list of resolution lines tailored to the actual auth failure.
|
|
239
|
+
|
|
240
|
+
`mode` is the value of PTY_AUTH_MODE we observed (may be "" if unset).
|
|
241
|
+
`detail` is the failure message from `_check_auth_mode` — we use it to
|
|
242
|
+
distinguish "no mode set" from "mode set but missing credentials".
|
|
243
|
+
"""
|
|
244
|
+
cont = continuation if continuation is not None else " " * len(indent)
|
|
245
|
+
lines = []
|
|
246
|
+
|
|
247
|
+
if not mode:
|
|
248
|
+
lines.append(f"{indent}Pick an auth mode and set its credentials. Most common:")
|
|
249
|
+
lines.append(f"{cont} {_shell_export('PTY_AUTH_MODE', 'aws_iam')} "
|
|
250
|
+
f"# Cloud Protect behind AWS API Gateway")
|
|
251
|
+
lines.append(f"{cont} {_shell_export('PTY_AUTH_MODE', 'bearer_token')} "
|
|
252
|
+
f"# static JWT or OAuth2 client_credentials")
|
|
253
|
+
return lines
|
|
254
|
+
|
|
255
|
+
if mode == "aws_iam":
|
|
256
|
+
lines.append(f"{indent}aws_iam mode needs AWS credentials. Either:")
|
|
257
|
+
lines.append(f"{cont} {_shell_export('AWS_PROFILE', '<profile-name>')} "
|
|
258
|
+
f"# uses ~/.aws/credentials")
|
|
259
|
+
lines.append(f"{cont} or set the three env vars directly:")
|
|
260
|
+
lines.append(f"{cont} {_shell_export('AWS_ACCESS_KEY_ID', '<key>')}")
|
|
261
|
+
lines.append(f"{cont} {_shell_export('AWS_SECRET_ACCESS_KEY', '<secret>')}")
|
|
262
|
+
lines.append(f"{cont} {_shell_export('AWS_SESSION_TOKEN', '<session-token>')} "
|
|
263
|
+
f"# required for STS / SSO / temporary credentials")
|
|
264
|
+
lines.append(f"{cont} {_shell_export('AWS_DEFAULT_REGION', 'us-east-1')} "
|
|
265
|
+
f"# region of your Cloud Protect deployment")
|
|
266
|
+
elif mode == "bearer_token":
|
|
267
|
+
lines.append(f"{indent}bearer_token mode needs either a static token or OAuth2 creds:")
|
|
268
|
+
lines.append(f"{cont} {_shell_export('PTY_STATIC_TOKEN', '<jwt>')}")
|
|
269
|
+
lines.append(f"{cont} or: {_shell_export('PTY_TOKEN_ENDPOINT', '<url>')}, "
|
|
270
|
+
f"PTY_CLIENT_ID, PTY_CLIENT_SECRET")
|
|
271
|
+
elif mode == "oauth2_client_credentials":
|
|
272
|
+
lines.append(f"{indent}oauth2_client_credentials needs three vars:")
|
|
273
|
+
lines.append(f"{cont} {_shell_export('PTY_TOKEN_ENDPOINT', '<url>')}")
|
|
274
|
+
lines.append(f"{cont} {_shell_export('PTY_CLIENT_ID', '<id>')}")
|
|
275
|
+
lines.append(f"{cont} {_shell_export('PTY_CLIENT_SECRET', '<secret>')}")
|
|
276
|
+
elif mode == "mtls":
|
|
277
|
+
lines.append(f"{indent}mtls mode needs client cert + key:")
|
|
278
|
+
lines.append(f"{cont} {_shell_export('PTY_CLIENT_CERT', '/path/to/client.crt')}")
|
|
279
|
+
lines.append(f"{cont} {_shell_export('PTY_CLIENT_KEY', '/path/to/client.key')}")
|
|
280
|
+
else:
|
|
281
|
+
lines.append(f"{indent}{detail}")
|
|
282
|
+
return lines
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _check_endpoint_reachable(host):
|
|
286
|
+
"""Check if TE endpoint is reachable."""
|
|
287
|
+
import requests
|
|
288
|
+
try:
|
|
289
|
+
# Try a lightweight request
|
|
290
|
+
resp = requests.get(f"{host}/v1/version", timeout=10, verify=True)
|
|
291
|
+
if resp.status_code in (200, 401, 403):
|
|
292
|
+
return True, f"HTTP {resp.status_code}"
|
|
293
|
+
return True, f"HTTP {resp.status_code}"
|
|
294
|
+
except requests.exceptions.SSLError:
|
|
295
|
+
# Try without SSL verification
|
|
296
|
+
try:
|
|
297
|
+
resp = requests.get(f"{host}/v1/version", timeout=10, verify=False)
|
|
298
|
+
return True, f"reachable (SSL warning)"
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
except requests.exceptions.ConnectionError:
|
|
302
|
+
return False, "Connection refused"
|
|
303
|
+
except requests.exceptions.Timeout:
|
|
304
|
+
return False, "Timeout"
|
|
305
|
+
except Exception as e:
|
|
306
|
+
return False, str(e)
|
|
307
|
+
return False, "Unreachable"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _check_auth_works(host):
|
|
311
|
+
"""Attempt an authenticated request to TE/Cloud Protect."""
|
|
312
|
+
try:
|
|
313
|
+
from appython import Protector
|
|
314
|
+
|
|
315
|
+
protector = Protector()
|
|
316
|
+
session = protector.create_session("superuser")
|
|
317
|
+
# Attempt a protect call — if auth works this will either succeed
|
|
318
|
+
# or fail with a policy error (not auth error)
|
|
319
|
+
try:
|
|
320
|
+
session.protect("auth_check_probe", "name")
|
|
321
|
+
return True, "authenticated (protect succeeded)"
|
|
322
|
+
except Exception as e:
|
|
323
|
+
err = str(e).lower()
|
|
324
|
+
# Auth/connection errors
|
|
325
|
+
if "401" in err or "403" in err or "unauthorized" in err or "forbidden" in err:
|
|
326
|
+
return False, f"credentials rejected — {e}"
|
|
327
|
+
if "connection" in err or "timeout" in err or "unreachable" in err:
|
|
328
|
+
return False, f"connection failed — {e}"
|
|
329
|
+
# Any other error (e.g. DE not found, policy error) means auth worked
|
|
330
|
+
return True, "authenticated (endpoint responded)"
|
|
331
|
+
except Exception as e:
|
|
332
|
+
return False, str(e)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _check_data_elements(host, stats):
|
|
336
|
+
"""Verify the policy is reachable end-to-end via a single protect call.
|
|
337
|
+
|
|
338
|
+
A successful `protect` proves three things at once: the SDK is configured,
|
|
339
|
+
Cloud Protect is reachable, and the policy/DE is deployed. There is no
|
|
340
|
+
value in running this for every DE × user combination — it just multiplies
|
|
341
|
+
latency and retry storms when something is wrong.
|
|
342
|
+
|
|
343
|
+
Strategy: pick one DE and one user, prefer those the bundled rules say
|
|
344
|
+
can both protect AND unProtect (so we can also verify round-trip), then
|
|
345
|
+
fall back to any DE the user used in the local stats.
|
|
346
|
+
"""
|
|
347
|
+
rules_index = _load_rules_index()
|
|
348
|
+
|
|
349
|
+
# Choose a DE — prefer something the user actually used locally and that
|
|
350
|
+
# is also part of the standard bundled policy; fall back to `name`.
|
|
351
|
+
stats_des = list(stats.get("data_elements", {}).keys()) if stats else []
|
|
352
|
+
bundled_set = set(_bundled_de_names())
|
|
353
|
+
candidate_des = [de for de in stats_des if de in bundled_set] or ["name"]
|
|
354
|
+
|
|
355
|
+
# Choose a user — prefer one bundled into the standard policy.
|
|
356
|
+
bundled_users = _bundled_member_names()
|
|
357
|
+
stats_users = list(stats.get("policy_users", {}).keys()) if stats else []
|
|
358
|
+
candidate_users = [u for u in stats_users if u in bundled_users] or [_DEFAULT_POLICY_USER]
|
|
359
|
+
|
|
360
|
+
# Pick the (de, user) pair that supports the strongest test (round-trip).
|
|
361
|
+
chosen_de = None
|
|
362
|
+
chosen_user = None
|
|
363
|
+
can_round_trip = False
|
|
364
|
+
if rules_index:
|
|
365
|
+
for de in candidate_des:
|
|
366
|
+
for user in candidate_users:
|
|
367
|
+
if (_expected_can_protect(rules_index, user, de) is True
|
|
368
|
+
and _expected_round_trip(rules_index, user, de) is True):
|
|
369
|
+
chosen_de, chosen_user, can_round_trip = de, user, True
|
|
370
|
+
break
|
|
371
|
+
if chosen_de:
|
|
372
|
+
break
|
|
373
|
+
if not chosen_de:
|
|
374
|
+
for de in candidate_des:
|
|
375
|
+
for user in candidate_users:
|
|
376
|
+
if _expected_can_protect(rules_index, user, de) is True:
|
|
377
|
+
chosen_de, chosen_user = de, user
|
|
378
|
+
break
|
|
379
|
+
if chosen_de:
|
|
380
|
+
break
|
|
381
|
+
if not chosen_de:
|
|
382
|
+
chosen_de = candidate_des[0]
|
|
383
|
+
chosen_user = candidate_users[0]
|
|
384
|
+
|
|
385
|
+
plaintext = "test_migration_check"
|
|
386
|
+
try:
|
|
387
|
+
from appython import Protector
|
|
388
|
+
protector = Protector()
|
|
389
|
+
session = protector.create_session(chosen_user)
|
|
390
|
+
try:
|
|
391
|
+
protected = session.protect(plaintext, chosen_de)
|
|
392
|
+
except Exception as e:
|
|
393
|
+
return False, f"protect failed (user={chosen_user}, de={chosen_de}): {e}"
|
|
394
|
+
|
|
395
|
+
if not can_round_trip:
|
|
396
|
+
return True, (f"protect OK (user={chosen_user}, de={chosen_de}) "
|
|
397
|
+
f"— round-trip not attempted (user lacks unProtect on this DE)")
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
unprotected = session.unprotect(protected, chosen_de)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
return False, f"unprotect failed (user={chosen_user}, de={chosen_de}): {e}"
|
|
403
|
+
|
|
404
|
+
if unprotected == plaintext:
|
|
405
|
+
return True, f"round-trip OK (user={chosen_user}, de={chosen_de})"
|
|
406
|
+
if unprotected == protected:
|
|
407
|
+
return True, (f"protect OK (user={chosen_user}, de={chosen_de}) "
|
|
408
|
+
f"— unprotect returned the protected value (PROTECTED_VALUE policy)")
|
|
409
|
+
return False, f"unprotect mismatch (user={chosen_user}, de={chosen_de}): got {unprotected!r}"
|
|
410
|
+
except Exception as e:
|
|
411
|
+
return False, str(e)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _bundled_de_names():
|
|
415
|
+
"""Return all DE names from the bundled Developer Edition payloads."""
|
|
416
|
+
try:
|
|
417
|
+
with open(_PAYLOADS_DIR / "dataelements.json") as f:
|
|
418
|
+
des = json.load(f)
|
|
419
|
+
return [d.get("name") for d in des if d.get("name")]
|
|
420
|
+
except Exception:
|
|
421
|
+
return []
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _bundled_member_names():
|
|
425
|
+
"""Return all policy-user names from the bundled members payload.
|
|
426
|
+
|
|
427
|
+
These are the only users that `pty-migrate create-policy` will register
|
|
428
|
+
on PPC, so they are the only ones worth checking for. Anything in the
|
|
429
|
+
stats file that is not in this set is test/sandbox data the user wrote
|
|
430
|
+
on their own and not part of the standard Developer Edition policy.
|
|
431
|
+
"""
|
|
432
|
+
try:
|
|
433
|
+
with open(_PAYLOADS_DIR / "members.json") as f:
|
|
434
|
+
groups = json.load(f)
|
|
435
|
+
names = set()
|
|
436
|
+
for members in groups.values():
|
|
437
|
+
for m in members or []:
|
|
438
|
+
name = m.get("name")
|
|
439
|
+
if name:
|
|
440
|
+
names.add(name)
|
|
441
|
+
return names
|
|
442
|
+
except Exception:
|
|
443
|
+
return set()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _check_ppc_deployment(ppc, stats, full=False):
|
|
447
|
+
"""Verify PPC deployment state: DEs, roles+members, datastore export key.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
ppc: dict with 'host', 'user', 'password' (resolved from args + env).
|
|
451
|
+
stats: parsed usage stats (may be None).
|
|
452
|
+
full: when True, verify all bundled DE names instead of only those in stats.
|
|
453
|
+
|
|
454
|
+
Returns (results, info) where:
|
|
455
|
+
results: list of (label, ok, detail) tuples for each sub-check.
|
|
456
|
+
info: dict with side-channel data the caller needs for resolution
|
|
457
|
+
hints (currently: dev_edition_uid).
|
|
458
|
+
"""
|
|
459
|
+
results = []
|
|
460
|
+
info = {"dev_edition_uid": None}
|
|
461
|
+
try:
|
|
462
|
+
from pty_migrate.ppc_client import PPCClient
|
|
463
|
+
client = PPCClient(ppc["host"], ppc["user"], ppc["password"])
|
|
464
|
+
client.authenticate()
|
|
465
|
+
except Exception as e:
|
|
466
|
+
return [("PPC connection", False, f"failed to connect/authenticate: {e}")], info
|
|
467
|
+
|
|
468
|
+
# Datastores — DevEdition expected
|
|
469
|
+
try:
|
|
470
|
+
datastores = client.list_datastores()
|
|
471
|
+
except Exception as e:
|
|
472
|
+
return [("PPC datastores", False, str(e))], info
|
|
473
|
+
if not datastores:
|
|
474
|
+
results.append(("PPC datastores", False, "no datastores defined"))
|
|
475
|
+
return results, info
|
|
476
|
+
ds_names = [d.get("name", "?") for d in datastores]
|
|
477
|
+
dev_edition_ds = next((d for d in datastores if d.get("name") == "DevEdition"), None)
|
|
478
|
+
if dev_edition_ds:
|
|
479
|
+
info["dev_edition_uid"] = dev_edition_ds.get("uid") or dev_edition_ds.get("id")
|
|
480
|
+
results.append(("PPC datastores", True,
|
|
481
|
+
f"DevEdition present (id={info['dev_edition_uid']}, "
|
|
482
|
+
f"{len(datastores)} total: {', '.join(ds_names)})"))
|
|
483
|
+
else:
|
|
484
|
+
results.append(("PPC datastores", False,
|
|
485
|
+
f"DevEdition datastore not found (have: {', '.join(ds_names)})"))
|
|
486
|
+
|
|
487
|
+
# Data elements present on PPC
|
|
488
|
+
bundled_de_set = set(_bundled_de_names())
|
|
489
|
+
if full:
|
|
490
|
+
required_des = list(bundled_de_set) or _DEFAULT_DATA_ELEMENTS
|
|
491
|
+
scope_note = "all bundled Developer Edition DEs"
|
|
492
|
+
skipped_des = []
|
|
493
|
+
else:
|
|
494
|
+
stats_des = list(stats.get("data_elements", {}).keys()) if stats else []
|
|
495
|
+
if not stats_des:
|
|
496
|
+
required_des = list(_DEFAULT_DATA_ELEMENTS)
|
|
497
|
+
skipped_des = []
|
|
498
|
+
elif bundled_de_set:
|
|
499
|
+
# Only check DEs that are part of the standard DE policy. Anything
|
|
500
|
+
# else in stats (e.g. `pii_test`, `dob`) is user/test data the
|
|
501
|
+
# migration tool does not create and cannot verify.
|
|
502
|
+
required_des = [de for de in stats_des if de in bundled_de_set]
|
|
503
|
+
skipped_des = [de for de in stats_des if de not in bundled_de_set]
|
|
504
|
+
else:
|
|
505
|
+
required_des = stats_des
|
|
506
|
+
skipped_des = []
|
|
507
|
+
scope_note = "standard DEs from local stats"
|
|
508
|
+
try:
|
|
509
|
+
ppc_des = client.list_data_elements()
|
|
510
|
+
except Exception as e:
|
|
511
|
+
return results + [("PPC data elements", False, str(e))], info
|
|
512
|
+
ppc_de_names = {d.get("name") for d in ppc_des}
|
|
513
|
+
missing_des = [de for de in required_des if de not in ppc_de_names]
|
|
514
|
+
skipped_suffix = (
|
|
515
|
+
f" (ignored {len(skipped_des)} non-standard: {', '.join(skipped_des)})"
|
|
516
|
+
if skipped_des else ""
|
|
517
|
+
)
|
|
518
|
+
if missing_des:
|
|
519
|
+
results.append(("PPC data elements", False,
|
|
520
|
+
f"{len(missing_des)} of {len(required_des)} {scope_note} missing: "
|
|
521
|
+
f"{', '.join(missing_des)}{skipped_suffix}"))
|
|
522
|
+
elif not required_des:
|
|
523
|
+
results.append(("PPC data elements", None,
|
|
524
|
+
f"no standard DEs to check{skipped_suffix}"))
|
|
525
|
+
else:
|
|
526
|
+
results.append(("PPC data elements", True,
|
|
527
|
+
f"all {len(required_des)} {scope_note} present{skipped_suffix}"))
|
|
528
|
+
|
|
529
|
+
# Policy users registered as role members on PPC
|
|
530
|
+
bundled_users = _bundled_member_names()
|
|
531
|
+
stats_users = list(stats.get("policy_users", {}).keys()) if stats else []
|
|
532
|
+
if bundled_users:
|
|
533
|
+
required_users = [u for u in stats_users if u in bundled_users]
|
|
534
|
+
skipped_users = [u for u in stats_users if u not in bundled_users]
|
|
535
|
+
else:
|
|
536
|
+
required_users = stats_users
|
|
537
|
+
skipped_users = []
|
|
538
|
+
user_skipped_suffix = (
|
|
539
|
+
f" (ignored {len(skipped_users)} non-standard: {', '.join(skipped_users)})"
|
|
540
|
+
if skipped_users else ""
|
|
541
|
+
)
|
|
542
|
+
if required_users:
|
|
543
|
+
try:
|
|
544
|
+
roles = client.list_roles()
|
|
545
|
+
all_members = set()
|
|
546
|
+
for role in roles:
|
|
547
|
+
rid = role.get("uid") or role.get("id")
|
|
548
|
+
if not rid:
|
|
549
|
+
continue
|
|
550
|
+
try:
|
|
551
|
+
members = client.list_role_members(rid)
|
|
552
|
+
for m in members:
|
|
553
|
+
name = m.get("memberName") or m.get("name") or m.get("username")
|
|
554
|
+
if name:
|
|
555
|
+
all_members.add(name)
|
|
556
|
+
except Exception:
|
|
557
|
+
continue
|
|
558
|
+
missing_users = [u for u in required_users if u not in all_members]
|
|
559
|
+
if missing_users:
|
|
560
|
+
results.append(("PPC role members", False,
|
|
561
|
+
f"users not in any role: {', '.join(missing_users)}"
|
|
562
|
+
f"{user_skipped_suffix}"))
|
|
563
|
+
else:
|
|
564
|
+
results.append(("PPC role members", True,
|
|
565
|
+
f"all {len(required_users)} users are role members"
|
|
566
|
+
f"{user_skipped_suffix}"))
|
|
567
|
+
except Exception as e:
|
|
568
|
+
results.append(("PPC role members", False, str(e)))
|
|
569
|
+
else:
|
|
570
|
+
results.append(("PPC role members", None,
|
|
571
|
+
f"no standard policy users in stats{user_skipped_suffix}"))
|
|
572
|
+
|
|
573
|
+
# Datastore export keys (required for Cloud Protect to publish the policy)
|
|
574
|
+
ds_with_keys = []
|
|
575
|
+
ds_without_keys = []
|
|
576
|
+
for ds in datastores:
|
|
577
|
+
ds_uid = ds.get("uid") or ds.get("id")
|
|
578
|
+
if not ds_uid:
|
|
579
|
+
continue
|
|
580
|
+
try:
|
|
581
|
+
keys = client.list_export_keys(ds_uid)
|
|
582
|
+
if keys:
|
|
583
|
+
ds_with_keys.append(ds.get("name", ds_uid))
|
|
584
|
+
else:
|
|
585
|
+
ds_without_keys.append(ds.get("name", ds_uid))
|
|
586
|
+
except Exception:
|
|
587
|
+
ds_without_keys.append(ds.get("name", ds_uid))
|
|
588
|
+
if "DevEdition" in ds_with_keys:
|
|
589
|
+
results.append(("PPC datastore export keys", True,
|
|
590
|
+
f"configured on DevEdition (and: {', '.join(ds_with_keys)})"))
|
|
591
|
+
elif ds_with_keys:
|
|
592
|
+
results.append(("PPC datastore export keys", False,
|
|
593
|
+
f"export keys present on {', '.join(ds_with_keys)} "
|
|
594
|
+
f"but not on DevEdition"))
|
|
595
|
+
else:
|
|
596
|
+
results.append(("PPC datastore export keys", False,
|
|
597
|
+
f"no export keys on any datastore ({', '.join(ds_without_keys)})"))
|
|
598
|
+
|
|
599
|
+
return results, info
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _resolve_ppc_args(args):
|
|
603
|
+
"""Resolve PPC connection args using CLI > env > config file > default.
|
|
604
|
+
|
|
605
|
+
Returns a dict {host, user, password} with any value possibly None.
|
|
606
|
+
Passwords are file-readable only with `allow_secrets_in_file: true`
|
|
607
|
+
AND chmod 600 on the config file (see pty_migrate.config).
|
|
608
|
+
"""
|
|
609
|
+
from pty_migrate.config import resolve, resolve_password
|
|
610
|
+
password, pw_source = resolve_password(
|
|
611
|
+
getattr(args, "ppc_password", None), "PTY_PPC_PASSWORD", "ppc_password"
|
|
612
|
+
)
|
|
613
|
+
if pw_source == "file":
|
|
614
|
+
print(" · PPC password loaded from ~/.protegrity/config.yaml (chmod 600 verified).")
|
|
615
|
+
return {
|
|
616
|
+
"host": resolve(getattr(args, "ppc_host", None), "PTY_PPC_HOST", "ppc_host"),
|
|
617
|
+
"user": resolve(getattr(args, "ppc_user", None), "PTY_PPC_USER", "ppc_user", "workbench"),
|
|
618
|
+
"password": password,
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def run_check(args):
|
|
623
|
+
"""Execute the check command."""
|
|
624
|
+
stats = _load_stats(args.stats_file)
|
|
625
|
+
ppc = _resolve_ppc_args(args)
|
|
626
|
+
with_protect_test = getattr(args, "with_protect_fn_test", False)
|
|
627
|
+
ppc_creds_present = bool(ppc["host"] and ppc["password"])
|
|
628
|
+
|
|
629
|
+
# Mode resolution: PPC verification is the default; --with-protect-fn-test
|
|
630
|
+
# opts into the round-trip path. Both can run together.
|
|
631
|
+
do_ppc = ppc_creds_present
|
|
632
|
+
do_protect_test = with_protect_test
|
|
633
|
+
if not do_ppc and not do_protect_test:
|
|
634
|
+
print()
|
|
635
|
+
print(" ✗ PPC credentials required.")
|
|
636
|
+
print(" Set PTY_PPC_HOST and PTY_PPC_PASSWORD environment variables, or pass")
|
|
637
|
+
print(" --ppc-host and --ppc-password.")
|
|
638
|
+
print(" Alternatively, pass --with-protect-fn-test to run a protect/unprotect")
|
|
639
|
+
print(" smoke check against Cloud Protect (no PPC required).")
|
|
640
|
+
print()
|
|
641
|
+
return 1
|
|
642
|
+
|
|
643
|
+
title = "Protegrity Developer → Team Edition Migration Readiness Check"
|
|
644
|
+
print()
|
|
645
|
+
print(f" {title}")
|
|
646
|
+
print(f" {'─' * len(title)}")
|
|
647
|
+
print()
|
|
648
|
+
|
|
649
|
+
issues = 0
|
|
650
|
+
skipped = 0
|
|
651
|
+
|
|
652
|
+
# 1. SDK versions
|
|
653
|
+
py_status, py_version = _check_sdk_version()
|
|
654
|
+
java_ok, java_version = _check_java_sdk_version()
|
|
655
|
+
py_ok = py_status == "ok"
|
|
656
|
+
|
|
657
|
+
if py_ok:
|
|
658
|
+
print(f" ✓ Python SDK: {py_version} (minimum: 1.2.1)")
|
|
659
|
+
elif py_status == "old":
|
|
660
|
+
print(f" ✗ Python SDK: {py_version} (need >= 1.2.1)")
|
|
661
|
+
print(f" → pip install --upgrade protegrity-ai-developer-python")
|
|
662
|
+
elif py_status == "load_error":
|
|
663
|
+
# Package installed but `import appython` raised. Report verbatim so
|
|
664
|
+
# users can act on the underlying ImportError without us guessing.
|
|
665
|
+
print(f" ✗ Python SDK: {py_version}")
|
|
666
|
+
print(f" → Reinstall the SDK: pip install --force-reinstall protegrity-ai-developer-python")
|
|
667
|
+
else: # missing
|
|
668
|
+
print(f" · Python SDK: not installed")
|
|
669
|
+
|
|
670
|
+
if java_ok is True:
|
|
671
|
+
print(f" ✓ Java SDK: {java_version} (minimum: 1.1.0)")
|
|
672
|
+
elif java_ok is False:
|
|
673
|
+
print(f" ✗ Java SDK: {java_version} (need >= 1.1.0)")
|
|
674
|
+
print(f" → Update the Java SDK dependency in your pom.xml to >= 1.1.0")
|
|
675
|
+
else:
|
|
676
|
+
print(f" · Java SDK: not installed")
|
|
677
|
+
|
|
678
|
+
# Migration only needs one SDK at the required version.
|
|
679
|
+
sdk_ok = py_ok or (java_ok is True)
|
|
680
|
+
if not sdk_ok:
|
|
681
|
+
# Don't repeat per-SDK fix hints already printed above. Only add the
|
|
682
|
+
# "install at least one" message when neither is present at all.
|
|
683
|
+
if py_status == "missing" and java_ok is None:
|
|
684
|
+
print(f" → Install at least one SDK:")
|
|
685
|
+
print(f" Python: pip install --upgrade protegrity-ai-developer-python")
|
|
686
|
+
print(f" Java: add com.protegrity:application-protector-java >= 1.1.0 to pom.xml")
|
|
687
|
+
issues += 1
|
|
688
|
+
|
|
689
|
+
# 2. Team Edition host configured
|
|
690
|
+
ok, host = _check_te_host()
|
|
691
|
+
if ok:
|
|
692
|
+
print(f" ✓ PTY_CP_HOST: {host}")
|
|
693
|
+
else:
|
|
694
|
+
print(f" ✗ PTY_CP_HOST not set")
|
|
695
|
+
print(f" → export PTY_CP_HOST=<your-cloud-protect-invoke-url>")
|
|
696
|
+
issues += 1
|
|
697
|
+
|
|
698
|
+
# 3. Auth mode configured
|
|
699
|
+
auth_ok, mode, detail = _check_auth_mode()
|
|
700
|
+
if auth_ok:
|
|
701
|
+
print(f" ✓ PTY_AUTH_MODE: {mode} ({detail})")
|
|
702
|
+
else:
|
|
703
|
+
print(f" ✗ {detail}")
|
|
704
|
+
for line in _auth_fix_hints(mode, detail, indent=" → "):
|
|
705
|
+
print(line)
|
|
706
|
+
issues += 1
|
|
707
|
+
|
|
708
|
+
# 4. Team Edition endpoint reachable
|
|
709
|
+
if ok and host:
|
|
710
|
+
reachable, reach_detail = _check_endpoint_reachable(host)
|
|
711
|
+
if reachable:
|
|
712
|
+
print(f" ✓ Team Edition endpoint reachable ({reach_detail})")
|
|
713
|
+
else:
|
|
714
|
+
print(f" ✗ Team Edition endpoint unreachable: {reach_detail}")
|
|
715
|
+
issues += 1
|
|
716
|
+
else:
|
|
717
|
+
print(f" ⊘ Team Edition endpoint: skipped (no host configured)")
|
|
718
|
+
skipped += 1
|
|
719
|
+
|
|
720
|
+
# 5. Authentication works (only needed for protect-fn test)
|
|
721
|
+
auth_works = None
|
|
722
|
+
if do_protect_test and ok and host and auth_ok:
|
|
723
|
+
auth_works, auth_detail = _check_auth_works(host)
|
|
724
|
+
if auth_works:
|
|
725
|
+
print(f" ✓ Authentication: {auth_detail}")
|
|
726
|
+
else:
|
|
727
|
+
print(f" ✗ Authentication failed: {auth_detail}")
|
|
728
|
+
print(f" → Verify your credentials and that the policy is deployed on your PPC")
|
|
729
|
+
print(f" → Run: pty-migrate create-policy --ppc-host <your-ppc-host> --ppc-password <password>")
|
|
730
|
+
issues += 1
|
|
731
|
+
elif do_protect_test:
|
|
732
|
+
print(f" ⊘ Authentication: skipped (prerequisites not met)")
|
|
733
|
+
skipped += 1
|
|
734
|
+
|
|
735
|
+
# 6. Policy users
|
|
736
|
+
if stats:
|
|
737
|
+
users = list(stats.get("policy_users", {}).keys())
|
|
738
|
+
if users:
|
|
739
|
+
print(f" ✓ Policy users (from stats): {', '.join(users)}")
|
|
740
|
+
else:
|
|
741
|
+
print(f" · No policy users in stats — using default: {_DEFAULT_POLICY_USER}")
|
|
742
|
+
else:
|
|
743
|
+
print(f" · No stats file — using default user: {_DEFAULT_POLICY_USER}")
|
|
744
|
+
|
|
745
|
+
de_failed = False
|
|
746
|
+
ppc_failed = False
|
|
747
|
+
|
|
748
|
+
# 7a. PPC deployment verification (default mode)
|
|
749
|
+
if do_ppc:
|
|
750
|
+
scope = "all bundled Developer Edition data elements" if getattr(args, "full", False) \
|
|
751
|
+
else "data elements from local stats"
|
|
752
|
+
print(f" · PPC deployment verification ({ppc['host']}, scope: {scope}):")
|
|
753
|
+
ppc_results, ppc_info = _check_ppc_deployment(ppc, stats, full=getattr(args, "full", False))
|
|
754
|
+
dev_edition_uid = ppc_info.get("dev_edition_uid")
|
|
755
|
+
for label, ppc_ok, detail in ppc_results:
|
|
756
|
+
if ppc_ok is True:
|
|
757
|
+
print(f" ✓ {label}: {detail}")
|
|
758
|
+
elif ppc_ok is False:
|
|
759
|
+
print(f" ✗ {label}: {detail}")
|
|
760
|
+
ppc_failed = True
|
|
761
|
+
issues += 1
|
|
762
|
+
else:
|
|
763
|
+
print(f" · {label}: {detail}")
|
|
764
|
+
if ppc_failed:
|
|
765
|
+
print(f" → Run: pty-migrate create-policy --ppc-host {ppc['host']} --ppc-password <password>")
|
|
766
|
+
print(f" (add --full to create the full Developer Edition policy)")
|
|
767
|
+
if dev_edition_uid:
|
|
768
|
+
print(f" → Ensure the DevEdition datastore (id={dev_edition_uid}) has the KMS export key configured.")
|
|
769
|
+
else:
|
|
770
|
+
print(f" → Ensure the DevEdition datastore has the KMS export key configured.")
|
|
771
|
+
|
|
772
|
+
# 7b. Round-trip protect/unprotect test (--with-protect-fn-test)
|
|
773
|
+
if do_protect_test:
|
|
774
|
+
if ok and host and auth_ok and auth_works:
|
|
775
|
+
de_ok, de_detail = _check_data_elements(host, stats)
|
|
776
|
+
if de_ok is True:
|
|
777
|
+
print(f" ✓ Protect/unprotect test: {de_detail}")
|
|
778
|
+
elif de_ok is False:
|
|
779
|
+
print(f" ✗ Protect/unprotect test: {de_detail}")
|
|
780
|
+
de_failed = True
|
|
781
|
+
issues += 1
|
|
782
|
+
else:
|
|
783
|
+
print(f" ⊘ Protect/unprotect test: {de_detail}")
|
|
784
|
+
skipped += 1
|
|
785
|
+
elif auth_works is False:
|
|
786
|
+
# Auth probe already failed — skip the full sweep so we don't
|
|
787
|
+
# hang for minutes hammering 28 DEs × N users with bad creds.
|
|
788
|
+
print(f" ⊘ Protect/unprotect test: skipped (authentication failed)")
|
|
789
|
+
skipped += 1
|
|
790
|
+
else:
|
|
791
|
+
print(f" ⊘ Protect/unprotect test: skipped (prerequisites not met)")
|
|
792
|
+
skipped += 1
|
|
793
|
+
|
|
794
|
+
# Result
|
|
795
|
+
print()
|
|
796
|
+
print(f" {'─' * 50}")
|
|
797
|
+
if issues == 0:
|
|
798
|
+
print(f" RESULT: ✓ READY FOR MIGRATION")
|
|
799
|
+
print()
|
|
800
|
+
print(f" Next steps:")
|
|
801
|
+
step = 1
|
|
802
|
+
if do_ppc and not do_protect_test:
|
|
803
|
+
print(f" {step}. Trigger the Policy Agent Lambda so the policy is published to Cloud Protect")
|
|
804
|
+
print(f" (runs hourly if CRON enabled, or invoke manually from the AWS Lambda console).")
|
|
805
|
+
step += 1
|
|
806
|
+
print(f" {step}. Re-run `pty-migrate check --with-protect-fn-test` to verify end-to-end.")
|
|
807
|
+
step += 1
|
|
808
|
+
de_vars_set = [v for v in (
|
|
809
|
+
"DEV_EDITION_EMAIL", "DEV_EDITION_PASSWORD", "DEV_EDITION_API_KEY",
|
|
810
|
+
"DEV_EDITION_HOST", "DEV_EDITION_VERSION",
|
|
811
|
+
) if os.getenv(v)]
|
|
812
|
+
if de_vars_set:
|
|
813
|
+
print(f" {step}. Remove leftover Developer Edition env vars: {', '.join(de_vars_set)}")
|
|
814
|
+
step += 1
|
|
815
|
+
print(f" {step}. Your application will now use Team Edition automatically.")
|
|
816
|
+
else:
|
|
817
|
+
print(f" RESULT: ✗ NOT READY — {issues} issue(s) to resolve")
|
|
818
|
+
print()
|
|
819
|
+
print(f" To resolve:")
|
|
820
|
+
step = 1
|
|
821
|
+
if not sdk_ok:
|
|
822
|
+
if py_status == "load_error":
|
|
823
|
+
print(f" {step}. Reinstall Python SDK: pip install --force-reinstall protegrity-ai-developer-python")
|
|
824
|
+
elif py_status == "old":
|
|
825
|
+
print(f" {step}. Upgrade Python SDK: pip install --upgrade protegrity-ai-developer-python")
|
|
826
|
+
elif java_ok is False:
|
|
827
|
+
print(f" {step}. Upgrade Java SDK to >= 1.1.0 in your pom.xml.")
|
|
828
|
+
else:
|
|
829
|
+
print(f" {step}. Install at least one SDK (Python >= 1.2.1 or Java >= 1.1.0).")
|
|
830
|
+
step += 1
|
|
831
|
+
if not ok or not host:
|
|
832
|
+
print(f" {step}. Set Team Edition host: export PTY_CP_HOST=<your-cloud-protect-url>")
|
|
833
|
+
step += 1
|
|
834
|
+
if not auth_ok:
|
|
835
|
+
for line in _auth_fix_hints(mode, detail, indent=f" {step}. ",
|
|
836
|
+
continuation=" "):
|
|
837
|
+
print(line)
|
|
838
|
+
step += 1
|
|
839
|
+
if auth_works is False:
|
|
840
|
+
print(f" {step}. Authentication is configured but Cloud Protect rejected the request.")
|
|
841
|
+
print(f" - For aws_iam: confirm the IAM principal (profile, role, or keys) is")
|
|
842
|
+
print(f" allowed by the API Gateway resource policy / Lambda authorizer for {host}.")
|
|
843
|
+
print(f" - For bearer_token / oauth2: confirm the token is valid and not expired.")
|
|
844
|
+
print(f" - For mtls: confirm PTY_CLIENT_CERT / PTY_CLIENT_KEY are trusted by CP.")
|
|
845
|
+
print(f" - Confirm the Policy Agent Lambda has synced the policy to Cloud Protect")
|
|
846
|
+
print(f" (a freshly-deployed policy may take up to an hour without manual trigger).")
|
|
847
|
+
step += 1
|
|
848
|
+
if ppc_failed:
|
|
849
|
+
print(f" {step}. Create/deploy the Developer Edition policy on PPC:")
|
|
850
|
+
print(f" pty-migrate create-policy --ppc-host <your-ppc-host> --ppc-password <password> --full")
|
|
851
|
+
step += 1
|
|
852
|
+
ds_id_token = dev_edition_uid if dev_edition_uid else "{id}"
|
|
853
|
+
print(f" {step}. Add the KMS export key to the DevEdition datastore:")
|
|
854
|
+
print(f" POST /pty/v2/pim/datastores/{ds_id_token}/export/keys with the KMS public-key PEM.")
|
|
855
|
+
if dev_edition_uid:
|
|
856
|
+
print(f" Example:")
|
|
857
|
+
print(f" curl -k -X POST https://{ppc['host']}/pty/v2/pim/datastores/{dev_edition_uid}/export/keys \\")
|
|
858
|
+
print(f" -H \"Authorization: Bearer $TOKEN\" -H \"Content-Type: application/json\" \\")
|
|
859
|
+
print(f" -d '{{\"algorithm\":\"RSA-OAEP-256\",\"pem\":\"<KMS-PUBLIC-KEY-PEM>\"}}'")
|
|
860
|
+
print(f" (The cloud-side Policy Agent Lambda reads this key from its own")
|
|
861
|
+
print(f" PTY_DATASTORE_KEY env var — it is NOT a client-side variable.)")
|
|
862
|
+
step += 1
|
|
863
|
+
print(f" {step}. Trigger the Policy Agent Lambda to sync the policy to Cloud Protect")
|
|
864
|
+
print(f" (runs hourly if CRON enabled, or invoke manually from AWS Lambda console).")
|
|
865
|
+
step += 1
|
|
866
|
+
if de_failed:
|
|
867
|
+
print(f" {step}. Re-run protect/unprotect test once the Policy Agent has synced.")
|
|
868
|
+
step += 1
|
|
869
|
+
print()
|
|
870
|
+
|
|
871
|
+
return 0 if issues == 0 else 1
|