Skip to content

Commit 5af4ebd

Browse files
Armis collector enhancement (demisto#30491)
* initial commit * README changes * Added support for devices last run time, devices max fetch * Version bump and RN * Change unit test for new changes. Parse event type correctly. Make changes to comply with dict events instead of list events. * Changes to events from list to dict. * Remove redundant after AQL date filter * Alerts and Activity default max = 5k Devices default max = 10k * Remove redundant description in yml. * Updated the README * fix flake8 long line issues * Change param name to snake_case. No need to demisto.setLastRun multiple times. * Apply suggestions from code review Tech doc review changes. Co-authored-by: ShirleyDenkberg <[email protected]> * update RN. --------- Co-authored-by: ShirleyDenkberg <[email protected]>
1 parent b540767 commit 5af4ebd

File tree

6 files changed

+195
-82
lines changed

6 files changed

+195
-82
lines changed

Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector.py

+83-32
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class EVENT_TYPE(NamedTuple):
1515
unique_id_key: str
1616
aql_query: str
1717
type: str
18+
order_by: str
1819

1920

2021
''' CONSTANTS '''
@@ -24,11 +25,14 @@ class EVENT_TYPE(NamedTuple):
2425
VENDOR = 'armis'
2526
PRODUCT = 'security'
2627
API_V1_ENDPOINT = '/api/v1'
27-
DEFAULT_MAX_FETCH = 1000
28+
DEFAULT_MAX_FETCH = 5000
29+
DEVICES_DEFAULT_MAX_FETCH = 10000
2830
EVENT_TYPES = {
29-
'Alerts': EVENT_TYPE('alertId', 'in:alerts', 'alerts'),
30-
'Threat activities': EVENT_TYPE('activityUUID', 'in:activity type:"Threat Detected"', 'threat_activities'),
31+
'Alerts': EVENT_TYPE('alertId', 'in:alerts', 'alerts', 'time'),
32+
'Activities': EVENT_TYPE('activityUUID', 'in:activity', 'activity', 'time'),
33+
'Devices': EVENT_TYPE('id', 'in:devices', 'devices', 'firstSeen'),
3134
}
35+
DEVICES_LAST_FETCH = 'devices_last_fetch_time'
3236

3337
''' CLIENT CLASS '''
3438

@@ -53,7 +57,8 @@ def update_access_token(self, access_token=None):
5357
self._headers = headers
5458
self._access_token = access_token
5559

56-
def fetch_by_aql_query(self, aql_query: str, max_fetch: int, after: None | datetime = None):
60+
def fetch_by_aql_query(self, aql_query: str, max_fetch: int, after: None | datetime = None,
61+
order_by: str = 'time'):
5762
""" Fetches events using AQL query.
5863
5964
Args:
@@ -64,7 +69,7 @@ def fetch_by_aql_query(self, aql_query: str, max_fetch: int, after: None | datet
6469
Returns:
6570
list[dict]: List of events objects represented as dictionaries.
6671
"""
67-
params: dict[str, Any] = {'aql': aql_query, 'includeTotal': 'true', 'length': max_fetch, 'orderBy': 'time'}
72+
params: dict[str, Any] = {'aql': aql_query, 'includeTotal': 'true', 'length': max_fetch, 'orderBy': order_by}
6873
if not after: # this should only happen when get-events command is used without from_date argument
6974
after = datetime.now()
7075
params['aql'] += f' after:{after.strftime(DATE_FORMAT)}' # add 'after' date filter to AQL query in the desired format
@@ -264,7 +269,7 @@ def dedup_events(events: list[dict], events_last_fetch_ids: list[str], unique_id
264269
return new_events, new_ids
265270

266271

267-
def fetch_by_event_type(event_type: EVENT_TYPE, events: list, next_run: dict, client: Client,
272+
def fetch_by_event_type(event_type: EVENT_TYPE, events: dict, next_run: dict, client: Client,
268273
max_fetch: int, last_run: dict, fetch_start_time: datetime | None):
269274
""" Fetch events by specific event type.
270275
@@ -286,23 +291,28 @@ def fetch_by_event_type(event_type: EVENT_TYPE, events: list, next_run: dict, cl
286291
response = client.fetch_by_aql_query(
287292
aql_query=event_type.aql_query,
288293
max_fetch=max_fetch,
289-
after=event_type_fetch_start_time
294+
after=event_type_fetch_start_time,
295+
order_by=event_type.order_by
290296
)
291297
demisto.debug(f'debug-log: fetched {len(response)} {event_type.type} from API')
292298
if response:
293299
new_events, next_run[last_fetch_ids] = dedup_events(
294300
response, last_run.get(last_fetch_ids, []), event_type.unique_id_key)
295301
next_run[last_fetch_time] = new_events[-1].get('time') if new_events else last_run.get(last_fetch_time)
296-
297-
events.extend(new_events)
302+
events.setdefault(event_type.type, []).extend(new_events)
298303
demisto.debug(f'debug-log: overall {len(new_events)} {event_type.type} (after dedup)')
299304
demisto.debug(f'debug-log: last {event_type.type} in list: {new_events[-1] if new_events else {}}')
300305
else:
301306
next_run.update(last_run)
302307

303308

304-
def fetch_events(client: Client, max_fetch: int, last_run: dict, fetch_start_time: datetime | None,
305-
event_types_to_fetch: list[str]):
309+
def fetch_events(client: Client,
310+
max_fetch: int,
311+
devices_max_fetch: int,
312+
last_run: dict,
313+
fetch_start_time: datetime | None,
314+
event_types_to_fetch: list[str],
315+
device_fetch_interval: timedelta | None):
306316
""" Fetch events from Armis API.
307317
308318
Args:
@@ -315,17 +325,22 @@ def fetch_events(client: Client, max_fetch: int, last_run: dict, fetch_start_tim
315325
Returns:
316326
(list[dict], dict) : List of fetched events and next run dictionary.
317327
"""
318-
events: list[dict] = []
328+
events: dict[str, list[dict]] = {}
319329
next_run: dict[str, list | str] = {}
330+
if 'Devices' in event_types_to_fetch\
331+
and not should_run_device_fetch(last_run, device_fetch_interval, datetime.now()):
332+
event_types_to_fetch.remove('Devices')
320333

321334
for event_type in event_types_to_fetch:
335+
event_max_fetch = max_fetch if event_type != "Devices" else devices_max_fetch
322336
try:
323-
fetch_by_event_type(EVENT_TYPES[event_type], events, next_run, client, max_fetch, last_run, fetch_start_time)
337+
fetch_by_event_type(EVENT_TYPES[event_type], events, next_run, client,
338+
event_max_fetch, last_run, fetch_start_time)
324339
except Exception as e:
325340
if "Invalid access token" in str(e):
326341
client.update_access_token()
327342
fetch_by_event_type(EVENT_TYPES[event_type], events, next_run, client,
328-
max_fetch, last_run, fetch_start_time)
343+
event_max_fetch, last_run, fetch_start_time)
329344

330345
next_run['access_token'] = client._access_token
331346

@@ -361,7 +376,8 @@ def handle_from_date_argument(from_date: str) -> datetime | None:
361376
return from_date_datetime if from_date_datetime else None
362377

363378

364-
def handle_fetched_events(events: list[dict[str, Any]], next_run: dict[str, str | list]):
379+
def handle_fetched_events(events: dict[str, list[dict[str, Any]]],
380+
next_run: dict[str, str | list]):
365381
""" Handle fetched events.
366382
- Send the fetched events to XSIAM.
367383
- Set last run values for next fetch cycle.
@@ -371,35 +387,41 @@ def handle_fetched_events(events: list[dict[str, Any]], next_run: dict[str, str
371387
next_run (dict[str, str | list]): Next run dictionary.
372388
"""
373389
if events:
374-
add_time_to_events(events)
375-
demisto.debug(f'debug-log: {len(events)} events are about to be sent to XSIAM.')
376-
send_events_to_xsiam(
377-
events,
378-
vendor=VENDOR,
379-
product=PRODUCT
380-
)
390+
for event_type, events_list in events.items():
391+
add_time_to_events(events_list)
392+
demisto.debug(f'debug-log: {len(events_list)} events are about to be sent to XSIAM.')
393+
product = f'{PRODUCT}_{event_type}' if event_type != 'alerts' else PRODUCT
394+
send_events_to_xsiam(
395+
events_list,
396+
vendor=VENDOR,
397+
product=product
398+
)
399+
demisto.debug(f'debug-log: {len(events)} events were sent to XSIAM.')
381400
demisto.setLastRun(next_run)
382-
demisto.debug(f'debug-log: {len(events)} events were sent to XSIAM.')
383401
else:
384402
demisto.debug('debug-log: No new events fetched.')
385403

386404
demisto.debug(f'debug-log: {next_run=}')
387405

388406

389-
def events_to_command_results(events: list[dict[str, Any]]) -> CommandResults:
407+
def events_to_command_results(events: dict[str, list[dict[str, Any]]]) -> list:
390408
""" Return a CommandResults object with a table of fetched events.
391409
392410
Args:
393411
events (list[dict[str, Any]]): list of fetched events.
394412
395413
Returns:
396-
CommandResults: CommandResults object with a table of fetched events.
414+
command_results_list: a List of CommandResults objects containing tables of fetched events.
397415
"""
398-
return CommandResults(
399-
raw_response=events,
400-
readable_output=tableToMarkdown(name=f'{VENDOR} {PRODUCT} events',
401-
t=events,
402-
removeNull=True))
416+
command_results_list: list = []
417+
for key, value in events.items():
418+
product = f'{PRODUCT}_{key}' if key != 'alerts' else PRODUCT
419+
command_results_list.append(CommandResults(
420+
raw_response=events,
421+
readable_output=tableToMarkdown(name=f'{VENDOR} {product} events',
422+
t=value,
423+
removeNull=True)))
424+
return command_results_list
403425

404426

405427
def set_last_run_with_current_time(last_run: dict, event_types_to_fetch) -> None:
@@ -417,6 +439,29 @@ def set_last_run_with_current_time(last_run: dict, event_types_to_fetch) -> None
417439
last_run[last_fetch_time] = now_str
418440

419441

442+
def should_run_device_fetch(last_run,
443+
device_fetch_interval: timedelta | None,
444+
datetime_now: datetime):
445+
"""
446+
Args:
447+
last_run: last run object.
448+
device_fetch_interval: device fetch interval.
449+
datetime_now: time now
450+
451+
Returns: True if fetch device interval time has passed since last time that fetch run.
452+
453+
"""
454+
if not device_fetch_interval:
455+
return False
456+
if last_fetch_time := last_run.get(DEVICES_LAST_FETCH):
457+
last_check_time = datetime.strptime(last_fetch_time, DATE_FORMAT)
458+
else:
459+
# first time device fetch
460+
return True
461+
demisto.debug(f'Should run device fetch? {last_check_time=}, {device_fetch_interval=}')
462+
return datetime_now - last_check_time > device_fetch_interval
463+
464+
420465
''' MAIN FUNCTION '''
421466

422467

@@ -430,11 +475,15 @@ def main(): # pragma: no cover
430475
base_url = urljoin(params.get('server_url'), API_V1_ENDPOINT)
431476
verify_certificate = not params.get('insecure', True)
432477
max_fetch = arg_to_number(params.get('max_fetch')) or DEFAULT_MAX_FETCH
478+
devices_max_fetch = arg_to_number(params.get('devices_max_fetch')) or DEVICES_DEFAULT_MAX_FETCH
433479
proxy = params.get('proxy', False)
434480
event_types_to_fetch = argToList(params.get('event_types_to_fetch', []))
481+
event_types_to_fetch = [event_type.strip(' ') for event_type in event_types_to_fetch]
435482
should_push_events = argToBoolean(args.get('should_push_events', False))
436483
from_date = args.get('from_date')
437484
fetch_start_time = handle_from_date_argument(from_date) if from_date else None
485+
parsed_interval = dateparser.parse(params.get('deviceFetchInterval', '24 hours')) or dateparser.parse('24 hours')
486+
device_fetch_interval: timedelta = (datetime.now() - parsed_interval) # type: ignore[operator]
438487

439488
demisto.debug(f'Command being called is {command}')
440489

@@ -467,12 +516,14 @@ def main(): # pragma: no cover
467516
events, next_run = fetch_events(
468517
client=client,
469518
max_fetch=max_fetch,
519+
devices_max_fetch=devices_max_fetch,
470520
last_run=last_run,
471521
fetch_start_time=fetch_start_time,
472522
event_types_to_fetch=event_types_to_fetch,
523+
device_fetch_interval=device_fetch_interval
473524
)
474-
475-
demisto.debug(f'debug-log: {len(events)} events fetched from armis api')
525+
for key, value in events.items():
526+
demisto.debug(f'debug-log: {len(value)} events of type: {key} fetched from armis api')
476527

477528
if should_push_events:
478529
handle_fetched_events(events, next_run)

Packs/Armis/Integrations/ArmisEventCollector/ArmisEventCollector.yml

+31-8
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,53 @@ configuration:
1616
hiddenusername: true
1717
type: 9
1818
section: Connect
19-
- display: Number of events to fetch per type
19+
- display: Maximum number of events per fetch
2020
name: max_fetch
21-
additionalinfo: The maximum number of events to fetch per event type.
21+
additionalinfo: Alerts and activity events.
2222
type: 0
23-
defaultvalue: 1000
23+
defaultvalue: 5000
2424
section: Collect
25+
- display: Maximum number of device events per fetch
26+
name: devices_max_fetch
27+
type: 0
28+
section: Collect
29+
additionalinfo: Devices events.
30+
defaultvalue: 10000
2531
- display: Trust any certificate (not secure)
2632
name: insecure
2733
type: 8
2834
section: Connect
2935
- display: Use system proxy settings
3036
name: proxy
31-
type: 8
3237
section: Connect
38+
type: 8
3339
- display: Event types to fetch
3440
name: event_types_to_fetch
3541
section: Collect
3642
required: true
3743
type: 16
38-
defaultvalue: Alerts,Threat activities
44+
defaultvalue: Alerts,Devices,Activities
3945
options:
4046
- Alerts
41-
- Threat activities
42-
description: Collects alerts & threat activities from Armis resources.
47+
- Devices
48+
- Activities
49+
- section: Collect
50+
advanced: true
51+
display: Events Fetch Interval
52+
additionalinfo: Alerts and activity events.
53+
name: eventFetchInterval
54+
defaultvalue: "1"
55+
type: 19
56+
required: false
57+
- section: Collect
58+
advanced: true
59+
display: Device Fetch Interval
60+
additionalinfo: Time between fetch of devices (for example 12 hours, 60 minutes, etc.).
61+
name: deviceFetchInterval
62+
defaultvalue: "24 hours"
63+
type: 0
64+
required: false
65+
description: Collects alerts, devices and activities from Armis resources.
4366
display: Armis Event Collector
4467
name: ArmisEventCollector
4568
supportlevelheader: xsoar
@@ -54,7 +77,7 @@ script:
5477
- 'true'
5578
- 'false'
5679
required: true
57-
- description: The date from which to fetch events. The format should be YYYY-MM-DD or YYYY-MM-DDT:HH:MM:SS. If not specified, the current date will be used.
80+
- description: The date from which to fetch events. The format should be '20 minutes', '1 hour' or '2 days'.
5881
name: from_date
5982
required: false
6083
description: Manual command to fetch and display events. This command is used for developing/debugging and is to be used with caution, as it can create events, leading to events duplication and exceeding the API request limitation.

0 commit comments

Comments
 (0)