1
1
import click
2
2
import py42 .sdk .queries .alerts .filters as f
3
3
from c42eventextractor .extractors import AlertExtractor
4
+ from py42 .exceptions import Py42NotFoundError
4
5
from py42 .sdk .queries .alerts .filters import AlertState
5
6
from py42 .sdk .queries .alerts .filters import RuleType
6
7
from py42 .sdk .queries .alerts .filters import Severity
8
+ from py42 .util import format_dict
7
9
8
- import code42cli .click_ext .groups
9
10
import code42cli .cmds .search .extraction as ext
10
11
import code42cli .cmds .search .options as searchopt
11
12
import code42cli .errors as errors
12
13
import code42cli .options as opt
14
+ from code42cli .bulk import generate_template_cmd_factory
15
+ from code42cli .bulk import run_bulk_process
16
+ from code42cli .click_ext .groups import OrderedGroup
13
17
from code42cli .cmds .search import SendToCommand
14
18
from code42cli .cmds .search .cursor_store import AlertCursorStore
15
19
from code42cli .cmds .search .extraction import handle_no_events
16
20
from code42cli .cmds .search .options import server_options
17
21
from code42cli .date_helper import convert_datetime_to_timestamp
18
22
from code42cli .date_helper import limit_date_range
23
+ from code42cli .file_readers import read_csv_arg
19
24
from code42cli .options import format_option
20
25
from code42cli .output_formats import JsonOutputFormat
26
+ from code42cli .output_formats import OutputFormat
21
27
from code42cli .output_formats import OutputFormatter
22
28
23
29
39
45
callback = searchopt .is_in_filter (f .Severity ),
40
46
help = "Filter alerts by severity. Defaults to returning all severities." ,
41
47
)
42
- state_option = click .option (
48
+ filter_state_option = click .option (
43
49
"--state" ,
44
50
multiple = True ,
45
51
type = click .Choice (AlertState .choices ()),
134
140
help = "The output format of the result. Defaults to json format." ,
135
141
default = JsonOutputFormat .RAW ,
136
142
)
143
+ alert_id_arg = click .argument ("alert-id" )
144
+ note_option = click .option ("--note" , help = "A note to attach to the alert." )
145
+ update_state_option = click .option (
146
+ "--state" ,
147
+ help = "The state to give to the alert." ,
148
+ type = click .Choice (AlertState .choices ()),
149
+ )
137
150
138
151
139
- def _get_search_default_header ():
152
+ def _get_default_output_header ():
140
153
return {
154
+ "id" : "Id" ,
141
155
"name" : "RuleName" ,
142
156
"actor" : "Username" ,
143
157
"createdAt" : "ObservedDate" ,
144
- "state" : "Status " ,
158
+ "state" : "State " ,
145
159
"severity" : "Severity" ,
146
160
"description" : "Description" ,
147
161
}
@@ -155,7 +169,7 @@ def search_options(f):
155
169
return f
156
170
157
171
158
- def alert_options (f ):
172
+ def filter_options (f ):
159
173
f = actor_option (f )
160
174
f = actor_contains_option (f )
161
175
f = exclude_actor_option (f )
@@ -168,11 +182,11 @@ def alert_options(f):
168
182
f = exclude_rule_type_option (f )
169
183
f = description_option (f )
170
184
f = severity_option (f )
171
- f = state_option (f )
185
+ f = filter_state_option (f )
172
186
return f
173
187
174
188
175
- @click .group (cls = code42cli . click_ext . groups . OrderedGroup )
189
+ @click .group (cls = OrderedGroup )
176
190
@opt .sdk_options (hidden = True )
177
191
def alerts (state ):
178
192
"""Get and send alert data."""
@@ -203,7 +217,7 @@ def _call_extractor(
203
217
204
218
205
219
@alerts .command ()
206
- @alert_options
220
+ @filter_options
207
221
@search_options
208
222
@click .option (
209
223
"--or-query" , is_flag = True , cls = searchopt .AdvancedQueryAndSavedSearchIncompatible
@@ -225,11 +239,11 @@ def search(
225
239
use_checkpoint ,
226
240
or_query ,
227
241
include_all ,
228
- ** kwargs
242
+ ** kwargs ,
229
243
):
230
244
"""Search for alerts."""
231
245
output_header = ext .try_get_default_header (
232
- include_all , _get_search_default_header (), format
246
+ include_all , _get_default_output_header (), format
233
247
)
234
248
formatter = OutputFormatter (format , output_header )
235
249
cursor = _get_alert_cursor_store (cli_state .profile .name ) if use_checkpoint else None
@@ -246,7 +260,7 @@ def search(
246
260
247
261
248
262
@alerts .command (cls = SendToCommand )
249
- @alert_options
263
+ @filter_options
250
264
@search_options
251
265
@click .option (
252
266
"--or-query" , is_flag = True , cls = searchopt .AdvancedQueryAndSavedSearchIncompatible
@@ -283,3 +297,87 @@ def _get_alert_extractor(sdk, handlers):
283
297
284
298
def _get_alert_cursor_store (profile_name ):
285
299
return AlertCursorStore (profile_name )
300
+
301
+
302
+ @alerts .command ()
303
+ @opt .sdk_options ()
304
+ @alert_id_arg
305
+ @click .option (
306
+ "--include-observations" , is_flag = True , help = "View observations of the alert."
307
+ )
308
+ def show (state , alert_id , include_observations ):
309
+ """Display the details of a single alert."""
310
+ formatter = OutputFormatter (OutputFormat .TABLE , _get_default_output_header ())
311
+
312
+ try :
313
+ response = state .sdk .alerts .get_details (alert_id )
314
+ except Py42NotFoundError :
315
+ raise errors .Code42CLIError (f"No alert found with ID '{ alert_id } '." )
316
+
317
+ alert = response ["alerts" ][0 ]
318
+ formatter .echo_formatted_list ([alert ])
319
+
320
+ # Show note details
321
+ note = alert .get ("note" )
322
+ if note :
323
+ click .echo ("\n Note:\n " )
324
+ click .echo (format_dict (note ))
325
+
326
+ if include_observations :
327
+ observations = alert .get ("observations" )
328
+ if observations :
329
+ click .echo ("\n Observations:\n " )
330
+ click .echo (format_dict (observations ))
331
+ else :
332
+ click .echo ("\n No observations found." )
333
+
334
+
335
+ @alerts .command ()
336
+ @opt .sdk_options ()
337
+ @alert_id_arg
338
+ @update_state_option
339
+ @note_option
340
+ def update (cli_state , alert_id , state , note ):
341
+ """Update alert information."""
342
+ _update_alert (cli_state .sdk , alert_id , state , note )
343
+
344
+
345
+ @alerts .group (cls = OrderedGroup )
346
+ @opt .sdk_options (hidden = True )
347
+ def bulk (state ):
348
+ """Tools for executing bulk alert actions."""
349
+ pass
350
+
351
+
352
+ UPDATE_ALERT_CSV_HEADERS = ["id" , "state" , "note" ]
353
+ update_alerts_generate_template = generate_template_cmd_factory (
354
+ group_name = ALERTS_KEYWORD ,
355
+ commands_dict = {"update" : UPDATE_ALERT_CSV_HEADERS },
356
+ help_message = "Generate the CSV template needed for bulk alert commands." ,
357
+ )
358
+ bulk .add_command (update_alerts_generate_template )
359
+
360
+
361
+ @bulk .command (
362
+ name = "update" ,
363
+ help = f"Bulk update alerts using a CSV file with format: { ',' .join (UPDATE_ALERT_CSV_HEADERS )} " ,
364
+ )
365
+ @opt .sdk_options ()
366
+ @read_csv_arg (headers = UPDATE_ALERT_CSV_HEADERS )
367
+ def bulk_update (cli_state , csv_rows ):
368
+ """Bulk update alerts."""
369
+ sdk = cli_state .sdk
370
+
371
+ def handle_row (id , state , note ):
372
+ _update_alert (sdk , id , state , note )
373
+
374
+ run_bulk_process (
375
+ handle_row , csv_rows , progress_label = "Updating alerts:" ,
376
+ )
377
+
378
+
379
+ def _update_alert (sdk , alert_id , alert_state , note ):
380
+ if alert_state :
381
+ sdk .alerts .update_state (alert_state , [alert_id ], note = note )
382
+ elif note :
383
+ sdk .alerts .update_note (alert_id , note )
0 commit comments