1
1
import click
2
2
import py42 .sdk .queries .alerts .filters as f
3
- from c42eventextractor .extractors import AlertExtractor
4
3
from py42 .exceptions import Py42NotFoundError
4
+ from py42 .sdk .queries .alerts .alert_query import AlertQuery
5
5
from py42 .sdk .queries .alerts .filters import AlertState
6
6
from py42 .sdk .queries .alerts .filters import RuleType
7
7
from py42 .sdk .queries .alerts .filters import Severity
8
8
from py42 .util import format_dict
9
9
10
- import code42cli .cmds .search .extraction as ext
11
10
import code42cli .cmds .search .options as searchopt
12
11
import code42cli .errors as errors
13
12
import code42cli .options as opt
16
15
from code42cli .click_ext .groups import OrderedGroup
17
16
from code42cli .cmds .search import SendToCommand
18
17
from code42cli .cmds .search .cursor_store import AlertCursorStore
19
- from code42cli .cmds .search .extraction import handle_no_events
20
18
from code42cli .cmds .search .options import server_options
19
+ from code42cli .cmds .util import convert_to_or_query
20
+ from code42cli .cmds .util import create_time_range_filter
21
+ from code42cli .cmds .util import try_get_default_header
21
22
from code42cli .date_helper import convert_datetime_to_timestamp
22
23
from code42cli .date_helper import limit_date_range
23
24
from code42cli .file_readers import read_csv_arg
24
25
from code42cli .options import format_option
25
26
from code42cli .output_formats import JsonOutputFormat
26
27
from code42cli .output_formats import OutputFormat
27
28
from code42cli .output_formats import OutputFormatter
28
-
29
+ from code42cli .util import hash_event
30
+ from code42cli .util import parse_timestamp
31
+ from code42cli .util import warn_interrupt
29
32
30
33
ALERTS_KEYWORD = "alerts"
34
+ ALERT_PAGE_SIZE = 25
35
+
31
36
begin = opt .begin_option (
32
37
ALERTS_KEYWORD ,
33
38
callback = lambda ctx , param , arg : convert_datetime_to_timestamp (
@@ -202,20 +207,6 @@ def clear_checkpoint(state, checkpoint_name):
202
207
_get_alert_cursor_store (state .profile .name ).delete (checkpoint_name )
203
208
204
209
205
- def _call_extractor (
206
- cli_state , handlers , begin , end , or_query , advanced_query , ** kwargs
207
- ):
208
- extractor = _get_alert_extractor (cli_state .sdk , handlers )
209
- extractor .use_or_query = or_query
210
- if advanced_query :
211
- cli_state .search_filters = advanced_query
212
- if begin or end :
213
- cli_state .search_filters .append (
214
- ext .create_time_range_filter (f .DateObserved , begin , end )
215
- )
216
- extractor .extract (* cli_state .search_filters )
217
-
218
-
219
210
@alerts .command ()
220
211
@filter_options
221
212
@search_options
@@ -242,21 +233,78 @@ def search(
242
233
** kwargs ,
243
234
):
244
235
"""Search for alerts."""
245
- output_header = ext . try_get_default_header (
236
+ output_header = try_get_default_header (
246
237
include_all , _get_default_output_header (), format
247
238
)
248
239
formatter = OutputFormatter (format , output_header )
249
240
cursor = _get_alert_cursor_store (cli_state .profile .name ) if use_checkpoint else None
250
- handlers = ext .create_handlers (
251
- cli_state .sdk ,
252
- AlertExtractor ,
253
- cursor ,
254
- use_checkpoint ,
255
- formatter = formatter ,
256
- force_pager = include_all ,
257
- )
258
- _call_extractor (cli_state , handlers , begin , end , or_query , advanced_query , ** kwargs )
259
- handle_no_events (not handlers .TOTAL_EVENTS and not errors .ERRORED )
241
+ if use_checkpoint :
242
+ checkpoint_name = use_checkpoint
243
+ checkpoint = cursor .get (checkpoint_name )
244
+ if checkpoint is not None :
245
+ begin = checkpoint
246
+
247
+ query = _construct_query (cli_state , begin , end , advanced_query , or_query )
248
+ alerts_gen = cli_state .sdk .alerts .get_all_alert_details (query )
249
+
250
+ if use_checkpoint :
251
+ checkpoint_name = use_checkpoint
252
+ # update checkpoint to alertId of last event retrieved
253
+ alerts_gen = _dedupe_checkpointed_events_and_store_updated_checkpoint (
254
+ cursor , checkpoint_name , alerts_gen
255
+ )
256
+ alerts_list = []
257
+ for alert in alerts_gen :
258
+ alerts_list .append (alert )
259
+ if not alerts_list :
260
+ click .echo ("No results found." )
261
+ return
262
+ formatter .echo_formatted_list (alerts_list )
263
+
264
+
265
+ def _construct_query (state , begin , end , advanced_query , or_query ):
266
+
267
+ if advanced_query :
268
+ state .search_filters = advanced_query
269
+ else :
270
+ if begin or end :
271
+ state .search_filters .append (
272
+ create_time_range_filter (f .DateObserved , begin , end )
273
+ )
274
+ if or_query :
275
+ state .search_filters = convert_to_or_query (state .search_filters )
276
+ query = AlertQuery (* state .search_filters )
277
+ query .page_size = ALERT_PAGE_SIZE
278
+ query .sort_direction = "asc"
279
+ query .sort_key = "CreatedAt"
280
+ return query
281
+
282
+
283
+ def _dedupe_checkpointed_events_and_store_updated_checkpoint (
284
+ cursor , checkpoint_name , alerts_gen
285
+ ):
286
+ """De-duplicates events across checkpointed runs. Since using the timestamp of the last event
287
+ processed as the `--begin` time of the next run causes the last event to show up again in the
288
+ next results, we hash the last event(s) of each run and store those hashes in the cursor to
289
+ filter out on the next run. It's also possible that two events have the exact same timestamp, so
290
+ `checkpoint_events` needs to be a list of hashes so we can filter out everything that's actually
291
+ been processed.
292
+ """
293
+
294
+ checkpoint_alerts = cursor .get_alerts (checkpoint_name )
295
+ new_timestamp = None
296
+ new_alerts = []
297
+ for alert in alerts_gen :
298
+ event_hash = hash_event (alert )
299
+ if event_hash not in checkpoint_alerts :
300
+ if alert [f .DateObserved ._term ] != new_timestamp :
301
+ new_timestamp = alert [f .DateObserved ._term ]
302
+ new_alerts .clear ()
303
+ new_alerts .append (event_hash )
304
+ yield alert
305
+ ts = parse_timestamp (new_timestamp )
306
+ cursor .replace (checkpoint_name , ts )
307
+ cursor .replace_alerts (checkpoint_name , new_alerts )
260
308
261
309
262
310
@alerts .command (cls = SendToCommand )
@@ -280,19 +328,31 @@ def send_to(cli_state, begin, end, advanced_query, use_checkpoint, or_query, **k
280
328
HOSTNAME format: address:port where port is optional and defaults to 514.
281
329
"""
282
330
cursor = _get_cursor (cli_state , use_checkpoint )
283
- handlers = ext .create_send_to_handlers (
284
- cli_state .sdk , AlertExtractor , cursor , use_checkpoint , cli_state .logger ,
285
- )
286
- _call_extractor (cli_state , handlers , begin , end , or_query , advanced_query , ** kwargs )
287
- handle_no_events (not handlers .TOTAL_EVENTS and not errors .ERRORED )
288
331
332
+ if use_checkpoint :
333
+ checkpoint_name = use_checkpoint
334
+ checkpoint = cursor .get (checkpoint_name )
335
+ if checkpoint is not None :
336
+ begin = checkpoint
289
337
290
- def _get_cursor (state , use_checkpoint ):
291
- return _get_alert_cursor_store (state .profile .name ) if use_checkpoint else None
338
+ query = _construct_query (cli_state , begin , end , advanced_query , or_query )
339
+ alerts_gen = cli_state .sdk .alerts .get_all_alert_details (query )
340
+
341
+ if use_checkpoint :
342
+ checkpoint_name = use_checkpoint
343
+ alerts_gen = _dedupe_checkpointed_events_and_store_updated_checkpoint (
344
+ cursor , checkpoint_name , alerts_gen
345
+ )
346
+ with warn_interrupt ():
347
+ alert = None
348
+ for alert in alerts_gen :
349
+ cli_state .logger .info (alert )
350
+ if alert is None : # generator was empty
351
+ click .echo ("No results found." )
292
352
293
353
294
- def _get_alert_extractor ( sdk , handlers ):
295
- return AlertExtractor ( sdk , handlers )
354
+ def _get_cursor ( state , use_checkpoint ):
355
+ return _get_alert_cursor_store ( state . profile . name ) if use_checkpoint else None
296
356
297
357
298
358
def _get_alert_cursor_store (profile_name ):
0 commit comments