Code42 Event Collector
Code42 Insider Risk software solutions provide the right balance of transparency, technology and training to detect and appropriately respond to data risk. Use the Code42EventCollector integration to fetch file events and audit logs.
Data Enrichment & Threat Intelligence · Code42
Details
| ID | Code42 Event Collector |
|---|---|
| Provider | Permira |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 8.4.0 |
| Docker Image | demisto/py42:1.0.0.10120494 |
| Supported Modules | Agentix XSIAM EDR Cortex Cloud Cloud Runtime Security |
README
Code42 Insider Risk software solutions provide the right balance of transparency, technology and training to detect and appropriately respond to data risk. Use the Code42EventCollector integration to fetch file events and audit logs.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure Code42 Event Collector in Cortex
| Parameter | Required |
|---|---|
| Server URL (e.g., https://api.us.code42.com, see help section) | True |
| API Client ID | True |
| API Client Secret | True |
| Maximum number of file events per fetch | True |
| Maximum number of audit events per fetch | True |
| Trust any certificate (not secure) | False |
Code42 Event Collector Authentication
Code42 API uses the OAuth 2.0 protocol for authentication and authorization.
The domain used for making API requests can be determined using the domain you use to log in to the Code42 console.
| Console Domain | API Domain |
|---|---|
| console.us.code42.com | api.us.code42.com |
| console.us2.code42.com | api.us2.code42.com |
| console.ie.code42.com | api.ie.code42.com |
| console.gov.code42.com | api.gov.code42.com |
For each request sent to the API, a bearer token will be requested to authenticate your action. The bearer token should be renewed each 15 minutes. This is done automatically by the integration.
You can retrieve your API credentials by following the instructions in the Code 42 documentation.
Code42 Event Collector Rate Limits
The Code42 API can handle up to 120 requests per minute. After that the API will start to decline client’s requests.
The integration with the default configuration should not raise any rate-limits.
Code42 Event Collector Required Scopes
To use the Code42 Event Collector, make sure you have the correct product plan which must include full Code42 API access.
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
code42-get-events
Manual command to get events, used mainly for debugging
Base Command
code42-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| start_date | Starting time from which to get events. | Required |
| end_date | Time until when to get events. | Required |
| limit | The maximum number of events to return. Default is 100. | Required |
| event_type | The type of event to return. Possible values are: audit-logs, file-events. | Required |
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (e.g., https://api.us.code42.com, see help section) (required)credentials— API Client ID (required)max_file_events_per_fetch— Maximum number of file events per fetchmax_audit_events_per_fetch— Maximum number of audit events per fetchinsecure— Trust any certificate (not secure)event_types_to_fetch— Event Types To Fetch
Commands (1)
-
code42-get-eventsManual command to get events, used mainly for debugging.
import hashlib from collections.abc import Iterable from enum import Enum from typing import Any import incydr from incydr.enums.file_events import EventSearchTerm import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa DEFAULT_FILE_EVENTS_MAX_FETCH = 50000 DEFAULT_AUDIT_EVENTS_MAX_FETCH = 100000 DATE_FORMAT = "%Y-%m-%d %H:%M:%S.%fZ" MAX_FETCH_AUDIT_LOGS = 100000 MAX_AUDIT_LOGS_PAGE_SIZE = 9000 # Ensure page size is larger than batch size to reduce the number of requests MAX_AUDIT_LOGS_BATCH_SIZE = 8000 MAX_FETCH_FILE_EVENTS = 50000 MAX_FILE_EVENTS_PAGE_SIZE = 9000 # Ensure page size is larger than batch size to reduce the number of requests MAX_FILE_EVENTS_BATCH_SIZE = 8000 FILE_EVENTS_LOOK_BACK = timedelta(seconds=45) # The time filter in Code 42 is only accurate up to the first 23 characters (first 3 microsecond digits) # i.e., a query for incidents inserted after "2025-01-01 00:00:00.123456Z" is the same as "2025-01-01 00:00:00.123000Z" CODE42_DATETIME_ACCURACY = 23 VENDOR = "code42" PRODUCT = "code42" class FileEventLastRun(str, Enum): TIME = "file-event-time" # saves the last time of previous fetch of file-events FETCHED_IDS = "file-event-ids" # saved a list of IDs of previous fetch which are is the latest time CUMULATIVE_COUNT = "file-event-count" # running total of the number of events (reset back to 0 when max fetch is reached) NEXT_TRIGGER = "file-event-next-trigger" # helps determine when to trigger next fetch class AuditLogLastRun(str, Enum): TIME = "audit-log-time" # saves the last time of previous fetch of audit-logs FETCHED_IDS = "audit-log-ids" # saved a list of IDs of previous fetch which are is the latest time CUMULATIVE_COUNT = "audit-log-count" # running total of the number of events (reset back to 0 when max fetch is reached) NEXT_TRIGGER = "audit-log-next-trigger" # helps determine when to trigger next fetch NEXT_TRIGGER_VALUE = "1" # seconds to wait before triggering the next fetch iteration when batching is in progress class EventType(str, Enum): FILE = "file" AUDIT = "audit" class Client: def __init__(self, base_url: str, client_id: str, client_secret: str, verify: bool): self.client_id = client_id self.client_secret = client_secret self.code42_client = incydr.Client(base_url, api_client_id=client_id, api_client_secret=client_secret) self.code42_client.session.verify = verify def get_audit_logs( self, start_time: datetime | str, end_time: datetime | timedelta | str | None = None, limit: int = MAX_FETCH_AUDIT_LOGS, page_size: int = MAX_AUDIT_LOGS_PAGE_SIZE, ): """ Get audit logs Args: start_time: from which start time to get logs end_time: until which time to get logs limit: maximum events to retrieve page_size: the page size per single request """ demisto.debug(f"Starting {EventType.AUDIT.value} logs query: {start_time=}, {end_time=}, {limit=}.") audit_logs = [] for audit_log in self.code42_client.audit_log.v1.iter_all( start_time=start_time, end_time=end_time, page_size=page_size, # iterates all the pages ): encoded_audit_log = json.dumps(audit_log, sort_keys=True).encode() audit_log["id"] = hashlib.sha256(encoded_audit_log).hexdigest() audit_log["_time"] = dateparser.parse(audit_log["timestamp"]) audit_log["eventType"] = EventType.AUDIT audit_logs.append(audit_log) sorted_audit_logs = sorted( audit_logs, key=lambda _log: _log["_time"], # type: ignore[arg-type, return-value] )[:limit] demisto.debug(f"Finished {EventType.AUDIT.value} logs query. Got {len(sorted_audit_logs)} events.") return sorted_audit_logs def get_file_events( self, start_time: datetime | str | timedelta, end_time: datetime | str | timedelta | None = None, limit: int = MAX_FETCH_FILE_EVENTS, page_size: int = MAX_FILE_EVENTS_PAGE_SIZE, ) -> List[dict[str, Any]]: """ Get file events Args: start_time: from which start time to get events end_time: until which time to get events limit: maximum events to retrieve page_size: the page size per single request """ demisto.debug(f"Starting {EventType.FILE.value} events query: {start_time=}, {end_time=}, {limit=}.") query = incydr.EventQuery( start_date=start_time, end_date=end_time, page_size=page_size, sort_dir="asc", sort_key=EventSearchTerm.EVENT_INSERTED, ) for filter in query.groups[0].filters: filter.term = EventSearchTerm.EVENT_INSERTED response = self.code42_client.file_events.v2.search(query) if response.total_count == 0: return [] file_events = response.file_events while query.page_token is not None and len(file_events) < limit: response = self.code42_client.file_events.v2.search(query) if current_events := response.file_events: file_events.extend(current_events) sorted_file_events = sorted(file_events, key=lambda x: x.event.inserted)[:limit] events = [] for event in sorted_file_events: event_dict = event.dict() event_dict["eventType"] = EventType.FILE event_dict["_time"] = event.event.inserted events.append(event_dict) demisto.debug(f"Finished {EventType.FILE.value} events query. Got {len(sorted_file_events)} events.") return events def dedup_fetched_events(events: List[dict], last_run_fetched_event_ids: Iterable[str], keys_list_to_id: List[str]) -> List[dict]: """ Dedup events, removes events which were already fetched. Args: events (list[dict]): the events to deduplicate last_run_fetched_event_ids (Iterable[dict]): a list of already fetched IDs from previous run keys_list_to_id (list): a list of keys to retrieve the ID from the event """ new_events = [] for event in events: event_id = dict_safe_get(event, keys=keys_list_to_id) if event_id not in last_run_fetched_event_ids: new_events.append(event) return new_events def get_event_ids(events: List[Dict[str, Any]], keys_to_id: List[str]) -> List[str]: return [dict_safe_get(event, keys=keys_to_id) for event in events] def get_latest_file_event_ids_and_time( events: List[dict], pre_fetch_look_back: Optional[datetime] = None ) -> tuple[dict[str, str], str]: """ Get the latest event IDs and get latest time Args: events: list of events keys_to_id: a list of nested keys to get into the event ID """ latest_time_event = max(event["_time"] for event in events) next_fetch_from = datetime.fromisoformat( min(latest_time_event, pre_fetch_look_back or latest_time_event).strftime("%Y-%m-%dT%H:%M:%S.%f")[ :CODE42_DATETIME_ACCURACY ] + "000+00:00" ) latest_event_ids = { dict_safe_get(event, keys=["event", "id"]): event["_time"].isoformat() for event in events if event["_time"] >= next_fetch_from } demisto.debug(f"Next file events fetch from {next_fetch_from}") return latest_event_ids, next_fetch_from.strftime(DATE_FORMAT) def get_latest_audit_logs_ids_and_time(events: List[dict]) -> tuple[List[str], str]: """ Get the latest event IDs and get latest time Args: events: list of events keys_to_id: a list of nested keys to get into the event ID """ latest_time_event = datetime.fromisoformat( max([event["_time"] for event in events]).strftime("%Y-%m-%dT%H:%M:%S.%f")[:CODE42_DATETIME_ACCURACY] + "000+00:00" ) latest_event_ids: List = [dict_safe_get(event, keys=["id"]) for event in events if event["_time"] >= latest_time_event] demisto.debug(f"Latest Audit Log event IDs occurred after {latest_time_event}") return latest_event_ids, latest_time_event.strftime(DATE_FORMAT) def datetime_to_date_string(events: List[Dict[str, Any]]): """ Recursively convert all datetime fields inside an event to date-strings Args: events: list of events """ def _datetime_to_date_string(_event: Dict[str, Any]): for key in _event: if isinstance(_event[key], datetime): _event[key] = _event[key].strftime(DATE_FORMAT) elif isinstance(_event[key], dict): _datetime_to_date_string(_event[key]) elif isinstance(_event[key], list): for k in _event[key]: if isinstance(k, dict): _datetime_to_date_string(k) for event in events: _datetime_to_date_string(event) def set_event_limit(remaining_count: int, max_batch_size: int) -> int: """Sets the limit of events to be fetched to a positive integer smaller than the given `max_batch_size`. Args: remaining_count (int): The remaining number of events unfetched events. max_batch_size (int): The upper limit to be requested. Returns: int: The upper limit of the number events to be fetched """ if remaining_count > max_batch_size: demisto.debug(f"Found {remaining_count=} larger than {max_batch_size=}. Setting limit to {max_batch_size}.") limit = max_batch_size elif remaining_count <= 0: demisto.debug(f"Found {remaining_count=} less than or equal to 0. Setting limit to {max_batch_size}.") limit = max_batch_size else: demisto.debug(f"Found {remaining_count=} within range. Setting limit to {remaining_count}.") limit = remaining_count return limit def test_module(client: Client, event_types_to_fetch) -> str: """ Tests that it is possible to retrieve file events and audit logs and credentials are valid """ if "File" in event_types_to_fetch: client.get_file_events(timedelta(minutes=1), limit=1) if "Audit" in event_types_to_fetch: client.get_audit_logs(datetime.now() - timedelta(minutes=1), limit=1) return "ok" def fetch_file_events(client: Client, last_run: dict, max_fetch_file_events: int) -> tuple[list[dict[str, Any]], dict[str, Any]]: """ Fetches file events in batches according to the `MAX_FILE_EVENTS_BATCH_SIZE` Args: client: Code42EventCollector client last_run: Last run object max_fetch_file_events: Maximum number of file events to return (split into batches if larger than the maximum batch size) """ demisto.debug(f"Starting to fetch {EventType.FILE.value} events, {max_fetch_file_events=}, {last_run=}.") new_last_run = last_run.copy() file_event_time = cast( # `cast` signals return value to type checker (does not do anything during runtime) datetime, dateparser.parse(last_run[FileEventLastRun.TIME]) if FileEventLastRun.TIME in last_run else (datetime.now() - timedelta(minutes=1)), ) fetched_events = last_run.get(FileEventLastRun.FETCHED_IDS, {}) pre_fetch_look_back = datetime.now(tz=timezone.utc) - FILE_EVENTS_LOOK_BACK cumulative_count = last_run.get(FileEventLastRun.CUMULATIVE_COUNT, 0) remaining_count = max_fetch_file_events - cumulative_count limit = set_event_limit(remaining_count=remaining_count, max_batch_size=MAX_FILE_EVENTS_BATCH_SIZE) demisto.debug( f"Starting batch fetch for {EventType.FILE.value} events. " f"Last progress={cumulative_count}/{max_fetch_file_events}, {limit=}." ) # Assume over-fetching negligible (very few last run fetched IDs since timestamp in microsecond resolution) file_events = client.get_file_events(file_event_time, limit=limit + len(fetched_events)) dedup_file_events = dedup_fetched_events( events=file_events, last_run_fetched_event_ids=fetched_events, keys_list_to_id=["event", "id"], ) demisto.debug( f"Fetched {len(file_events)} {EventType.FILE.value} events. Got {len(dedup_file_events)} events after deduplication." ) if not file_events: new_last_run.update( { FileEventLastRun.CUMULATIVE_COUNT.value: 0, FileEventLastRun.NEXT_TRIGGER.value: None, } ) else: latest_file_event_ids, latest_file_event_time = get_latest_file_event_ids_and_time( events=file_events + format_last_run_dupes(fetched_events), pre_fetch_look_back=pre_fetch_look_back, ) datetime_to_date_string(dedup_file_events) latest_cumulative_count = cumulative_count + len(dedup_file_events) # If event count under the requested limit or accumulated enough events, reset to indicate batched fetching is done if len(dedup_file_events) < limit or latest_cumulative_count >= max_fetch_file_events: latest_cumulative_count = 0 next_trigger = None # fetch interval demisto.debug( f"Batching complete for {EventType.FILE.value} events. " f"New progress={latest_cumulative_count}/{max_fetch_file_events}, {next_trigger=}." ) # Otherwise, if batched fetching is still ongoing, keep latest cumulative count and trigger near immediate next fetch else: next_trigger = NEXT_TRIGGER_VALUE demisto.debug( f"Batch continues for {EventType.FILE.value} events. " f"New progress={latest_cumulative_count}/{max_fetch_file_events}, {next_trigger=}." ) new_last_run.update( { FileEventLastRun.TIME.value: latest_file_event_time, FileEventLastRun.FETCHED_IDS.value: latest_file_event_ids, FileEventLastRun.CUMULATIVE_COUNT.value: latest_cumulative_count, FileEventLastRun.NEXT_TRIGGER.value: next_trigger, } ) demisto.debug(f"Fetched {len(dedup_file_events)} {EventType.FILE.value} events, {new_last_run=}.") return dedup_file_events, new_last_run def fetch_audit_logs(client: Client, last_run: dict, max_fetch_audit_events: int) -> tuple[list[dict[str, Any]], dict[str, Any]]: """ Fetch audit logs in batches according to the `MAX_AUDIT_LOGS_BATCH_SIZE` Args: client: Code42EventCollector client last_run: Last run object max_fetch_audit_events: Maximum number of audit logs to return (split into batches if larger than the maximum batch size) """ demisto.debug(f"Starting to fetch {EventType.AUDIT.value} logs, {max_fetch_audit_events=}, {last_run=}.") new_last_run = last_run.copy() audit_log_time = cast( # `cast` signals return value to type checker (does not do anything during runtime) datetime, dateparser.parse(last_run[AuditLogLastRun.TIME]) if AuditLogLastRun.TIME in last_run else (datetime.now() - timedelta(minutes=1)), ) last_fetched_audit_log_ids = set(last_run.get(AuditLogLastRun.FETCHED_IDS, [])) cumulative_count = last_run.get(AuditLogLastRun.CUMULATIVE_COUNT, 0) remaining_count = max_fetch_audit_events - cumulative_count limit = set_event_limit(remaining_count=remaining_count, max_batch_size=MAX_AUDIT_LOGS_BATCH_SIZE) demisto.debug( f"Starting batch fetch for {EventType.AUDIT.value} logs. " f"Last progress={cumulative_count}/{max_fetch_audit_events}, {limit=}." ) # Assume over-fetching negligible (very few last run fetched IDs since timestamp in microsecond resolution) audit_logs = client.get_audit_logs(audit_log_time, limit=limit + len(last_fetched_audit_log_ids)) dedup_audit_logs = dedup_fetched_events( events=audit_logs, last_run_fetched_event_ids=last_fetched_audit_log_ids, keys_list_to_id=["id"], ) demisto.debug( f"Fetched {len(audit_logs)} {EventType.AUDIT.value} logs. Got {len(dedup_audit_logs)} logs after deduplication." ) if not audit_logs: new_last_run.update( { AuditLogLastRun.CUMULATIVE_COUNT.value: 0, AuditLogLastRun.NEXT_TRIGGER.value: None, } ) else: latest_audit_log_ids, latest_audit_log_time = get_latest_audit_logs_ids_and_time(dedup_audit_logs) datetime_to_date_string(dedup_audit_logs) latest_cumulative_count = cumulative_count + len(dedup_audit_logs) # If event count under the requested limit or accumulated enough events, reset to indicate batched fetching is done if len(dedup_audit_logs) < limit or latest_cumulative_count >= max_fetch_audit_events: latest_cumulative_count = 0 next_trigger = None # fetch interval demisto.debug( f"Batching complete for {EventType.AUDIT.value} logs. " f"New progress={latest_cumulative_count}/{max_fetch_audit_events}, {next_trigger=}." ) # Otherwise, if batched fetching is still ongoing, keep latest cumulative count and trigger near immediate next fetch else: next_trigger = NEXT_TRIGGER_VALUE demisto.debug( f"Batch continues for {EventType.AUDIT.value} logs. " f"New progress={latest_cumulative_count}/{max_fetch_audit_events}, {next_trigger=}." ) new_last_run.update( { AuditLogLastRun.TIME.value: latest_audit_log_time, AuditLogLastRun.FETCHED_IDS.value: latest_audit_log_ids, AuditLogLastRun.CUMULATIVE_COUNT.value: latest_cumulative_count, AuditLogLastRun.NEXT_TRIGGER.value: next_trigger, } ) demisto.debug(f"Fetched {len(dedup_audit_logs)} {EventType.AUDIT.value} logs, {new_last_run=}.") return dedup_audit_logs, new_last_run def format_last_run_dupes(dupes: dict) -> list[dict]: return [{"event": {"id": dupe_id}, "_time": datetime.fromisoformat(dupe_time)} for dupe_id, dupe_time in dupes.items()] def fetch_events( client: Client, last_run: dict, max_fetch_file_events: int, max_fetch_audit_events: int, event_types_to_fetch: List[str] ): """ Fetch audit-logs & file-events """ demisto.debug(f"Starting fetching events. Using {event_types_to_fetch=}, got {last_run=}.") total_event_count: int = 0 # Fetch file events and send to XSIAM in batches with nextTrigger if needed if "File" in event_types_to_fetch: file_events, file_events_last_run = fetch_file_events( client, last_run=last_run, max_fetch_file_events=max_fetch_file_events ) last_run.update(file_events_last_run) demisto.debug(f"Starting sending {len(file_events)} {EventType.FILE.value} events.") # `demisto.updateModuleHealth` *not* called as part of `send_events_to_xsiam` since `multiple_threads=True` futures = send_events_to_xsiam(file_events, multiple_threads=True, vendor=VENDOR, product=PRODUCT) if futures: tuple(concurrent.futures.as_completed(futures)) # wait for all the alerts to be sent XSIAM demisto.debug(f"Finished sending {len(file_events)} {EventType.FILE.value} events. Updating module health") demisto.updateModuleHealth({"eventsPulled": len(file_events)}) total_event_count += len(file_events) # Fetch audit logs and send to XSIAM in batches with nextTrigger if needed if "Audit" in event_types_to_fetch: audit_logs, audit_logs_last_run = fetch_audit_logs( client, last_run=last_run, max_fetch_audit_events=max_fetch_audit_events ) for log in audit_logs: log.pop("id", None) last_run.update(audit_logs_last_run) demisto.debug(f"Starting sending {len(audit_logs)} {EventType.AUDIT.value} events.") # `demisto.updateModuleHealth` *not* called as part of `send_events_to_xsiam` since `multiple_threads=True` futures = send_events_to_xsiam(audit_logs, multiple_threads=True, vendor=VENDOR, product=PRODUCT) if futures: tuple(concurrent.futures.as_completed(futures)) # wait for all the alerts to be sent XSIAM demisto.debug(f"Finished sending {len(audit_logs)} {EventType.AUDIT.value} events. Updating module health") demisto.updateModuleHealth({"eventsPulled": len(audit_logs)}) total_event_count += len(audit_logs) # If at least one type has not completed batching, trigger next fetch iteration in 1 second if last_run.get(FileEventLastRun.NEXT_TRIGGER.value) or last_run.get(AuditLogLastRun.NEXT_TRIGGER.value): last_run["nextTrigger"] = NEXT_TRIGGER_VALUE demisto.debug("At least on event type has batching in progress. Next run will be triggered in 1 second.") else: last_run["nextTrigger"] = None demisto.debug("All event types finished batching. Next run will be triggered based on fetch interval.") demisto.debug(f"Finished fetching and sending {total_event_count} events. Setting {last_run=}.") demisto.setLastRun(last_run) def get_events_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Get events command, used mainly for debugging """ start_date = args["start_date"] end_date = args.get("end_date") limit = arg_to_number(args.get("limit")) or 100 event_type = args["event_type"] if event_type == "audit": events = client.get_audit_logs(start_date, end_time=end_date, limit=limit) readable_output = tableToMarkdown( "Audit Logs", events, headers=["actorId", "actorName", "timestamp", "type"], headerTransform=pascalToSpace, removeNull=True, ) else: events = client.get_file_events(start_date, end_time=end_date, limit=limit) for event in events: event["Id"] = event["event"]["id"] readable_output = tableToMarkdown("File Events", events, headers=["id", "_time"], removeNull=True) return CommandResults( outputs_prefix="Code42EventCollector.Events", outputs=events, raw_response=events, readable_output=readable_output ) def main() -> None: params = demisto.params() client_id: str = params.get("credentials", {}).get("identifier", "") client_secret: str = params.get("credentials", {}).get("password", "") base_url: str = params.get("url", "").rstrip("/") verify_certificate = not params.get("insecure", False) max_fetch_file_events = arg_to_number(params.get("max_file_events_per_fetch")) or DEFAULT_FILE_EVENTS_MAX_FETCH max_fetch_audit_events = arg_to_number(params.get("max_audit_events_per_fetch")) or DEFAULT_AUDIT_EVENTS_MAX_FETCH event_types_to_fetch = argToList(params.get("event_types_to_fetch", ["File"])) command = demisto.command() demisto.info(f"Command being called is {command}") try: client = Client( client_id=client_id, client_secret=client_secret, base_url=base_url, verify=verify_certificate, ) if command == "test-module": return_results(test_module(client, event_types_to_fetch)) elif command == "fetch-events": fetch_events( client, last_run=demisto.getLastRun(), max_fetch_file_events=max_fetch_file_events, max_fetch_audit_events=max_fetch_audit_events, event_types_to_fetch=event_types_to_fetch, ) elif command == "code42-get-events": return_results(get_events_command(client, demisto.args())) else: raise NotImplementedError(f"Unknown command {command!r}") except Exception as e: demisto.error(traceback.format_exc()) return_error(f"Failed to execute {command} command.\nError:\ntype:{type(e)}, error:{str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): main()