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.
- illumio_pylo/API/APIConnector.py +1308 -0
- illumio_pylo/API/AuditLog.py +42 -0
- illumio_pylo/API/ClusterHealth.py +136 -0
- illumio_pylo/API/CredentialsManager.py +286 -0
- illumio_pylo/API/Explorer.py +1077 -0
- illumio_pylo/API/JsonPayloadTypes.py +240 -0
- illumio_pylo/API/RuleSearchQuery.py +128 -0
- illumio_pylo/API/__init__.py +0 -0
- illumio_pylo/AgentStore.py +139 -0
- illumio_pylo/Exception.py +44 -0
- illumio_pylo/Helpers/__init__.py +3 -0
- illumio_pylo/Helpers/exports.py +508 -0
- illumio_pylo/Helpers/functions.py +166 -0
- illumio_pylo/IPList.py +135 -0
- illumio_pylo/IPMap.py +285 -0
- illumio_pylo/Label.py +25 -0
- illumio_pylo/LabelCommon.py +48 -0
- illumio_pylo/LabelGroup.py +68 -0
- illumio_pylo/LabelStore.py +403 -0
- illumio_pylo/LabeledObject.py +25 -0
- illumio_pylo/Organization.py +258 -0
- illumio_pylo/Query.py +331 -0
- illumio_pylo/ReferenceTracker.py +41 -0
- illumio_pylo/Rule.py +671 -0
- illumio_pylo/Ruleset.py +306 -0
- illumio_pylo/RulesetStore.py +101 -0
- illumio_pylo/SecurityPrincipal.py +62 -0
- illumio_pylo/Service.py +256 -0
- illumio_pylo/SoftwareVersion.py +125 -0
- illumio_pylo/VirtualService.py +17 -0
- illumio_pylo/VirtualServiceStore.py +75 -0
- illumio_pylo/Workload.py +506 -0
- illumio_pylo/WorkloadStore.py +289 -0
- illumio_pylo/__init__.py +82 -0
- illumio_pylo/cli/NativeParsers.py +96 -0
- illumio_pylo/cli/__init__.py +134 -0
- illumio_pylo/cli/__main__.py +10 -0
- illumio_pylo/cli/commands/__init__.py +32 -0
- illumio_pylo/cli/commands/credential_manager.py +168 -0
- illumio_pylo/cli/commands/iplist_import_from_file.py +185 -0
- illumio_pylo/cli/commands/misc.py +7 -0
- illumio_pylo/cli/commands/ruleset_export.py +129 -0
- illumio_pylo/cli/commands/update_pce_objects_cache.py +44 -0
- illumio_pylo/cli/commands/ven_duplicate_remover.py +366 -0
- illumio_pylo/cli/commands/ven_idle_to_visibility.py +287 -0
- illumio_pylo/cli/commands/ven_upgrader.py +226 -0
- illumio_pylo/cli/commands/workload_export.py +251 -0
- illumio_pylo/cli/commands/workload_import.py +423 -0
- illumio_pylo/cli/commands/workload_relabeler.py +510 -0
- illumio_pylo/cli/commands/workload_reset_names_to_null.py +83 -0
- illumio_pylo/cli/commands/workload_used_in_rule_finder.py +80 -0
- illumio_pylo/docs/Doxygen +1757 -0
- illumio_pylo/tmp.py +104 -0
- illumio_pylo/utilities/__init__.py +0 -0
- illumio_pylo/utilities/cli.py +10 -0
- illumio_pylo/utilities/credentials.example.json +20 -0
- illumio_pylo/utilities/explorer_report_exporter.py +86 -0
- illumio_pylo/utilities/health_monitoring.py +102 -0
- illumio_pylo/utilities/iplist_analyzer.py +148 -0
- illumio_pylo/utilities/iplists_stats_duplicates_unused_finder.py +75 -0
- illumio_pylo/utilities/resources/iplists-import-example.csv +3 -0
- illumio_pylo/utilities/resources/iplists-import-example.xlsx +0 -0
- illumio_pylo/utilities/resources/workload-exporter-filter-example.csv +3 -0
- illumio_pylo/utilities/resources/workloads-import-example.csv +2 -0
- illumio_pylo/utilities/resources/workloads-import-example.xlsx +0 -0
- illumio_pylo/utilities/ven_compatibility_report_export.py +240 -0
- illumio_pylo/utilities/ven_idle_to_illumination.py +344 -0
- illumio_pylo/utilities/ven_reassign_pce.py +183 -0
- illumio_pylo-0.2.5.dist-info/LICENSE +176 -0
- illumio_pylo-0.2.5.dist-info/METADATA +197 -0
- illumio_pylo-0.2.5.dist-info/RECORD +73 -0
- illumio_pylo-0.2.5.dist-info/WHEEL +5 -0
- 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$:")
|