11import click
22import py42 .sdk .queries .alerts .filters as f
33from c42eventextractor .extractors import AlertExtractor
4+ from py42 .exceptions import Py42NotFoundError
45from py42 .sdk .queries .alerts .filters import AlertState
56from py42 .sdk .queries .alerts .filters import RuleType
67from py42 .sdk .queries .alerts .filters import Severity
8+ from py42 .util import format_dict
79
8- import code42cli .click_ext .groups
910import code42cli .cmds .search .extraction as ext
1011import code42cli .cmds .search .options as searchopt
1112import code42cli .errors as errors
1213import 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
1317from code42cli .cmds .search import SendToCommand
1418from code42cli .cmds .search .cursor_store import AlertCursorStore
1519from code42cli .cmds .search .extraction import handle_no_events
1620from code42cli .cmds .search .options import server_options
1721from code42cli .date_helper import convert_datetime_to_timestamp
1822from code42cli .date_helper import limit_date_range
23+ from code42cli .file_readers import read_csv_arg
1924from code42cli .options import format_option
2025from code42cli .output_formats import JsonOutputFormat
26+ from code42cli .output_formats import OutputFormat
2127from code42cli .output_formats import OutputFormatter
2228
2329
3945 callback = searchopt .is_in_filter (f .Severity ),
4046 help = "Filter alerts by severity. Defaults to returning all severities." ,
4147)
42- state_option = click .option (
48+ filter_state_option = click .option (
4349 "--state" ,
4450 multiple = True ,
4551 type = click .Choice (AlertState .choices ()),
134140 help = "The output format of the result. Defaults to json format." ,
135141 default = JsonOutputFormat .RAW ,
136142)
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+ )
137150
138151
139- def _get_search_default_header ():
152+ def _get_default_output_header ():
140153 return {
154+ "id" : "Id" ,
141155 "name" : "RuleName" ,
142156 "actor" : "Username" ,
143157 "createdAt" : "ObservedDate" ,
144- "state" : "Status " ,
158+ "state" : "State " ,
145159 "severity" : "Severity" ,
146160 "description" : "Description" ,
147161 }
@@ -155,7 +169,7 @@ def search_options(f):
155169 return f
156170
157171
158- def alert_options (f ):
172+ def filter_options (f ):
159173 f = actor_option (f )
160174 f = actor_contains_option (f )
161175 f = exclude_actor_option (f )
@@ -168,11 +182,11 @@ def alert_options(f):
168182 f = exclude_rule_type_option (f )
169183 f = description_option (f )
170184 f = severity_option (f )
171- f = state_option (f )
185+ f = filter_state_option (f )
172186 return f
173187
174188
175- @click .group (cls = code42cli . click_ext . groups . OrderedGroup )
189+ @click .group (cls = OrderedGroup )
176190@opt .sdk_options (hidden = True )
177191def alerts (state ):
178192 """Get and send alert data."""
@@ -203,7 +217,7 @@ def _call_extractor(
203217
204218
205219@alerts .command ()
206- @alert_options
220+ @filter_options
207221@search_options
208222@click .option (
209223 "--or-query" , is_flag = True , cls = searchopt .AdvancedQueryAndSavedSearchIncompatible
@@ -225,11 +239,11 @@ def search(
225239 use_checkpoint ,
226240 or_query ,
227241 include_all ,
228- ** kwargs
242+ ** kwargs ,
229243):
230244 """Search for alerts."""
231245 output_header = ext .try_get_default_header (
232- include_all , _get_search_default_header (), format
246+ include_all , _get_default_output_header (), format
233247 )
234248 formatter = OutputFormatter (format , output_header )
235249 cursor = _get_alert_cursor_store (cli_state .profile .name ) if use_checkpoint else None
@@ -246,7 +260,7 @@ def search(
246260
247261
248262@alerts .command (cls = SendToCommand )
249- @alert_options
263+ @filter_options
250264@search_options
251265@click .option (
252266 "--or-query" , is_flag = True , cls = searchopt .AdvancedQueryAndSavedSearchIncompatible
@@ -283,3 +297,87 @@ def _get_alert_extractor(sdk, handlers):
283297
284298def _get_alert_cursor_store (profile_name ):
285299 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