Skip to content

Commit 745c940

Browse files
author
Juliya Smith
authored
Feature/bulk alert update (#275)
* wip * Add commands * CL * Adjust help message for bulk gen templ * Make help text generic * conform help gen messag * show command inc note * bump py42 * optimize * WIP * test show * Test update state * Finish testing * Correct CL * add to cl * Style * optionally include observations * rm dup line * drop s
1 parent 1931d8c commit 745c940

File tree

4 files changed

+384
-13
lines changed

4 files changed

+384
-13
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ how a consumer would use the library (e.g. adding unit tests, updating documenta
1212

1313
### Added
1414

15+
- New command `code42 alerts show` that displays information about a single alert.
16+
17+
- New command `code42 alerts update` that can update an alert's state or note.
18+
19+
- New command `code42 alerts bulk generate-tempate` for generating CSV templates for bulk
20+
commands.
21+
22+
- New command `code42 alerts bulk update` for bulk updating alerts.
23+
24+
### Changed
25+
26+
- `code42 alerts search` now includes the alert ID in its table output.
27+
28+
- `code42 alerts search` table output now refers to the alert state as `state` instead of
29+
`status`.
30+
1531
- `code42 cases file-events bulk` with sub-commands:
1632
- `generate-template`: that creates the file template. And parameters:
1733
- `cmd`: with options `add` and `remove`.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"keyring==18.0.1",
3939
"keyrings.alt==3.2.0",
4040
"pandas>=1.1.3",
41-
"py42>=1.11.1",
41+
"py42>=1.14.1",
4242
],
4343
extras_require={
4444
"dev": [

src/code42cli/cmds/alerts.py

Lines changed: 109 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
import click
22
import py42.sdk.queries.alerts.filters as f
33
from c42eventextractor.extractors import AlertExtractor
4+
from py42.exceptions import Py42NotFoundError
45
from py42.sdk.queries.alerts.filters import AlertState
56
from py42.sdk.queries.alerts.filters import RuleType
67
from py42.sdk.queries.alerts.filters import Severity
8+
from py42.util import format_dict
79

8-
import code42cli.click_ext.groups
910
import code42cli.cmds.search.extraction as ext
1011
import code42cli.cmds.search.options as searchopt
1112
import code42cli.errors as errors
1213
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
1317
from code42cli.cmds.search import SendToCommand
1418
from code42cli.cmds.search.cursor_store import AlertCursorStore
1519
from code42cli.cmds.search.extraction import handle_no_events
1620
from code42cli.cmds.search.options import server_options
1721
from code42cli.date_helper import convert_datetime_to_timestamp
1822
from code42cli.date_helper import limit_date_range
23+
from code42cli.file_readers import read_csv_arg
1924
from code42cli.options import format_option
2025
from code42cli.output_formats import JsonOutputFormat
26+
from code42cli.output_formats import OutputFormat
2127
from code42cli.output_formats import OutputFormatter
2228

2329

@@ -39,7 +45,7 @@
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()),
@@ -134,14 +140,22 @@
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)
177191
def 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

284298
def _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("\nNote:\n")
324+
click.echo(format_dict(note))
325+
326+
if include_observations:
327+
observations = alert.get("observations")
328+
if observations:
329+
click.echo("\nObservations:\n")
330+
click.echo(format_dict(observations))
331+
else:
332+
click.echo("\nNo 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

Comments
 (0)