praetorian-cli 2.2.13__py3-none-any.whl → 2.2.15__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.
- praetorian_cli/handlers/update.py +8 -2
- praetorian_cli/sdk/chariot.py +25 -14
- praetorian_cli/sdk/entities/risks.py +45 -2
- praetorian_cli/sdk/test/test_file.py +16 -0
- praetorian_cli/sdk/test/test_risk.py +28 -0
- {praetorian_cli-2.2.13.dist-info → praetorian_cli-2.2.15.dist-info}/METADATA +1 -1
- {praetorian_cli-2.2.13.dist-info → praetorian_cli-2.2.15.dist-info}/RECORD +11 -11
- {praetorian_cli-2.2.13.dist-info → praetorian_cli-2.2.15.dist-info}/WHEEL +0 -0
- {praetorian_cli-2.2.13.dist-info → praetorian_cli-2.2.15.dist-info}/entry_points.txt +0 -0
- {praetorian_cli-2.2.13.dist-info → praetorian_cli-2.2.15.dist-info}/licenses/LICENSE +0 -0
- {praetorian_cli-2.2.13.dist-info → praetorian_cli-2.2.15.dist-info}/top_level.txt +0 -0
|
@@ -36,7 +36,8 @@ def asset(chariot, key, status, surface):
|
|
|
36
36
|
@click.argument('key', required=True)
|
|
37
37
|
@click.option('-s', '--status', type=click.Choice([s.value for s in Risk]), help=f'Status of the risk')
|
|
38
38
|
@click.option('-c', '--comment', default='', help='Comment for the risk')
|
|
39
|
-
|
|
39
|
+
@click.option('-r', '--remove-comment', type=int, default=None, help='Remove comment at index (0, 1, ... or -1 for most recent)')
|
|
40
|
+
def risk(chariot, key, status, comment, remove_comment):
|
|
40
41
|
""" Update the status and comment of a risk
|
|
41
42
|
|
|
42
43
|
\b
|
|
@@ -47,8 +48,13 @@ def risk(chariot, key, status, comment):
|
|
|
47
48
|
Example usages:
|
|
48
49
|
- praetorian chariot update risk "#risk#www.example.com#CVE-2024-23049" --status OH --comment "Open it as a high severity risk"
|
|
49
50
|
- praetorian chariot update risk "#risk#www.example.com#open-ssh-port" --status RH --comment "John stopped sshd on the server"
|
|
51
|
+
- praetorian chariot update risk "#risk#www.example.com#CVE-2024-23049" --remove-comment 0
|
|
52
|
+
- praetorian chariot update risk "#risk#www.example.com#CVE-2024-23049" --remove-comment -1
|
|
50
53
|
"""
|
|
51
|
-
|
|
54
|
+
if comment and remove_comment is not None:
|
|
55
|
+
raise click.UsageError("Cannot use --comment and --remove-comment together")
|
|
56
|
+
|
|
57
|
+
chariot.risks.update(key, status, comment, remove_comment)
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
@update.command()
|
praetorian_cli/sdk/chariot.py
CHANGED
|
@@ -64,25 +64,19 @@ class Chariot:
|
|
|
64
64
|
import urllib3
|
|
65
65
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
66
66
|
|
|
67
|
-
def chariot_request(self, method: str, url: str, **kwargs) -> requests.Response:
|
|
67
|
+
def chariot_request(self, method: str, url: str, headers: dict = {}, **kwargs) -> requests.Response:
|
|
68
68
|
"""
|
|
69
|
-
Centralized
|
|
69
|
+
Centralized wrapper around requests.request. Take care of proxy, beta flag, and
|
|
70
|
+
supplies the authentication headers
|
|
70
71
|
"""
|
|
71
|
-
|
|
72
72
|
self.add_beta_url_param(kwargs)
|
|
73
73
|
|
|
74
|
-
return self.request(method, url, self.keychain.headers(), **kwargs)
|
|
75
|
-
|
|
76
|
-
def request(self, method: str, url: str, headers: dict = None, **kwargs) -> requests.Response:
|
|
77
|
-
"""
|
|
78
|
-
Centralized wrapper around requests.request, ensuring the HTTP proxy is respected.
|
|
79
|
-
"""
|
|
80
|
-
|
|
81
74
|
if self.proxy:
|
|
82
75
|
kwargs['proxies'] = {'http': self.proxy, 'https': self.proxy}
|
|
83
76
|
kwargs['verify'] = False
|
|
84
77
|
|
|
85
|
-
return requests.request(method, url, headers=headers, **kwargs)
|
|
78
|
+
return requests.request(method, url, headers=(headers | self.keychain.headers()), **kwargs)
|
|
79
|
+
|
|
86
80
|
|
|
87
81
|
def add_beta_url_param(self, kwargs: dict):
|
|
88
82
|
if 'params' in kwargs:
|
|
@@ -206,8 +200,12 @@ class Chariot:
|
|
|
206
200
|
return resp
|
|
207
201
|
|
|
208
202
|
def _upload(self, chariot_filepath: str, content: str) -> dict:
|
|
209
|
-
#
|
|
210
|
-
#
|
|
203
|
+
# Encrypted files have _encrypted/ prefix in the path. Encrypted files do not use presigned URLs.
|
|
204
|
+
# Instead, they use the /encrypted-file endpoint that directly gets and puts content.
|
|
205
|
+
if is_encrypted_partition(chariot_filepath):
|
|
206
|
+
return self.chariot_request('PUT', self.url('/encrypted-file'), params=dict(name=chariot_filepath), data=content)
|
|
207
|
+
|
|
208
|
+
# Regular files use presigned URLs
|
|
211
209
|
presigned_url = self.chariot_request('PUT', self.url('/file'), params=dict(name=chariot_filepath))
|
|
212
210
|
process_failure(presigned_url)
|
|
213
211
|
resp = requests.put(presigned_url.json()['url'], data=content)
|
|
@@ -216,6 +214,15 @@ class Chariot:
|
|
|
216
214
|
|
|
217
215
|
def download(self, name: str, global_=False) -> bytes:
|
|
218
216
|
params = dict(name=name)
|
|
217
|
+
# Encrypted files have _encrypted/ prefix in the path. Encrypted files do not use presigned URLs.
|
|
218
|
+
# Instead, they use the /encrypted-file endpoint that directly gets and puts content.
|
|
219
|
+
if is_encrypted_partition(name):
|
|
220
|
+
accept_binary = {'Accept': 'application/octet-stream'}
|
|
221
|
+
resp = self.chariot_request('GET', self.url('/encrypted-file'), params=params, headers=accept_binary)
|
|
222
|
+
process_failure(resp)
|
|
223
|
+
return resp.content
|
|
224
|
+
|
|
225
|
+
# Regular files, use presigned URLs
|
|
219
226
|
if global_:
|
|
220
227
|
params |= GLOBAL_FLAG
|
|
221
228
|
|
|
@@ -228,7 +235,7 @@ class Chariot:
|
|
|
228
235
|
message = f'Download request failed: response missing URL' + (f'\nBody: {resp.text}' if resp.text else '(empty)')
|
|
229
236
|
raise Exception(message)
|
|
230
237
|
|
|
231
|
-
resp =
|
|
238
|
+
resp = requests.request('GET', url)
|
|
232
239
|
process_failure(resp)
|
|
233
240
|
return resp.content
|
|
234
241
|
|
|
@@ -355,3 +362,7 @@ def extend(accumulate: dict, new: dict) -> dict:
|
|
|
355
362
|
extend(accumulate[key], value)
|
|
356
363
|
|
|
357
364
|
return accumulate
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def is_encrypted_partition(chariot_filepath: str) -> bool:
|
|
368
|
+
return chariot_filepath.startswith('_encrypted/')
|
|
@@ -44,9 +44,9 @@ class Risks:
|
|
|
44
44
|
risk['affected_assets'] = self.affected_assets(key)
|
|
45
45
|
return risk
|
|
46
46
|
|
|
47
|
-
def update(self, key, status=None, comment=None):
|
|
47
|
+
def update(self, key, status=None, comment=None, remove_comment=None):
|
|
48
48
|
"""
|
|
49
|
-
Update a risk's status and/or comment.
|
|
49
|
+
Update a risk's status and/or comment, or remove a comment.
|
|
50
50
|
|
|
51
51
|
:param key: The key of the risk. If you supply a prefix that matches multiple risks, all of them will be updated
|
|
52
52
|
:type key: str
|
|
@@ -54,6 +54,8 @@ class Risks:
|
|
|
54
54
|
:type status: str or None
|
|
55
55
|
:param comment: Comment for the risk update
|
|
56
56
|
:type comment: str or None
|
|
57
|
+
:param remove_comment: Index of comment to remove (0, 1, ... or -1 for most recent)
|
|
58
|
+
:type remove_comment: int or None
|
|
57
59
|
:return: API response containing update results
|
|
58
60
|
:rtype: dict
|
|
59
61
|
"""
|
|
@@ -62,6 +64,9 @@ class Risks:
|
|
|
62
64
|
params = params | dict(status=status)
|
|
63
65
|
if comment:
|
|
64
66
|
params = params | dict(comment=comment)
|
|
67
|
+
if remove_comment is not None:
|
|
68
|
+
index = self.resolve_comment_entry_index(key, remove_comment)
|
|
69
|
+
params = params | dict(remove=index)
|
|
65
70
|
|
|
66
71
|
return self.api.upsert('risk', params)
|
|
67
72
|
|
|
@@ -159,3 +164,41 @@ class Risks:
|
|
|
159
164
|
assets.extend(indirect_assets)
|
|
160
165
|
assets.extend(web_assets)
|
|
161
166
|
return assets
|
|
167
|
+
|
|
168
|
+
def resolve_comment_entry_index(self, key, note_index):
|
|
169
|
+
"""
|
|
170
|
+
Translate a note index to the actual history array index.
|
|
171
|
+
|
|
172
|
+
:param key: The key of the risk
|
|
173
|
+
:param note_index: Index into note entries (0, 1, ... or -1 for most recent note)
|
|
174
|
+
:return: The actual index in the history array
|
|
175
|
+
"""
|
|
176
|
+
risk = self.get(key)
|
|
177
|
+
history = risk.get('history', [])
|
|
178
|
+
note_indices = get_note_entry_indices(history)
|
|
179
|
+
|
|
180
|
+
if len(note_indices) == 0:
|
|
181
|
+
raise Exception(f"Risk {key} has no notes to remove")
|
|
182
|
+
|
|
183
|
+
# Handle negative indexing (e.g., -1 for last note)
|
|
184
|
+
if note_index < 0:
|
|
185
|
+
note_index = len(note_indices) + note_index
|
|
186
|
+
|
|
187
|
+
if note_index < 0 or note_index >= len(note_indices):
|
|
188
|
+
raise Exception(f"Note index {note_index} is out of range (0 to {len(note_indices) - 1})")
|
|
189
|
+
|
|
190
|
+
return note_indices[note_index]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_note_entries(risk):
|
|
194
|
+
history = risk.get('history', [])
|
|
195
|
+
return [entry for entry in history if is_note_entry(entry)]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_note_entry_indices(history):
|
|
199
|
+
"""Return the indices in the history array that are note entries."""
|
|
200
|
+
return [i for i, entry in enumerate(history) if is_note_entry(entry)]
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def is_note_entry(entry):
|
|
204
|
+
return entry.get('comment') and entry.get('from') is None
|
|
@@ -12,6 +12,7 @@ class TestFile:
|
|
|
12
12
|
self.sdk = setup_chariot()
|
|
13
13
|
micro = epoch_micro()
|
|
14
14
|
self.chariot_filepath = f'home/test-file-{micro}.txt'
|
|
15
|
+
self.encrypted_chariot_filepath = f'_encrypted/test-file-{micro}.txt'
|
|
15
16
|
self.sanitized_filepath = f'home_test-file-{micro}.txt'
|
|
16
17
|
self.bogus_filepath = f'bogus-filepath-{micro}.txt'
|
|
17
18
|
self.local_filepath = f'./test-file-{micro}.txt'
|
|
@@ -52,6 +53,21 @@ class TestFile:
|
|
|
52
53
|
self.sdk.files.get(self.chariot_filepath)
|
|
53
54
|
assert str(ex_info.value) == f'File {self.chariot_filepath} not found.'
|
|
54
55
|
|
|
56
|
+
def test_add_encrypted_file(self):
|
|
57
|
+
self.sdk.files.add(self.local_filepath, self.encrypted_chariot_filepath)
|
|
58
|
+
files, offset = self.sdk.files.list(self.encrypted_chariot_filepath)
|
|
59
|
+
assert files[0]['name'] == self.encrypted_chariot_filepath
|
|
60
|
+
|
|
61
|
+
def test_get_encrypted_file(self):
|
|
62
|
+
content = self.sdk.files.get_utf8(self.encrypted_chariot_filepath)
|
|
63
|
+
assert content == self.content
|
|
64
|
+
|
|
65
|
+
def test_delete_encrypted_file(self):
|
|
66
|
+
self.sdk.files.delete(self.encrypted_chariot_filepath)
|
|
67
|
+
with pytest.raises(Exception) as ex_info:
|
|
68
|
+
self.sdk.files.get(self.encrypted_chariot_filepath)
|
|
69
|
+
assert str(ex_info.value) == f'File {self.encrypted_chariot_filepath} not found.'
|
|
70
|
+
|
|
55
71
|
def teardown_class(self):
|
|
56
72
|
os.remove(self.local_filepath)
|
|
57
73
|
os.remove(self.sanitized_filepath)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pytest
|
|
2
2
|
|
|
3
3
|
from praetorian_cli.sdk.model.globals import Risk, Kind
|
|
4
|
+
from praetorian_cli.sdk.entities.risks import get_note_entries
|
|
4
5
|
from praetorian_cli.sdk.test.utils import make_test_values, clean_test_entities, setup_chariot
|
|
5
6
|
|
|
6
7
|
|
|
@@ -39,6 +40,33 @@ class TestRisk:
|
|
|
39
40
|
self.sdk.risks.update(self.risk_key, Risk.OPEN_CRITICAL.value)
|
|
40
41
|
assert self.get_risk()['status'] == Risk.OPEN_CRITICAL.value
|
|
41
42
|
|
|
43
|
+
def test_remove_comment(self):
|
|
44
|
+
self.sdk.risks.update(self.risk_key, comment='First comment')
|
|
45
|
+
self.sdk.risks.update(self.risk_key, comment='Second comment')
|
|
46
|
+
self.sdk.risks.update(self.risk_key, comment='Third comment')
|
|
47
|
+
|
|
48
|
+
risk = self.get_risk()
|
|
49
|
+
initial_history_len = len(get_note_entries(risk))
|
|
50
|
+
assert initial_history_len == 3
|
|
51
|
+
|
|
52
|
+
self.sdk.risks.update(self.risk_key, remove_comment=-2)
|
|
53
|
+
|
|
54
|
+
risk = self.get_risk()
|
|
55
|
+
new_history_len = len(get_note_entries(risk))
|
|
56
|
+
assert new_history_len == 2
|
|
57
|
+
|
|
58
|
+
self.sdk.risks.update(self.risk_key, remove_comment=-1)
|
|
59
|
+
|
|
60
|
+
risk = self.get_risk()
|
|
61
|
+
final_history_len = len(get_note_entries(risk))
|
|
62
|
+
assert final_history_len == 1
|
|
63
|
+
|
|
64
|
+
self.sdk.risks.update(self.risk_key, remove_comment=0)
|
|
65
|
+
|
|
66
|
+
risk = self.get_risk()
|
|
67
|
+
final_history_len = len(get_note_entries(risk))
|
|
68
|
+
assert final_history_len == 0
|
|
69
|
+
|
|
42
70
|
def test_delete_risk(self):
|
|
43
71
|
self.sdk.risks.delete(self.risk_key, Risk.DELETED_DUPLICATE_CRITICAL.value)
|
|
44
72
|
assert self.get_risk()['status'] == Risk.DELETED_DUPLICATE_CRITICAL.value
|
|
@@ -18,14 +18,14 @@ praetorian_cli/handlers/search.py,sha256=wPXiHrBx4NpNB8a79S9wE6TMArBjg5WsEFKzDoU
|
|
|
18
18
|
praetorian_cli/handlers/ssh_utils.py,sha256=53Kke-iFH4sJoCcweiT8q4WVRlaA7SvR5CCqdGFxHps,5903
|
|
19
19
|
praetorian_cli/handlers/test.py,sha256=uhARoRolaJf6DMRNX-1aj8SDYe1wAvhYDOBYWH39sqo,932
|
|
20
20
|
praetorian_cli/handlers/unlink.py,sha256=nUTGXZ7JBXwuHy2nzvL79sSO95Vyc0PftM6rm-9YWt8,1725
|
|
21
|
-
praetorian_cli/handlers/update.py,sha256=
|
|
21
|
+
praetorian_cli/handlers/update.py,sha256=xOThuMQ2n9Zq2-UAtRa7-9MujpoACcGUnad85-X16zA,3340
|
|
22
22
|
praetorian_cli/handlers/utils.py,sha256=PaHyDTF2uj6mMMIr1pP1Bfh22H3dMx_UWnJgXK7_yuY,2639
|
|
23
23
|
praetorian_cli/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
24
|
praetorian_cli/scripts/utils.py,sha256=lGCf4trEpsfECa9U42pDJ-f48EimlS-hG6AjnKjNt4I,501
|
|
25
25
|
praetorian_cli/scripts/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
praetorian_cli/scripts/commands/nmap-example.py,sha256=varKTkHKG4DAs9Ssf0c6SygP9GfuCG01aFxhfvixLM0,2727
|
|
27
27
|
praetorian_cli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
praetorian_cli/sdk/chariot.py,sha256=
|
|
28
|
+
praetorian_cli/sdk/chariot.py,sha256=amMgs7FxX9DY0tWNoizg7XfrVYWyLixbqqcaBQsuAKg,14626
|
|
29
29
|
praetorian_cli/sdk/keychain.py,sha256=KCz2mOTv09jgg6mrZQooRg1VrnvF9W0_BjRlEQhbzqU,7468
|
|
30
30
|
praetorian_cli/sdk/mcp_server.py,sha256=8UoTotD4UVl-tp1gqMjkOVpO__KeGCvy7mIpKXVc8Rg,8750
|
|
31
31
|
praetorian_cli/sdk/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -43,7 +43,7 @@ praetorian_cli/sdk/entities/integrations.py,sha256=NVaW_dWbnMkMIs-EYr2W7QAeamPVw
|
|
|
43
43
|
praetorian_cli/sdk/entities/jobs.py,sha256=k4QFw4qR5MdVyMAYstfnRIWr_ZH4q1PwDyUEGC-qBSM,8269
|
|
44
44
|
praetorian_cli/sdk/entities/keys.py,sha256=PgoGa3xyLMzWrIIQ8zgi7bfZiUFFumPtMDo64GjhdjE,6089
|
|
45
45
|
praetorian_cli/sdk/entities/preseeds.py,sha256=SeSY4K6diJMQzsjCBxYK3N9Lz0fUz3B_LMBOAAcBSLg,8890
|
|
46
|
-
praetorian_cli/sdk/entities/risks.py,sha256=
|
|
46
|
+
praetorian_cli/sdk/entities/risks.py,sha256=_MQlMo0y82DunwI04IjscZU2bNWyu8UM9YTazCGf_Jk,8291
|
|
47
47
|
praetorian_cli/sdk/entities/scanners.py,sha256=QCr5QlBy4jfBh8HRvZt9CoZTgNqLNnKNrI4sdfJf0jE,423
|
|
48
48
|
praetorian_cli/sdk/entities/schema.py,sha256=CPVws1CdRHyOAI7oT9A20WGOCZozTFqZnfo5ox3v0HQ,807
|
|
49
49
|
praetorian_cli/sdk/entities/search.py,sha256=9vTy9HZY2BTlqf5Zwpdl8HCRIfSEvWDRij2_-rAp2Ng,16938
|
|
@@ -68,12 +68,12 @@ praetorian_cli/sdk/test/test_configuration.py,sha256=ysyWpt7iq_tNkdvLU8gULCuwbXV
|
|
|
68
68
|
praetorian_cli/sdk/test/test_conversation.py,sha256=i1cBaRmFmtLcFLr85OtgE60DynUgWB7EqqjGU-NBWvc,6951
|
|
69
69
|
praetorian_cli/sdk/test/test_definition.py,sha256=8ShZFXJYHJUPH5rfmF3LYk9NE8W4lJBNHE2DhyJgXaY,1016
|
|
70
70
|
praetorian_cli/sdk/test/test_extend.py,sha256=bHTCwtW0jN1GvFocB_uMJcEj4_IXvCkr35yMWKESbTU,1778
|
|
71
|
-
praetorian_cli/sdk/test/test_file.py,sha256=
|
|
71
|
+
praetorian_cli/sdk/test/test_file.py,sha256=ZpFBSKbfq9kzfQdy_tmwCH8rwYxW6GB4NlhTQHEw87Y,2940
|
|
72
72
|
praetorian_cli/sdk/test/test_job.py,sha256=J7VqnVxRfvbpxNnYN9Rt3LvxmbNvSKwVqsSZxFEW1xc,1916
|
|
73
73
|
praetorian_cli/sdk/test/test_key.py,sha256=yDrR0-JLwMB3eDx-tI-ss8GGbiJaJijE4dAK8-FUuzc,1425
|
|
74
74
|
praetorian_cli/sdk/test/test_mcp.py,sha256=Ltg283j4Q12dcfCgUgDMKk6neRdDaThcndyywZl5XWs,938
|
|
75
75
|
praetorian_cli/sdk/test/test_preseed.py,sha256=VhKSbSB6OmCYnVOrI6RDsYKBs1PekEuv0KOjbTt_k14,1186
|
|
76
|
-
praetorian_cli/sdk/test/test_risk.py,sha256=
|
|
76
|
+
praetorian_cli/sdk/test/test_risk.py,sha256=WuVTPxL9yj84nJx84oXVAlz0KshLxs8GG3tDeSe3QZU,3000
|
|
77
77
|
praetorian_cli/sdk/test/test_search.py,sha256=SB9Tgo_N3CCpfvla898oLB9IZyGK9Dju1r9REK6Wmek,1996
|
|
78
78
|
praetorian_cli/sdk/test/test_seed.py,sha256=WfnEPZwMXHFtt4jyVT-1JitIW1zTrl7rwlX8jXz22vY,1303
|
|
79
79
|
praetorian_cli/sdk/test/test_setting.py,sha256=hdPQj71rjSYxa-PODG2D-kJd8C9gkAg1jQXnqYU4P6A,1326
|
|
@@ -96,9 +96,9 @@ praetorian_cli/ui/aegis/commands/set.py,sha256=WJ0Qt30fBa_hyWEaawyA3h3rSeWHFPbJS
|
|
|
96
96
|
praetorian_cli/ui/aegis/commands/ssh.py,sha256=KGsNlN0i-Cwp6gWyr-cjML9_L13oE7xFenysF2pC8Rc,3045
|
|
97
97
|
praetorian_cli/ui/conversation/__init__.py,sha256=sNhNN_ZG1Va_7OLTaoXlIFL6ageKHWdufFVYw6F_aV8,90
|
|
98
98
|
praetorian_cli/ui/conversation/textual_chat.py,sha256=gGgEs7HhNAto9rTVrGbz62mG10OqmtArI-aKxFLSfVs,24405
|
|
99
|
-
praetorian_cli-2.2.
|
|
100
|
-
praetorian_cli-2.2.
|
|
101
|
-
praetorian_cli-2.2.
|
|
102
|
-
praetorian_cli-2.2.
|
|
103
|
-
praetorian_cli-2.2.
|
|
104
|
-
praetorian_cli-2.2.
|
|
99
|
+
praetorian_cli-2.2.15.dist-info/licenses/LICENSE,sha256=Zv97QripiVALv-WokW_Elsiz9vtOfbtNt1aLZhhk67I,1067
|
|
100
|
+
praetorian_cli-2.2.15.dist-info/METADATA,sha256=tFrGUx-JASajeDC0bE5j-btCN4Lru3dM93aLyt7hrF0,7752
|
|
101
|
+
praetorian_cli-2.2.15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
102
|
+
praetorian_cli-2.2.15.dist-info/entry_points.txt,sha256=uJbDvZdkYaLiCh2DMvXPUGKFm2p5ZfzJCizUK3-PUEE,56
|
|
103
|
+
praetorian_cli-2.2.15.dist-info/top_level.txt,sha256=QbUdRPGEj_TyHO-E7AD5BxFfR8ore37i273jP4Gn43c,15
|
|
104
|
+
praetorian_cli-2.2.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|