illumio-pylo 0.2.5__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 (73) hide show
  1. illumio_pylo/API/APIConnector.py +1308 -0
  2. illumio_pylo/API/AuditLog.py +42 -0
  3. illumio_pylo/API/ClusterHealth.py +136 -0
  4. illumio_pylo/API/CredentialsManager.py +286 -0
  5. illumio_pylo/API/Explorer.py +1077 -0
  6. illumio_pylo/API/JsonPayloadTypes.py +240 -0
  7. illumio_pylo/API/RuleSearchQuery.py +128 -0
  8. illumio_pylo/API/__init__.py +0 -0
  9. illumio_pylo/AgentStore.py +139 -0
  10. illumio_pylo/Exception.py +44 -0
  11. illumio_pylo/Helpers/__init__.py +3 -0
  12. illumio_pylo/Helpers/exports.py +508 -0
  13. illumio_pylo/Helpers/functions.py +166 -0
  14. illumio_pylo/IPList.py +135 -0
  15. illumio_pylo/IPMap.py +285 -0
  16. illumio_pylo/Label.py +25 -0
  17. illumio_pylo/LabelCommon.py +48 -0
  18. illumio_pylo/LabelGroup.py +68 -0
  19. illumio_pylo/LabelStore.py +403 -0
  20. illumio_pylo/LabeledObject.py +25 -0
  21. illumio_pylo/Organization.py +258 -0
  22. illumio_pylo/Query.py +331 -0
  23. illumio_pylo/ReferenceTracker.py +41 -0
  24. illumio_pylo/Rule.py +671 -0
  25. illumio_pylo/Ruleset.py +306 -0
  26. illumio_pylo/RulesetStore.py +101 -0
  27. illumio_pylo/SecurityPrincipal.py +62 -0
  28. illumio_pylo/Service.py +256 -0
  29. illumio_pylo/SoftwareVersion.py +125 -0
  30. illumio_pylo/VirtualService.py +17 -0
  31. illumio_pylo/VirtualServiceStore.py +75 -0
  32. illumio_pylo/Workload.py +506 -0
  33. illumio_pylo/WorkloadStore.py +289 -0
  34. illumio_pylo/__init__.py +82 -0
  35. illumio_pylo/cli/NativeParsers.py +96 -0
  36. illumio_pylo/cli/__init__.py +134 -0
  37. illumio_pylo/cli/__main__.py +10 -0
  38. illumio_pylo/cli/commands/__init__.py +32 -0
  39. illumio_pylo/cli/commands/credential_manager.py +168 -0
  40. illumio_pylo/cli/commands/iplist_import_from_file.py +185 -0
  41. illumio_pylo/cli/commands/misc.py +7 -0
  42. illumio_pylo/cli/commands/ruleset_export.py +129 -0
  43. illumio_pylo/cli/commands/update_pce_objects_cache.py +44 -0
  44. illumio_pylo/cli/commands/ven_duplicate_remover.py +366 -0
  45. illumio_pylo/cli/commands/ven_idle_to_visibility.py +287 -0
  46. illumio_pylo/cli/commands/ven_upgrader.py +226 -0
  47. illumio_pylo/cli/commands/workload_export.py +251 -0
  48. illumio_pylo/cli/commands/workload_import.py +423 -0
  49. illumio_pylo/cli/commands/workload_relabeler.py +510 -0
  50. illumio_pylo/cli/commands/workload_reset_names_to_null.py +83 -0
  51. illumio_pylo/cli/commands/workload_used_in_rule_finder.py +80 -0
  52. illumio_pylo/docs/Doxygen +1757 -0
  53. illumio_pylo/tmp.py +104 -0
  54. illumio_pylo/utilities/__init__.py +0 -0
  55. illumio_pylo/utilities/cli.py +10 -0
  56. illumio_pylo/utilities/credentials.example.json +20 -0
  57. illumio_pylo/utilities/explorer_report_exporter.py +86 -0
  58. illumio_pylo/utilities/health_monitoring.py +102 -0
  59. illumio_pylo/utilities/iplist_analyzer.py +148 -0
  60. illumio_pylo/utilities/iplists_stats_duplicates_unused_finder.py +75 -0
  61. illumio_pylo/utilities/resources/iplists-import-example.csv +3 -0
  62. illumio_pylo/utilities/resources/iplists-import-example.xlsx +0 -0
  63. illumio_pylo/utilities/resources/workload-exporter-filter-example.csv +3 -0
  64. illumio_pylo/utilities/resources/workloads-import-example.csv +2 -0
  65. illumio_pylo/utilities/resources/workloads-import-example.xlsx +0 -0
  66. illumio_pylo/utilities/ven_compatibility_report_export.py +240 -0
  67. illumio_pylo/utilities/ven_idle_to_illumination.py +344 -0
  68. illumio_pylo/utilities/ven_reassign_pce.py +183 -0
  69. illumio_pylo-0.2.5.dist-info/LICENSE +176 -0
  70. illumio_pylo-0.2.5.dist-info/METADATA +197 -0
  71. illumio_pylo-0.2.5.dist-info/RECORD +73 -0
  72. illumio_pylo-0.2.5.dist-info/WHEEL +5 -0
  73. illumio_pylo-0.2.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,42 @@
1
+ import logging
2
+ from typing import Optional, List, Literal
3
+
4
+ import illumio_pylo as pylo
5
+ from illumio_pylo.API.JsonPayloadTypes import AuditLogApiReplyEventJsonStructure, AuditLogApiEventType
6
+
7
+
8
+ class AuditLogQueryResult:
9
+ def __init__(self, raw_json: AuditLogApiReplyEventJsonStructure):
10
+ self.raw_json: AuditLogApiReplyEventJsonStructure = raw_json
11
+
12
+ def type_is(self, log_type: AuditLogApiEventType):
13
+ return self.raw_json == log_type
14
+
15
+
16
+ class AuditLogQueryResultSet:
17
+ def __init__(self, json_data: List[AuditLogApiReplyEventJsonStructure]):
18
+ self.results: List[AuditLogQueryResult] = []
19
+ #read json data in reverse order
20
+ for log in json_data[::-1]:
21
+ self.results.append(AuditLogQueryResult(log))
22
+
23
+
24
+ class AuditLogFilterSet:
25
+ def __init__(self):
26
+ self.event_type: Optional[str] = None
27
+ self.datetime_starts_from = None
28
+ self.datetime_ends_at = None
29
+
30
+
31
+ class AuditLogQuery:
32
+ def __init__(self, connector: 'pylo.APIConnector', max_results: int = 1000, max_running_time_seconds: int = 120):
33
+ self.api: 'pylo.APIConnector' = connector
34
+ self.filters: AuditLogFilterSet = AuditLogFilterSet()
35
+ self.max_running_time_seconds = max_running_time_seconds
36
+ self.max_results = max_results
37
+
38
+ def execute(self) -> AuditLogQueryResultSet:
39
+ json_result = self.api.audit_log_query(max_results=self.max_results, event_type= self.filters.event_type)
40
+ result_set = AuditLogQueryResultSet(json_result)
41
+ return result_set
42
+
@@ -0,0 +1,136 @@
1
+ from .APIConnector import APIConnector, get_field_or_die
2
+ from .. import PyloEx, string_list_to_text
3
+
4
+
5
+ class ClusterHealth:
6
+
7
+ allowed_status_list = {'normal': True, 'warning': True, 'error': True}
8
+
9
+ class ClusterHealthNode:
10
+
11
+ class ServiceStatus:
12
+
13
+ allowed_status = {'running': True, 'stopped': True, 'partial': True, 'not_running': True}
14
+
15
+ def __init__(self, name: str, status: str):
16
+ self.name = name
17
+ if status not in ClusterHealth.ClusterHealthNode.ServiceStatus.allowed_status:
18
+ raise PyloEx("A services status is created with unsupported status '{}'".format(status))
19
+
20
+ self.status = status # type: str
21
+
22
+ def is_running(self):
23
+ return self.status == 'running'
24
+
25
+ def is_not_running(self):
26
+ return self.status == 'stopped'
27
+
28
+ def is_partially_running(self):
29
+ return self.status == 'partial'
30
+
31
+ def __init__(self, json_data):
32
+ self.name = get_field_or_die('hostname', json_data) # type: str
33
+ self.type = get_field_or_die('type', json_data) # type: str
34
+ self.ip_address = get_field_or_die('ip_address', json_data) # type: str
35
+ self.runlevel = get_field_or_die('runlevel', json_data) # type: int
36
+
37
+ self.services = {} # type: dict[str, ClusterHealth.ClusterHealthNode.ServiceStatus]
38
+ services_statuses = get_field_or_die('services', json_data)
39
+
40
+ self.global_service_status = get_field_or_die('status', services_statuses)
41
+
42
+ def process_services(json_data: dict, status):
43
+ services = json_data.get(status)
44
+ if services is None:
45
+ return
46
+
47
+ for service in services:
48
+ new_service = ClusterHealth.ClusterHealthNode.ServiceStatus(service, status)
49
+ if new_service.name in self.services:
50
+ raise PyloEx("duplicated service name '{}'".format(new_service.name))
51
+ self.services[new_service.name] = new_service
52
+
53
+ process_services(services_statuses, 'running')
54
+ process_services(services_statuses, 'not_running')
55
+ process_services(services_statuses, 'partial')
56
+ process_services(services_statuses, 'optional')
57
+
58
+ def is_offline_or_unreachable(self):
59
+ if self.runlevel is None:
60
+ return True
61
+ return False
62
+
63
+ def get_troubled_services(self):
64
+ ret = []
65
+ for service in self.services.values():
66
+ if not service.is_running():
67
+ ret.append(service)
68
+ return ret
69
+
70
+ def get_running_services(self):
71
+ ret = []
72
+ for service in self.services.values():
73
+ if service.is_running():
74
+ ret.append(service)
75
+ return ret
76
+
77
+ def to_string(self, indent='', marker='*'):
78
+ def val_str(display_name: str, value):
79
+ return "{}{}{}: {}\n".format(indent, marker, display_name, value)
80
+ ret = ''
81
+ ret += val_str('fqdn', self.name)
82
+ indent += ' '
83
+ marker = '-'
84
+
85
+ ret += val_str('ip_address', self.ip_address)
86
+ ret += val_str('type', self.type)
87
+ if self.runlevel is None:
88
+ ret += val_str('runlevel', 'not running or not available')
89
+ return ret
90
+ else:
91
+ ret += val_str('runlevel', self.runlevel)
92
+
93
+ troubled_services = string_list_to_text(self.get_troubled_services())
94
+ ret += val_str('non-functional services', troubled_services)
95
+
96
+ running_services = string_list_to_text(self.get_running_services())
97
+ ret += val_str('running services', running_services)
98
+
99
+ return ret
100
+
101
+ def __init__(self, json_data):
102
+ self.raw_json = json_data
103
+
104
+ self.fqdn = get_field_or_die('fqdn', json_data) # type: str
105
+ self._status = get_field_or_die('status', json_data) # type: str
106
+ self.type = get_field_or_die('type', json_data) # type: str
107
+
108
+ if self._status not in ClusterHealth.allowed_status_list:
109
+ raise PyloEx("ClusterHealth has unsupported status '{}'".format(self._status))
110
+
111
+ nodes_list = get_field_or_die('nodes', json_data)
112
+ self.nodes_dict = {}
113
+
114
+ for node in nodes_list:
115
+ new_node = ClusterHealth.ClusterHealthNode(node)
116
+ self.nodes_dict[new_node.name] = new_node
117
+
118
+ def to_string(self):
119
+ ret = ''
120
+ ret += "cluster fqdn: '{}'\n".format(self.fqdn)
121
+ ret += "type: '{}'\n".format(self.type)
122
+ ret += "status: '{}'\n".format(self._status)
123
+ ret += "nodes details:\n"
124
+ for node in self.nodes_dict.values():
125
+ ret += node.to_string(indent=' ')
126
+
127
+ return ret
128
+
129
+ def status_is_ok(self):
130
+ return self._status == 'normal'
131
+
132
+ def status_is_warning(self):
133
+ return self._status == 'warning'
134
+
135
+ def status_is_error(self):
136
+ return self._status == 'error'
@@ -0,0 +1,286 @@
1
+ import base64
2
+ from hashlib import sha256
3
+ from typing import Dict, TypedDict, Union, List, Optional
4
+ import json
5
+ import os
6
+ from cryptography.fernet import Fernet
7
+ from ..Exception import PyloEx
8
+ from .. import log
9
+
10
+ try:
11
+ import paramiko
12
+ except ImportError:
13
+ log.debug("Paramiko library not found, SSH based encryption will not be available")
14
+ paramiko = None
15
+
16
+
17
+
18
+ class CredentialFileEntry(TypedDict):
19
+ name: str
20
+ fqdn: str
21
+ port: int
22
+ api_user: str
23
+ api_key: str
24
+ org_id: int
25
+ verify_ssl: bool
26
+
27
+
28
+ class CredentialProfile:
29
+ name: str
30
+ fqdn: str
31
+ port: int
32
+ api_user: str
33
+ api_key: str
34
+ org_id: int
35
+ verify_ssl: bool
36
+
37
+ def __init__(self, name: str, fqdn: str, port: int, api_user: str, api_key: str, org_id: int, verify_ssl: bool, originating_file: Optional[str] = None):
38
+ self.name = name
39
+ self.fqdn = fqdn
40
+ self.port = port
41
+ self.api_user = api_user
42
+ self.api_key = api_key
43
+ self.org_id = org_id
44
+ self.verify_ssl = verify_ssl
45
+ self.originating_file = originating_file
46
+
47
+ self.raw_json: Optional[CredentialFileEntry] = None
48
+
49
+
50
+ @staticmethod
51
+ def from_credentials_file_entry(credential_file_entry: CredentialFileEntry, originating_file: Optional[str] = None):
52
+ return CredentialProfile(credential_file_entry['name'],
53
+ credential_file_entry['fqdn'],
54
+ credential_file_entry['port'],
55
+ credential_file_entry['api_user'],
56
+ credential_file_entry['api_key'],
57
+ credential_file_entry['org_id'],
58
+ credential_file_entry['verify_ssl'],
59
+ originating_file)
60
+
61
+
62
+ CredentialsFileType = Union[CredentialFileEntry | List[CredentialFileEntry]]
63
+
64
+
65
+ def check_profile_json_structure(profile: Dict) -> None:
66
+ # ensure all fields from CredentialFileEntry are present
67
+ if "name" not in profile or type(profile["name"]) != str:
68
+ raise PyloEx("The profile {} does not contain a name".format(profile))
69
+ if "fqdn" not in profile:
70
+ raise PyloEx("The profile {} does not contain a fqdn".format(profile))
71
+ if "port" not in profile:
72
+ raise PyloEx("The profile {} does not contain a port".format(profile))
73
+ if "api_user" not in profile:
74
+ raise PyloEx("The profile {} does not contain an api_user".format(profile))
75
+ if "api_key" not in profile:
76
+ raise PyloEx("The profile {} does not contain an api_key".format(profile))
77
+ if "org_id" not in profile:
78
+ raise PyloEx("The profile {} does not contain an organization_id".format(profile))
79
+ if "verify_ssl" not in profile:
80
+ raise PyloEx("The profile {} does not contain a verify_ssl".format(profile))
81
+
82
+
83
+ def get_all_credentials_from_file(credential_file: str ) -> List[CredentialProfile]:
84
+ log.debug("Loading credentials from file: {}".format(credential_file))
85
+ with open(credential_file, 'r') as f:
86
+ credentials: CredentialsFileType = json.load(f)
87
+ profiles: List[CredentialProfile] = []
88
+ if isinstance(credentials, list):
89
+ for profile in credentials:
90
+ check_profile_json_structure(profile)
91
+ profiles.append(CredentialProfile.from_credentials_file_entry(profile, credential_file))
92
+ else:
93
+ check_profile_json_structure(credentials)
94
+ profiles.append(CredentialProfile.from_credentials_file_entry(credentials, credential_file))
95
+
96
+ return profiles
97
+
98
+
99
+ def get_credentials_from_file(fqdn_or_profile_name: str = None,
100
+ credential_file: str = None) -> CredentialProfile:
101
+
102
+ if fqdn_or_profile_name is None:
103
+ log.debug("No fqdn_or_profile_name provided, profile_name=default will be used")
104
+ fqdn_or_profile_name = "default"
105
+
106
+ credential_files: List[str] = []
107
+ if credential_file is not None:
108
+ credential_files.append(credential_file)
109
+ else:
110
+ credential_files = list_potential_credential_files()
111
+
112
+ credentials: List[CredentialProfile] = []
113
+
114
+ for file in credential_files:
115
+ log.debug("Loading credentials from file: {}".format(credential_file))
116
+ credentials.extend(get_all_credentials_from_file(file))
117
+
118
+ for credential_profile in credentials:
119
+ if credential_profile.name.lower() == fqdn_or_profile_name.lower():
120
+ return credential_profile
121
+ if credential_profile.fqdn.lower() == fqdn_or_profile_name.lower():
122
+ return credential_profile
123
+
124
+ raise PyloEx("No profile found in credential file '{}' with fqdn: {}".
125
+ format(credential_file, fqdn_or_profile_name))
126
+
127
+
128
+ def list_potential_credential_files() -> List[str]:
129
+ """
130
+ List the potential locations where a credential file could be found and return them if they exist
131
+ :return:
132
+ """
133
+ potential_credential_files = []
134
+ if os.environ.get('Pylo_CREDENTIAL_FILE', None) is not None:
135
+ potential_credential_files.append(os.environ.get('Pylo_CREDENTIAL_FILE'))
136
+ potential_credential_files.append(os.path.expanduser("~/.pylo/credentials.json"))
137
+ potential_credential_files.append(os.path.join(os.getcwd(), "credentials.json"))
138
+
139
+ return [file for file in potential_credential_files if os.path.exists(file)]
140
+
141
+
142
+ def get_all_credentials() -> List[CredentialProfile]:
143
+ """
144
+ Get all credentials from all potential credential files
145
+ :return:
146
+ """
147
+ credential_files = list_potential_credential_files()
148
+ credentials = []
149
+ for file in credential_files:
150
+ credentials.extend(get_all_credentials_from_file(file))
151
+ return credentials
152
+
153
+
154
+ def create_credential_in_file(file_full_path: str, data: CredentialFileEntry, overwrite_existing_profile = False) -> str:
155
+ """
156
+ Create a credential in a file and return the full path to the file
157
+ :param file_full_path:
158
+ :param data:
159
+ :param overwrite_existing_profile:
160
+ :return:
161
+ """
162
+ # if file already exists, load it and append the new credential to it
163
+ if os.path.isdir(file_full_path):
164
+ file_full_path = os.path.join(file_full_path, "credentials.json")
165
+
166
+ if os.path.exists(file_full_path):
167
+ with open(file_full_path, 'r') as f:
168
+ credentials: CredentialsFileType = json.load(f)
169
+ if isinstance(credentials, list):
170
+ # check if the profile already exists
171
+ for profile in credentials:
172
+ if profile['name'].lower() == data['name'].lower():
173
+ if overwrite_existing_profile:
174
+ profile = data
175
+ break
176
+ else:
177
+ raise PyloEx("Profile with name {} already exists in file {}".format(data['name'], file_full_path))
178
+ credentials.append(data)
179
+ else:
180
+ if data['name'].lower() == credentials['name'].lower():
181
+ if overwrite_existing_profile:
182
+ credentials = data
183
+ else:
184
+ raise PyloEx("Profile with name {} already exists in file {}".format(data['name'], file_full_path))
185
+ else:
186
+ credentials = [credentials, data]
187
+ else:
188
+ credentials = [data]
189
+
190
+ # write to the file
191
+ with open(file_full_path, 'w') as f:
192
+ json.dump(credentials, f, indent=4)
193
+
194
+ return file_full_path
195
+
196
+ def create_credential_in_default_file(data: CredentialFileEntry) -> str:
197
+ """
198
+ Create a credential in the default credential file and return the full path to the file
199
+ :param data:
200
+ :return:
201
+ """
202
+ file_path = os.path.expanduser("~/.pylo/credentials.json")
203
+ create_credential_in_file(os.path.expanduser(file_path), data)
204
+ return file_path
205
+
206
+
207
+ def encrypt_api_key_with_paramiko_key(ssh_key: paramiko.AgentKey, api_key: str) -> str:
208
+
209
+
210
+ def encrypt(raw: str, key: bytes) -> bytes:
211
+ """
212
+
213
+ :param raw:
214
+ :param key:
215
+ :return: base64 encoded encrypted string
216
+ """
217
+ f = Fernet(base64.urlsafe_b64encode(key))
218
+ token = f.encrypt(bytes(raw, 'utf-8'))
219
+ return token
220
+
221
+
222
+ # generate a random 128bit key
223
+ session_key_to_sign = os.urandom(16)
224
+
225
+ signed_message = ssh_key.sign_ssh_data(session_key_to_sign)
226
+
227
+ # use SHA256 to hash the signed message and use it as final AES 256 key
228
+ encryption_key = sha256(signed_message).digest()
229
+ #print("Encryption key: {}".format(encryption_key.hex()))
230
+ encrypted_text = encrypt(api_key, encryption_key)
231
+
232
+ api_key = "$encrypted$:ssh-Fernet:{}:{}:{}".format(base64.urlsafe_b64encode(ssh_key.get_fingerprint()).decode('utf-8'),
233
+ base64.urlsafe_b64encode(session_key_to_sign).decode('utf-8'),
234
+ encrypted_text.decode('utf-8'))
235
+
236
+ return api_key
237
+
238
+
239
+ def decrypt_api_key_with_paramiko_key(encrypted_api_key_payload: str) -> str:
240
+ def decrypt(token_b64_encoded: str, key: bytes):
241
+ f = Fernet(base64.urlsafe_b64encode(key))
242
+ return f.decrypt(token_b64_encoded).decode('utf-8')
243
+
244
+ # split the api_key into its components
245
+ api_key_parts = encrypted_api_key_payload.split(":")
246
+ if len(api_key_parts) != 5:
247
+ raise PyloEx("Invalid encrypted API key format")
248
+
249
+ # get the fingerprint and the session key
250
+ fingerprint = base64.urlsafe_b64decode(api_key_parts[2])
251
+ session_key = base64.urlsafe_b64decode(api_key_parts[3])
252
+ encrypted_api_key = api_key_parts[4]
253
+
254
+ # find the key in the agent
255
+ keys = paramiko.Agent().get_keys()
256
+ found_key = None
257
+ for key in keys:
258
+ if key.get_fingerprint() == fingerprint:
259
+ found_key = key
260
+ break
261
+
262
+ if found_key is None:
263
+ raise PyloEx("No key found in the agent with fingerprint {}".format(fingerprint.hex()))
264
+
265
+ # sign the session key
266
+ signed_session_key = found_key.sign_ssh_data(session_key)
267
+ encryption_key = sha256(signed_session_key).digest()
268
+ #print("Encryption key: {}".format(encryption_key.hex()))
269
+ #print("Encrypted from KEY fingerprint: {}".format(fingerprint.hex()))
270
+
271
+ return decrypt(token_b64_encoded=encrypted_api_key,
272
+ key=encryption_key
273
+ )
274
+
275
+ def decrypt_api_key(encrypted_api_key_payload: str) -> str:
276
+ # detect the encryption method
277
+ if not encrypted_api_key_payload.startswith("$encrypted$:"):
278
+ raise PyloEx("Invalid encrypted API key format")
279
+ if encrypted_api_key_payload.startswith("$encrypted$:ssh-Fernet:"):
280
+ return decrypt_api_key_with_paramiko_key(encrypted_api_key_payload)
281
+
282
+ raise PyloEx("Unsupported encryption method: {}".format(encrypted_api_key_payload.split(":")[1]))
283
+
284
+
285
+ def is_api_key_encrypted(encrypted_api_key_payload: str) -> bool:
286
+ return encrypted_api_key_payload.startswith("$encrypted$:")