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.
@@ -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
- def risk(chariot, key, status, comment):
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
- chariot.risks.update(key, status, comment)
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()
@@ -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 method to make HTTP requests to the Chariot API with all global headers/parameters set.
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
- # It is a two-step upload. The PUT request to the /file endpoint is to get a presigned URL for S3.
210
- # There is no data transfer.
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 = self.request('GET', url)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: praetorian-cli
3
- Version: 2.2.13
3
+ Version: 2.2.15
4
4
  Summary: For interacting with the Chariot API
5
5
  Home-page: https://github.com/praetorian-inc/praetorian-cli
6
6
  Author: Praetorian
@@ -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=rgdOsFTaEivTdTUjUxPNEo13XR7uoIpBFRUqUdFVjaI,2846
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=owN3nRg5Oh-p88PGOQ7DpHflkcMxBsE1xS1aAU_lOvA,13926
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=7WcAGiehoGuLlugIujxQC2FcA1ndOh7s_ooEGERpWM4,6630
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=rRikM2ceMy5TEX7YOFMPB2dnCRqROE9w8qoqVeCs9HM,2133
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=S428ZF9Sot6_F8vvXFL-1a5FxtmsFct4rBewTAgm0mE,1992
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.13.dist-info/licenses/LICENSE,sha256=Zv97QripiVALv-WokW_Elsiz9vtOfbtNt1aLZhhk67I,1067
100
- praetorian_cli-2.2.13.dist-info/METADATA,sha256=MUJwo3wrkUulp376p98YNPnFC_tmNaMvSwGWbANr5xI,7752
101
- praetorian_cli-2.2.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
102
- praetorian_cli-2.2.13.dist-info/entry_points.txt,sha256=uJbDvZdkYaLiCh2DMvXPUGKFm2p5ZfzJCizUK3-PUEE,56
103
- praetorian_cli-2.2.13.dist-info/top_level.txt,sha256=QbUdRPGEj_TyHO-E7AD5BxFfR8ore37i273jP4Gn43c,15
104
- praetorian_cli-2.2.13.dist-info/RECORD,,
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,,