diff --git a/Packs/Code42/Integrations/Code42/Code42.py b/Packs/Code42/Integrations/Code42/Code42.py index 85545717e75..aa0260bf4a8 100644 --- a/Packs/Code42/Integrations/Code42/Code42.py +++ b/Packs/Code42/Integrations/Code42/Code42.py @@ -1,12 +1,14 @@ -from typing import Optional, Dict, Any import demistomock as demisto from CommonServerPython import * -''' IMPORTS ''' + +""" IMPORTS """ import json import requests -from py42.sdk import SDK +import py42.sdk import py42.settings -from py42.sdk.file_event_query import ( +from datetime import datetime +from py42.sdk.queries.fileevents.file_event_query import FileEventQuery +from py42.sdk.queries.fileevents.filters import ( MD5, SHA256, Actor, @@ -15,425 +17,608 @@ DeviceUsername, ExposureType, EventType, - FileEventQuery -) -from py42.sdk.alert_query import ( - DateObserved, - Severity, - AlertState, - AlertQuery + FileCategory, ) -import time +from py42.sdk.queries.alerts.alert_query import AlertQuery +from py42.sdk.queries.alerts.filters import DateObserved, Severity, AlertState + # Disable insecure warnings requests.packages.urllib3.disable_warnings() -''' CONSTANTS ''' +""" CONSTANTS """ CODE42_EVENT_CONTEXT_FIELD_MAPPER = { - 'eventTimestamp': 'EventTimestamp', - 'createTimestamp': 'FileCreated', - 'deviceUid': 'EndpointID', - 'deviceUserName': 'DeviceUsername', - 'emailFrom': 'EmailFrom', - 'emailRecipients': 'EmailTo', - 'emailSubject': 'EmailSubject', - 'eventId': 'EventID', - 'eventType': 'EventType', - 'fileCategory': 'FileCategory', - 'fileOwner': 'FileOwner', - 'fileName': 'FileName', - 'filePath': 'FilePath', - 'fileSize': 'FileSize', - 'modifyTimestamp': 'FileModified', - 'md5Checksum': 'FileMD5', - 'osHostName': 'FileHostname', - 'privateIpAddresses': 'DevicePrivateIPAddress', - 'publicIpAddresses': 'DevicePublicIPAddress', - 'removableMediaBusType': 'RemovableMediaType', - 'removableMediaCapacity': 'RemovableMediaCapacity', - 'removableMediaMediaName': 'RemovableMediaMediaName', - 'removableMediaName': 'RemovableMediaName', - 'removableMediaSerialNumber': 'RemovableMediaSerialNumber', - 'removableMediaVendor': 'RemovableMediaVendor', - 'sha256Checksum': 'FileSHA256', - 'shared': 'FileShared', - 'sharedWith': 'FileSharedWith', - 'source': 'Source', - 'tabUrl': 'ApplicationTabURL', - 'url': 'FileURL', - 'processName': 'ProcessName', - 'processOwner': 'ProcessOwner', - 'windowTitle': 'WindowTitle', - 'exposure': 'Exposure', - 'sharingTypeAdded': 'SharingTypeAdded' + "eventTimestamp": "EventTimestamp", + "createTimestamp": "FileCreated", + "deviceUid": "EndpointID", + "deviceUserName": "DeviceUsername", + "emailFrom": "EmailFrom", + "emailRecipients": "EmailTo", + "emailSubject": "EmailSubject", + "eventId": "EventID", + "eventType": "EventType", + "fileCategory": "FileCategory", + "fileOwner": "FileOwner", + "fileName": "FileName", + "filePath": "FilePath", + "fileSize": "FileSize", + "modifyTimestamp": "FileModified", + "md5Checksum": "FileMD5", + "osHostName": "FileHostname", + "privateIpAddresses": "DevicePrivateIPAddress", + "publicIpAddresses": "DevicePublicIPAddress", + "removableMediaBusType": "RemovableMediaType", + "removableMediaCapacity": "RemovableMediaCapacity", + "removableMediaMediaName": "RemovableMediaMediaName", + "removableMediaName": "RemovableMediaName", + "removableMediaSerialNumber": "RemovableMediaSerialNumber", + "removableMediaVendor": "RemovableMediaVendor", + "sha256Checksum": "FileSHA256", + "shared": "FileShared", + "sharedWith": "FileSharedWith", + "source": "Source", + "tabUrl": "ApplicationTabURL", + "url": "FileURL", + "processName": "ProcessName", + "processOwner": "ProcessOwner", + "windowTitle": "WindowTitle", + "exposure": "Exposure", + "sharingTypeAdded": "SharingTypeAdded", } CODE42_ALERT_CONTEXT_FIELD_MAPPER = { - 'actor': 'Username', - 'createdAt': 'Occurred', - 'description': 'Description', - 'id': 'ID', - 'name': 'Name', - 'state': 'State', - 'type': 'Type', - 'severity': 'Severity' + "actor": "Username", + "createdAt": "Occurred", + "description": "Description", + "id": "ID", + "name": "Name", + "state": "State", + "type": "Type", + "severity": "Severity", } FILE_CONTEXT_FIELD_MAPPER = { - 'fileName': 'Name', - 'filePath': 'Path', - 'fileSize': 'Size', - 'md5Checksum': 'MD5', - 'sha256Checksum': 'SHA256', - 'osHostName': 'Hostname' + "fileName": "Name", + "filePath": "Path", + "fileSize": "Size", + "md5Checksum": "MD5", + "sha256Checksum": "SHA256", + "osHostName": "Hostname", } CODE42_FILE_TYPE_MAPPER = { - 'SourceCode': 'SOURCE_CODE', - 'Audio': 'AUDIO', - 'Executable': 'EXECUTABLE', - 'Document': 'DOCUMENT', - 'Image': 'IMAGE', - 'PDF': 'PDF', - 'Presentation': 'PRESENTATION', - 'Script': 'SCRIPT', - 'Spreadsheet': 'SPREADSHEET', - 'Video': 'VIDEO', - 'VirtualDiskImage': 'VIRTUAL_DISK_IMAGE', - 'Archive': 'ARCHIVE' + "SourceCode": "SOURCE_CODE", + "Audio": "AUDIO", + "Executable": "EXECUTABLE", + "Document": "DOCUMENT", + "Image": "IMAGE", + "PDF": "PDF", + "Presentation": "PRESENTATION", + "Script": "SCRIPT", + "Spreadsheet": "SPREADSHEET", + "Video": "VIDEO", + "VirtualDiskImage": "VIRTUAL_DISK_IMAGE", + "Archive": "ARCHIVE", } -SECURITY_EVENT_HEADERS = ['EventType', 'FileName', 'FileSize', 'FileHostname', 'FileOwner', 'FileCategory', 'DeviceUsername'] -SECURITY_ALERT_HEADERS = ['Type', 'Occurred', 'Username', 'Name', 'Description', 'State', 'ID'] +SECURITY_EVENT_HEADERS = [ + "EventType", + "FileName", + "FileSize", + "FileHostname", + "FileOwner", + "FileCategory", + "DeviceUsername", +] +SECURITY_ALERT_HEADERS = ["Type", "Occurred", "Username", "Name", "Description", "State", "ID"] + + +def _get_severity_filter_value(severity_arg): + """Converts single str to upper case. If given list of strs, converts all to upper case.""" + if severity_arg: + return ( + [severity_arg.upper()] + if isinstance(severity_arg, str) + else list(map(lambda x: x.upper(), severity_arg)) + ) + + +def _create_alert_query(event_severity_filter, start_time): + """Creates an alert query for the given severity (or severities) and start time.""" + alert_filters = AlertQueryFilters() + severity = event_severity_filter + alert_filters.append_result(_get_severity_filter_value(severity), Severity.is_in) + alert_filters.append(AlertState.eq(AlertState.OPEN)) + alert_filters.append_result(start_time, DateObserved.on_or_after) + alert_query = alert_filters.to_all_query() + return alert_query class Code42Client(BaseClient): """ - Client will implement the service API, should not contain Demisto logic. + Client will implement the service API, should not contain Cortex XSOAR logic. Should do requests and return data """ def __init__(self, sdk, base_url, auth, verify=True, proxy=False): super().__init__(base_url, verify=verify, proxy=proxy) - # Create the Code42 SDK instnace - self._sdk = sdk.create_using_local_account(base_url, auth[0], auth[1]) - py42.settings.set_user_agent_suffix("Demisto") + # Create the Code42 SDK instance + self._sdk = sdk or py42.sdk.from_local_account(base_url, auth[0], auth[1]) + py42.settings.set_user_agent_suffix("Cortex XSOAR") - def add_user_to_departing_employee(self, username, departure_epoch=None, note=None): - de = self._sdk.employee_case_management.departing_employee + def add_user_to_departing_employee(self, username, departure_date=None, note=None): try: - res = de.create_departing_employee(username, departure_epoch=departure_epoch, notes=note) + user_id = self.get_user_id(username) + self._sdk.detectionlists.departing_employee.add(user_id, departure_date=departure_date) + if note: + self._sdk.detectionlists.update_user_notes(user_id, note) except Exception: return None - return res.json().get('caseId') - - def fetch_alerts(self, start_time, event_severity_filter=None): - alert_filter = [] - # Create alert filter - if event_severity_filter: - alert_filter.append(Severity.is_in(list(map(lambda x: x.upper(), event_severity_filter)))) - alert_filter.append(AlertState.eq(AlertState.OPEN)) - alert_filter.append(DateObserved.on_or_after(start_time)) - alert_query = AlertQuery(self._sdk.user_context.get_current_tenant_id(), *alert_filter) - alert_query.sort_direction = "asc" - alerts = self._sdk.security.alerts + return user_id + + def fetch_alerts(self, start_time, event_severity_filter): try: - res = alerts.search_alerts(alert_query) + query = _create_alert_query(event_severity_filter, start_time) + res = self._sdk.alerts.search(query) except Exception: return None - return res.json().get('alerts') + return res["alerts"] def get_alert_details(self, alert_id): - alerts = self._sdk.security.alerts try: - res = alerts.get_query_details([alert_id]) + res = self._sdk.alerts.get_details(alert_id) except Exception: return None - else: - # There will only ever be one alert since we search on just one ID - return res.json().get('alerts')[0] + return res["alerts"][0] def get_current_user(self): try: - res = self._sdk.users.get_current_user() + res = self._sdk.users.get_current() except Exception: return None - return res.json() + return res def remove_user_from_departing_employee(self, username): try: - de = self._sdk.employee_case_management.departing_employee - res = de.get_case_by_username(username) + user_id = self.get_user_id(username) + self._sdk.detectionlists.departing_employee.remove(user_id) except Exception: return None - case_id = res.json().get('caseId') + return user_id + + def resolve_alert(self, id): try: - de.resolve_departing_employee(case_id) + self._sdk.alerts.resolve(id) except Exception: return None - return case_id + return id - def resolve_alert(self, id): - alerts = self._sdk.security.alerts + def get_user_id(self, username): try: - alerts.resolve_alert(id) + res = self._sdk.users.get_by_username(username) except Exception: return None - return id + return res["users"][0]["userUid"] def search_json(self, payload): try: - res = self._sdk.security.search_file_events(payload) + res = self._sdk.securitydata.search_file_events(payload) except Exception: return None - return res.json().get('fileEvents') + return res["fileEvents"] + + +class Code42SearchFilters(object): + def __init__(self): + self._filters = [] + + @property + def filters(self): + return self._filters + + def to_all_query(self): + """Override""" + + def append(self, _filter): + if _filter: + self._filters.append(_filter) + + def extend(self, _filters): + if _filters: + self._filters.extend(_filters) + + def append_result(self, value, create_filter): + """Safely creates and appends the filter to the working list.""" + if not value: + return + _filter = create_filter(value) + self.append(_filter) + + +class FileEventQueryFilters(Code42SearchFilters): + """Class for simplifying building up a file event search query""" + + def __init__(self, pg_size=None): + self._pg_size = pg_size + super(FileEventQueryFilters, self).__init__() + + def to_all_query(self): + """Convert list of search criteria to *args""" + query = FileEventQuery.all(*self._filters) + if self._pg_size: + query.page_size = self._pg_size + return query + + +class AlertQueryFilters(Code42SearchFilters): + """Class for simplifying building up an alert search query""" + def to_all_query(self): + query = AlertQuery.all(*self._filters) + query.page_size = 500 + query.sort_direction = "asc" + return query @logger def build_query_payload(args): - """ - Build a query payload combining passed args - """ - search_args = [] - if args.get('hash'): - if len(args['hash']) == 32: - search_args.append(MD5.eq(args['hash'])) - elif len(args['hash']) == 64: - search_args.append(SHA256.eq(args['hash'])) - if args.get('hostname'): - search_args.append(OSHostname.eq(args['hostname'])) - if args.get('username'): - search_args.append(DeviceUsername.eq(args['username'])) - if args.get('exposure'): - # Because the CLI can't accept lists, convert the args to a list if the type is string. - if isinstance(args['exposure'], str): - args['exposure'] = args['exposure'].split(',') - search_args.append(ExposureType.is_in(args['exposure'])) - # Convert list of search criteria to *args - query = FileEventQuery.all(*search_args) - query.page_size = args.get('results') - LOG('File Event Query: {}'.format(query)) - return str(query) + """Build a query payload combining passed args""" + + pg_size = args.get("results") + _hash = args.get("hash") + hostname = args.get("hostname") + username = args.get("username") + exposure = args.get("exposure") + + search_args = FileEventQueryFilters(pg_size) + search_args.append_result(_hash, _create_hash_filter) + search_args.append_result(hostname, OSHostname.eq) + search_args.append_result(username, DeviceUsername.eq) + search_args.append_result(exposure, _create_exposure_filter) + + query = search_args.to_all_query() + LOG("File Event Query: {}".format(str(query))) + return query + + +def _create_hash_filter(hash_arg): + if not hash_arg: + return None + elif len(hash_arg) == 32: + return MD5.eq(hash_arg) + elif len(hash_arg) == 64: + return SHA256.eq(hash_arg) + + +def _create_exposure_filter(exposure_arg): + # Because the CLI can't accept lists, convert the args to a list if the type is string. + if isinstance(exposure_arg, str): + exposure_arg = exposure_arg.split(",") + return ExposureType.is_in(exposure_arg) + + +def _create_category_filter(file_type): + category_value = CODE42_FILE_TYPE_MAPPER.get(file_type["category"], "UNCATEGORIZED") + return FileCategory.eq(category_value) + + +class ObservationToSecurityQueryMapper(object): + """Class to simplify the process of mapping observation data to query objects.""" + + _ENDPOINT_TYPE = "FedEndpointExfiltration" + _CLOUD_TYPE = "FedCloudSharePermissions" + _PUBLIC_SEARCHABLE = "PublicSearchableShare" + _PUBLIC_LINK = "PublicLinkShare" + + def __init__(self, observation, actor): + self._obs = observation + self._actor = actor + + @property + def _observation_data(self): + return self._obs["data"] + + @property + def _exfiltration_type(self): + return self._obs["type"] + + @property + def _is_endpoint_exfiltration(self): + return self._exfiltration_type == self._ENDPOINT_TYPE + + @property + def _is_cloud_exfiltration(self): + return self._exfiltration_type == self._CLOUD_TYPE + + def _create_user_filter(self): + return ( + DeviceUsername.eq(self._actor) + if self._is_endpoint_exfiltration + else Actor.eq(self._actor) + ) + + def map(self): + search_args = self._create_search_args() + query = search_args.to_all_query() + LOG("Alert Observation Query: {}".format(query)) + return query + + def _create_search_args(self): + filters = FileEventQueryFilters() + exposure_types = self._observation_data["exposureTypes"] + begin_time = _convert_date_arg_to_epoch(self._observation_data["firstActivityAt"]) + end_time = _convert_date_arg_to_epoch(self._observation_data["lastActivityAt"]) + + filters.append(self._create_user_filter()) + filters.append(EventTimestamp.on_or_after(begin_time)) + filters.append(EventTimestamp.on_or_before(end_time)) + filters.extend(self._create_exposure_filters(exposure_types)) + filters.append(self._create_file_category_filters()) + + return filters + + def _create_exposure_filters(self, exposure_types): + """Determine exposure types based on alert type""" + + if self._is_cloud_exfiltration: + exp_types = [] + if self._PUBLIC_SEARCHABLE in exposure_types: + exp_types.append(ExposureType.IS_PUBLIC) + if self._PUBLIC_LINK in exposure_types: + exp_types.append(ExposureType.SHARED_VIA_LINK) + return [ExposureType.is_in(exp_types)] + elif self._is_endpoint_exfiltration: + return [ + EventType.is_in(["CREATED", "MODIFIED", "READ_BY_APP"]), + ExposureType.is_in(exposure_types), + ] + return [] + + def _create_file_category_filters(self): + """Determine if file categorization is significant""" + observed_file_categories = self._observation_data["fileCategories"] + categories = [c["category"].upper() for c in observed_file_categories if c["isSignificant"]] + return FileCategory.is_in(categories) -@logger def map_observation_to_security_query(observation, actor): - file_categories: Dict[str, Any] - observation_data = json.loads(observation['data']) - search_args = [] - exp_types = [] - exposure_types = observation_data['exposureTypes'] - begin_time = observation_data['firstActivityAt'] - end_time = observation_data['lastActivityAt'] - if observation['type'] == 'FedEndpointExfiltration': - search_args.append(DeviceUsername.eq(actor)) - else: - search_args.append(Actor.eq(actor)) - search_args.append(EventTimestamp.on_or_after( - int(time.mktime(time.strptime(begin_time.replace('0000000', '000'), "%Y-%m-%dT%H:%M:%S.000Z"))))) - search_args.append(EventTimestamp.on_or_before( - int(time.mktime(time.strptime(end_time.replace('0000000', '000'), "%Y-%m-%dT%H:%M:%S.000Z"))))) - # Determine exposure types based on alert type - if observation['type'] == 'FedCloudSharePermissions': - if 'PublicSearchableShare' in exposure_types: - exp_types.append(ExposureType.IS_PUBLIC) - if 'PublicLinkShare' in exposure_types: - exp_types.append(ExposureType.SHARED_VIA_LINK) - elif observation['type'] == 'FedEndpointExfiltration': - exp_types = exposure_types - search_args.append(EventType.is_in(['CREATED', 'MODIFIED', 'READ_BY_APP'])) - search_args.append(ExposureType.is_in(exp_types)) - # Determine if file categorization is significant - file_categories = { - "filterClause": "OR" - } - filters = [] - for filetype in observation_data['fileCategories']: - if filetype['isSignificant']: - file_category = { - "operator": "IS", - "term": "fileCategory", - "value": CODE42_FILE_TYPE_MAPPER.get(filetype['category'], 'UNCATEGORIZED') - } - filters.append(file_category) - if len(filters): - file_categories['filters'] = filters - search_args.append(json.dumps(file_categories)) - # Convert list of search criteria to *args - query = FileEventQuery.all(*search_args) - LOG('Alert Observation Query: {}'.format(query)) - return str(query) + mapper = ObservationToSecurityQueryMapper(observation, actor) + return mapper.map() + + +def _convert_date_arg_to_epoch(date_arg): + date_arg = date_arg[:25] + return ( + datetime.strptime(date_arg, "%Y-%m-%dT%H:%M:%S.%f") - datetime.utcfromtimestamp(0) + ).total_seconds() @logger def map_to_code42_event_context(obj): - code42_context = {} - for (k, v) in CODE42_EVENT_CONTEXT_FIELD_MAPPER.items(): - if obj.get(k): - code42_context[v] = obj.get(k) + code42_context = _map_obj_to_context(obj, CODE42_EVENT_CONTEXT_FIELD_MAPPER) # FileSharedWith is a special case and needs to be converted to a list - if code42_context.get('FileSharedWith'): - shared_list = [] - for shared_with in code42_context['FileSharedWith']: - shared_list.append(shared_with['cloudUsername']) - code42_context['FileSharedWith'] = str(shared_list) + if code42_context.get("FileSharedWith"): + shared_list = [u["cloudUsername"] for u in code42_context["FileSharedWith"]] + code42_context["FileSharedWith"] = str(shared_list) return code42_context @logger def map_to_code42_alert_context(obj): - code42_context = {} - for (k, v) in CODE42_ALERT_CONTEXT_FIELD_MAPPER.items(): - if obj.get(k): - code42_context[v] = obj.get(k) - return code42_context + return _map_obj_to_context(obj, CODE42_ALERT_CONTEXT_FIELD_MAPPER) @logger def map_to_file_context(obj): - file_context = {} - for (k, v) in FILE_CONTEXT_FIELD_MAPPER.items(): - if obj.get(k): - file_context[v] = obj.get(k) - return file_context + return _map_obj_to_context(obj, FILE_CONTEXT_FIELD_MAPPER) + + +@logger +def _map_obj_to_context(obj, context_mapper): + return {v: obj.get(k) for k, v in context_mapper.items() if obj.get(k)} @logger def alert_get_command(client, args): code42_securityalert_context = [] - alert = client.get_alert_details(args['id']) + alert = client.get_alert_details(args["id"]) if alert: code42_context = map_to_code42_alert_context(alert) code42_securityalert_context.append(code42_context) readable_outputs = tableToMarkdown( - f'Code42 Security Alert Results', + f"Code42 Security Alert Results", code42_securityalert_context, - headers=SECURITY_ALERT_HEADERS + headers=SECURITY_ALERT_HEADERS, ) - return readable_outputs, {'Code42.SecurityAlert': code42_securityalert_context}, alert + return readable_outputs, {"Code42.SecurityAlert": code42_securityalert_context}, alert else: - return 'No results found', {}, {} + return "No results found", {}, {} @logger def alert_resolve_command(client, args): - code42_securityalert_context = [] - alert = client.resolve_alert(args['id']) - if alert: - # Retrieve new alert details - updated_alert = client.get_alert_details(args['id']) - if updated_alert: - code42_context = map_to_code42_alert_context(updated_alert) - code42_securityalert_context.append(code42_context) - readable_outputs = tableToMarkdown( - f'Code42 Security Alert Resolved', - code42_securityalert_context, - headers=SECURITY_ALERT_HEADERS - ) - return readable_outputs, {'Code42.SecurityAlert': code42_securityalert_context}, updated_alert - else: - return 'Error retrieving updated alert', {}, {} - else: - return 'No results found', {}, {} + code42_security_alert_context = [] + alert_id = client.resolve_alert(args["id"]) + + if not alert_id: + return "No results found", {}, {} + + # Retrieve new alert details + alert_details = client.get_alert_details(alert_id) + if not alert_details: + return "Error retrieving updated alert", {}, {} + + code42_context = map_to_code42_alert_context(alert_details) + code42_security_alert_context.append(code42_context) + readable_outputs = tableToMarkdown( + f"Code42 Security Alert Resolved", + code42_security_alert_context, + headers=SECURITY_ALERT_HEADERS, + ) + return ( + readable_outputs, + {"Code42.SecurityAlert": code42_security_alert_context}, + alert_details, + ) @logger def departingemployee_add_command(client, args): - departure_epoch: Optional[int] - # Convert date to epoch - if args.get('departuredate'): - try: - departure_epoch = int(time.mktime(time.strptime(args['departuredate'], '%Y-%m-%d'))) - except Exception: - return_error(message='Could not add user to Departing Employee Lens: ' - 'unable to parse departure date. Is it in YYYY-MM-DD format?') - else: - departure_epoch = None - case = client.add_user_to_departing_employee(args['username'], departure_epoch, args.get('note')) - if case: - de_context = { - 'CaseID': case, - 'Username': args['username'], - 'DepartureDate': args.get('departuredate'), - 'Note': args.get('note') - } - readable_outputs = tableToMarkdown( - f'Code42 Departing Employee Lens User Added', - de_context - ) - return readable_outputs, {'Code42.DepartingEmployee': de_context}, case - else: - return_error(message='Could not add user to Departing Employee Lens') + departing_date = args.get("departuredate") + username = args["username"] + note = args.get("note") + user_id = client.add_user_to_departing_employee(username, departing_date, note) + if not user_id: + return_error(message="Could not add user to Departing Employee List") + + de_context = { + "UserID": user_id, + "Username": username, + "DepartureDate": departing_date, + "Note": note, + } + readable_outputs = tableToMarkdown(f"Code42 Departing Employee List User Added", de_context) + return readable_outputs, {"Code42.DepartingEmployee": de_context}, user_id @logger def departingemployee_remove_command(client, args): - case = client.remove_user_from_departing_employee(args['username']) - if case: - de_context = { - 'CaseID': case, - 'Username': args['username'], - } + username = args["username"] + user_id = client.remove_user_from_departing_employee(username) + if user_id: + de_context = {"UserID": user_id, "Username": username} readable_outputs = tableToMarkdown( - f'Code42 Departing Employee Lens User Removed', - de_context + f"Code42 Departing Employee List User Removed", de_context ) - return readable_outputs, {'Code42.DepartingEmployee': de_context}, case + return readable_outputs, {"Code42.DepartingEmployee": de_context}, user_id else: - return_error(message='Could not remove user from Departing Employee Lens') - - -@logger -def fetch_incidents(client, last_run, first_fetch_time, event_severity_filter, - fetch_limit, include_files, integration_context=None): - incidents = [] - # Determine if there are remaining incidents from last fetch run - if integration_context: - remaining_incidents = integration_context.get("remaining_incidents") - # return incidents if exists in context. - if remaining_incidents: - return last_run, remaining_incidents[:fetch_limit], remaining_incidents[fetch_limit:] - # Get the last fetch time, if exists - start_query_time = last_run.get('last_fetch') - # Handle first time fetch, fetch incidents retroactively - if not start_query_time: - start_query_time, _ = parse_date_range(first_fetch_time, to_timestamp=True, utc=True) - start_query_time /= 1000 - alerts = client.fetch_alerts(start_query_time, demisto.params().get('alert_severity')) - for alert in alerts: - details = client.get_alert_details(alert['id']) - incident = { - 'name': 'Code42 - {}'.format(details['name']), - 'occurred': details['createdAt'], - } - if include_files: - details['fileevents'] = [] - for obs in details['observations']: - security_data_query = map_observation_to_security_query(obs, details['actor']) - file_events = client.search_json(security_data_query) - for event in file_events: - # We need to convert certain fields to a stringified list or React.JS will throw an error - if event.get('sharedWith'): - shared_list = [] - for shared_with in event['sharedWith']: - shared_list.append(shared_with['cloudUsername']) - event['sharedWith'] = str(shared_list) - if event.get('privateIpAddresses'): - event['privateIpAddresses'] = str(event['privateIpAddresses']) - details['fileevents'].append(event) - incident['rawJSON'] = json.dumps(details) - incidents.append(incident) - save_time = datetime.utcnow().timestamp() - next_run = {'last_fetch': save_time} - return next_run, incidents[:fetch_limit], incidents[fetch_limit:] + return_error(message="Could not remove user from Departing Employee List") + + +def _create_incident_from_alert_details(details): + return {"name": "Code42 - {}".format(details["name"]), "occurred": details["createdAt"]} + + +def _stringify_lists_if_needed(event): + # We need to convert certain fields to a stringified list or React.JS will throw an error + shared_with = event.get("sharedWith") + private_ip_addresses = event.get("privateIpAddresses") + if shared_with: + shared_list = [u["cloudUsername"] for u in shared_with] + event["sharedWith"] = str(shared_list) + if private_ip_addresses: + event["privateIpAddresses"] = str(private_ip_addresses) + + +def _process_event_from_observation(event): + _stringify_lists_if_needed(event) + return event + + +class Code42SecurityIncidentFetcher(object): + def __init__( + self, + client, + last_run, + first_fetch_time, + event_severity_filter, + fetch_limit, + include_files, + integration_context=None, + ): + self._client = client + self._last_run = last_run + self._first_fetch_time = first_fetch_time + self._event_severity_filter = event_severity_filter + self._fetch_limit = fetch_limit + self._include_files = (include_files,) + self._integration_context = integration_context + + @logger + def fetch(self): + remaining_incidents_from_last_run = self._fetch_remaining_incidents_from_last_run() + if remaining_incidents_from_last_run: + return remaining_incidents_from_last_run + start_query_time = self._get_start_query_time() + alerts = self._fetch_alerts(start_query_time) + incidents = [self._create_incident_from_alert(a) for a in alerts] + save_time = datetime.utcnow().timestamp() + next_run = {"last_fetch": save_time} + return next_run, incidents[: self._fetch_limit], incidents[self._fetch_limit:] + + def _fetch_remaining_incidents_from_last_run(self): + if self._integration_context: + remaining_incidents = self._integration_context.get("remaining_incidents") + # return incidents if exists in context. + if remaining_incidents: + return ( + self._last_run, + remaining_incidents[: self._fetch_limit], + remaining_incidents[self._fetch_limit:], + ) + + def _get_start_query_time(self): + start_query_time = self._try_get_last_fetch_time() + + # Handle first time fetch, fetch incidents retroactively + if not start_query_time: + start_query_time, _ = parse_date_range( + self._first_fetch_time, to_timestamp=True, utc=True + ) + start_query_time /= 1000 + + return start_query_time + + def _try_get_last_fetch_time(self): + return self._last_run.get("last_fetch") + + def _fetch_alerts(self, start_query_time): + return self._client.fetch_alerts(start_query_time, self._event_severity_filter) + + def _create_incident_from_alert(self, alert): + details = self._client.get_alert_details(alert["id"]) + incident = _create_incident_from_alert_details(details) + self._relate_files_to_alert(details) + incident["rawJSON"] = json.dumps(details) + return incident + + def _relate_files_to_alert(self, alert_details): + for obs in alert_details["observations"]: + file_events = self._get_file_events_from_alert_details(obs, alert_details) + alert_details["fileevents"] = [_process_event_from_observation(e) for e in file_events] + + def _get_file_events_from_alert_details(self, observation, alert_details): + security_data_query = map_observation_to_security_query(observation, alert_details["actor"]) + return self._client.search_json(security_data_query) + + +def fetch_incidents( + client, + last_run, + first_fetch_time, + event_severity_filter, + fetch_limit, + include_files, + integration_context=None, +): + fetcher = Code42SecurityIncidentFetcher( + client, + last_run, + first_fetch_time, + event_severity_filter, + fetch_limit, + include_files, + integration_context, + ) + return fetcher.fetch() @logger def securitydata_search_command(client, args): - code42_securitydata_context = [] + code42_security_data_context = [] + _json = args.get("json") file_context = [] # If JSON payload is passed as an argument, ignore all other args and search by JSON payload - if args.get('json') is not None: - file_events = client.search_json(args.get('json')) + if _json is not None: + file_events = client.search_json(_json) else: # Build payload payload = build_query_payload(args) @@ -441,83 +626,83 @@ def securitydata_search_command(client, args): if file_events: for file_event in file_events: code42_context_event = map_to_code42_event_context(file_event) - code42_securitydata_context.append(code42_context_event) + code42_security_data_context.append(code42_context_event) file_context_event = map_to_file_context(file_event) file_context.append(file_context_event) readable_outputs = tableToMarkdown( - f'Code42 Security Data Results', - code42_securitydata_context, - headers=SECURITY_EVENT_HEADERS + f"Code42 Security Data Results", + code42_security_data_context, + headers=SECURITY_EVENT_HEADERS, ) - return readable_outputs, {'Code42.SecurityData(val.EventID && val.EventID == obj.EventID)': code42_securitydata_context, - 'File': file_context}, file_events + security_data_context_key = "Code42.SecurityData(val.EventID && val.EventID == obj.EventID)" + context = {security_data_context_key: code42_security_data_context, "File": file_context} + return readable_outputs, context, file_events else: - return 'No results found', {}, {} + return "No results found", {}, {} def test_module(client): - user = client.get_current_user() - if user: - return 'ok' - else: - return 'Invalid credentials or host address. Check that the username and password are correct, \ - that the host is available and reachable, and that you have supplied the full scheme, \ - domain, and port (e.g. https://myhost.code42.com:4285)' + if client.get_current_user(): + return "ok" + return "Invalid credentials or host address. Check that the username and password are correct, \ + that the host is available and reachable, and that you have supplied the full scheme, \ + domain, and port (e.g. https://myhost.code42.com:4285)" def main(): """ PARSE AND VALIDATE INTEGRATION PARAMS """ - username = demisto.params().get('credentials').get('identifier') - password = demisto.params().get('credentials').get('password') - base_url = demisto.params().get('console_url') + username = demisto.params().get("credentials").get("identifier") + password = demisto.params().get("credentials").get("password") + base_url = demisto.params().get("console_url") # Remove trailing slash to prevent wrong URL path to service - verify_certificate = not demisto.params().get('insecure', False) - proxy = demisto.params().get('proxy', False) - LOG(f'Command being called is {demisto.command()}') + verify_certificate = not demisto.params().get("insecure", False) + proxy = demisto.params().get("proxy", False) + LOG(f"Command being called is {demisto.command()}") try: client = Code42Client( - sdk=SDK, base_url=base_url, + sdk=None, auth=(username, password), verify=verify_certificate, - proxy=proxy) + proxy=proxy, + ) commands = { - 'code42-alert-get': alert_get_command, - 'code42-alert-resolve': alert_resolve_command, - 'code42-securitydata-search': securitydata_search_command, - 'code42-departingemployee-add': departingemployee_add_command, - 'code42-departingemployee-remove': departingemployee_remove_command + "code42-alert-get": alert_get_command, + "code42-alert-resolve": alert_resolve_command, + "code42-securitydata-search": securitydata_search_command, + "code42-departingemployee-add": departingemployee_add_command, + "code42-departingemployee-remove": departingemployee_remove_command, } command = demisto.command() - if command == 'test-module': + if command == "test-module": # This is the call made when pressing the integration Test button. result = test_module(client) demisto.results(result) - elif command == 'fetch-incidents': + elif command == "fetch-incidents": integration_context = demisto.getIntegrationContext() # Set and define the fetch incidents command to run after activated via integration settings. next_run, incidents, remaining_incidents = fetch_incidents( client=client, last_run=demisto.getLastRun(), - first_fetch_time=demisto.params().get('fetch_time'), - event_severity_filter=demisto.params().get('alert_severity'), - fetch_limit=int(demisto.params().get('fetch_limit')), - include_files=demisto.params().get('include_files'), - integration_context=integration_context + first_fetch_time=demisto.params().get("fetch_time"), + event_severity_filter=demisto.params().get("alert_severity"), + fetch_limit=int(demisto.params().get("fetch_limit")), + include_files=demisto.params().get("include_files"), + integration_context=integration_context, ) demisto.setLastRun(next_run) demisto.incidents(incidents) # Store remaining incidents in integration context - integration_context['remaining_incidents'] = remaining_incidents + integration_context["remaining_incidents"] = remaining_incidents demisto.setIntegrationContext(integration_context) elif command in commands: return_outputs(*commands[command](client, demisto.args())) # Log exceptions except Exception as e: - return_error(f'Failed to execute {demisto.command()} command. Error: {str(e)}') + return_error(f"Failed to execute {demisto.command()} command. Error: {str(e)}") -if __name__ in ('__main__', '__builtin__', 'builtins'): +if __name__ in ("__main__", "__builtin__", "builtins"): main() diff --git a/Packs/Code42/Integrations/Code42/Code42.yml b/Packs/Code42/Integrations/Code42/Code42.yml index f70e7772b34..e114581bcdd 100644 --- a/Packs/Code42/Integrations/Code42/Code42.yml +++ b/Packs/Code42/Integrations/Code42/Code42.yml @@ -245,14 +245,14 @@ script: arguments: - name: username required: true - description: The username to add to the Departing Employee Lens. + description: The username to add to the Departing Employee List. - name: departuredate description: The departure date for the employee, in the format YYYY-MM-DD. - name: note description: Note to attach to the Departing Employee. outputs: - - contextPath: Code42.DepartingEmployee.CaseID - description: Internal Code42 Case ID for the Departing Employee. + - contextPath: Code42.DepartingEmployee.UserID + description: Internal Code42 User ID for the Departing Employee. type: string - contextPath: Code42.DepartingEmployee.Username description: The username of the Departing Employee. @@ -262,17 +262,17 @@ script: type: string - contextPath: Code42.DepartingEmployee.DepartureDate description: The departure date for the Departing Employee. - description: Adds a user to the Departing Employee Lens. + description: Adds a user to the Departing Employee List. - name: code42-departingemployee-remove arguments: - name: username - description: The username to remove from the Departing Employee Lens. + description: The username to remove from the Departing Employee List. outputs: - - contextPath: Code42.DepartingEmployee.CaseID - description: Internal Code42 Case ID for the Departing Employee. + - contextPath: Code42.DepartingEmployee.UserID + description: Internal Code42 User ID for the Departing Employee. - contextPath: Code42.DepartingEmployee.Username description: The username of the Departing Employee. - description: Removes a user from the Departing Employee Lens. + description: Removes a user from the Departing Employee List. - name: code42-alert-resolve arguments: - name: id @@ -284,7 +284,7 @@ script: description: The alert ID of the resolved alert. type: string description: Resolves a Code42 Security alert. - dockerimage: demisto/py42:1.0.0.6301 + dockerimage: demisto/py42:1.0.0.9242 isfetch: true runonce: false subtype: python3 diff --git a/Packs/Code42/Integrations/Code42/Code42_test.py b/Packs/Code42/Integrations/Code42/Code42_test.py index 85cd7e10cad..a02b98e779b 100644 --- a/Packs/Code42/Integrations/Code42/Code42_test.py +++ b/Packs/Code42/Integrations/Code42/Code42_test.py @@ -1,5 +1,8 @@ import json -from py42.sdk import SDK +import pytest +from requests import Response +from py42.sdk import SDKClient +from py42.response import Py42Response from Code42 import ( Code42Client, build_query_payload, @@ -12,469 +15,465 @@ departingemployee_add_command, departingemployee_remove_command, fetch_incidents, - securitydata_search_command + securitydata_search_command, ) import time MOCK_URL = "https://123-fake-api.com" -MOCK_SECURITYDATA_SEARCH_QUERY = { +MOCK_AUTH = ("123", "123") + +MOCK_FETCH_TIME = "24 hours" + +MOCK_SECURITY_DATA_SEARCH_QUERY = { "hash": "d41d8cd98f00b204e9800998ecf8427e", "hostname": "DESKTOP-0001", "username": "user3@example.com", "exposure": "ApplicationRead", - "results": 50 + "results": 50, } -MOCK_SECURITY_EVENT_RESPONSE = { - "fileEvents": [ +MOCK_SECURITY_EVENT_RESPONSE = """ +{ + "totalCount":3, + "fileEvents":[ { - "actor": None, - "cloudDriveId": None, - "createTimestamp": "2019-02-14T22:16:32.977Z", - "detectionSourceAlias": None, - "deviceUid": "902443375841117412", - "deviceUserName": "user1@example.com", - "directoryId": [ - ], - "domainName": "10.0.1.24", - "emailDlpPolicyNames": None, - "emailFrom": None, - "emailRecipients": None, - "emailSender": None, - "emailSubject": None, - "eventId": "0_39550347-381e-490e-8397-46629a0e7af6_902443373841117412_941153704842724615_952", - "eventTimestamp": "2019-10-02T16:57:31.990Z", - "eventType": "READ_BY_APP", - "exposure": [ - "ApplicationRead" - ], - "fileCategory": "IMAGE", - "fileId": None, - "fileName": "data.jpg", - "fileOwner": "user1", - "filePath": "C:/Users/user1/Pictures/", - "fileSize": 9875, - "fileType": "FILE", - "insertionTimestamp": "2019-02-14T22:22:06.126Z", - "md5Checksum": "8cfe4d76431ee20dd82fbd3778b6396f", - "modifyTimestamp": "2019-02-14T22:16:34.664Z", - "osHostName": "LAPTOP-012", - "privateIpAddresses": [ - "10.0.1.24", - "0:0:0:0:0:0:0:1", - "127.0.0.1", - "fe80:0:0:0:bd2b:9ac6:5b3a:b47f%eth0" - ], - "processName": "\\Device\\HarddiskVolume2\\Users\\user1\\AppData\\Local\\slack\\app-4.3.4\\slack.exe", - "processOwner": "user1", - "publicIpAddress": "126.18.85.1", - "removableMediaBusType": None, - "removableMediaCapacity": None, - "removableMediaMediaName": None, - "removableMediaName": None, - "removableMediaPartitionId": [ - ], - "removableMediaSerialNumber": None, - "removableMediaVendor": None, - "removableMediaVolumeName": [ - ], - "sha256Checksum": "adb54dbe1f8268ce39351bad43eddbc419a08e6db3bbf7eb7b601a5d88b8d03b", - "shared": None, - "sharedWith": [ - ], - "sharingTypeAdded": [ - ], - "source": "Endpoint", - "syncDestination": None, - "tabUrl": None, - "url": None, - "userUid": "902428473202285579", - "windowTitle": [ - "Slack | cats_omg | Sysadmin buddies" - ] + "eventId":"0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "eventType":"READ_BY_APP", + "eventTimestamp":"2020-05-28T12:46:39.838Z", + "insertionTimestamp":"2020-05-28T12:51:50.040Z", + "fieldErrors":[], + "filePath":"C:/Users/QA/Downloads/", + "fileName":"company_secrets.txt", + "fileType":"FILE", + "fileCategory":"IMAGE", + "fileCategoryByBytes":"Image", + "fileCategoryByExtension":"Image", + "fileSize":265122, + "fileOwner":"Test", + "md5Checksum":"9cea266b4e07974df1982ae3b9de92ce", + "sha256Checksum":"34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "createTimestamp":"2020-05-28T12:43:34.902Z", + "modifyTimestamp":"2020-05-28T12:43:35.105Z", + "deviceUserName":"test@example.com", + "osHostName":"HOSTNAME", + "domainName":"host.docker.internal", + "publicIpAddress":"162.222.47.183", + "privateIpAddresses":["172.20.128.36","127.0.0.1"], + "deviceUid":"935873453596901068", + "userUid":"912098363086307495", + "actor":null, + "directoryId":[], + "source":"Endpoint", + "url":null, + "shared":null, + "sharedWith":[], + "sharingTypeAdded":[], + "cloudDriveId":null, + "detectionSourceAlias":null, + "fileId":null, + "exposure":["ApplicationRead"], + "processOwner":"QA", + "processName":"chrome.exe", + "windowTitle":["Jira"], + "tabUrl":"example.com", + "removableMediaVendor":null, + "removableMediaName":null, + "removableMediaSerialNumber":null, + "removableMediaCapacity":null, + "removableMediaBusType":null, + "removableMediaMediaName":null, + "removableMediaVolumeName":[], + "removableMediaPartitionId":[], + "syncDestination":null, + "emailDlpPolicyNames":null, + "emailSubject":null, + "emailSender":null, + "emailFrom":null, + "emailRecipients":null, + "outsideActiveHours":false, + "mimeTypeByBytes":"image/png", + "mimeTypeByExtension":"image/png", + "mimeTypeMismatch":false, + "printJobName":null, + "printerName":null, + "printedFilesBackupPath":null, + "remoteActivity":"UNKNOWN", + "trusted":false }, { - "actor": "user2@example.com", - "cloudDriveId": "0BUjUO34z2CQnUk9PVA", - "createTimestamp": "2019-09-02T19:55:26.389Z", - "detectionSourceAlias": "Google Drive", - "deviceUid": None, - "deviceUserName": "NAME_NOT_AVAILABLE", - "directoryId": [ - "0BUjQW60z2RMnUk9CFE" - ], - "domainName": None, - "emailDlpPolicyNames": None, - "emailFrom": None, - "emailRecipients": None, - "emailSender": None, - "emailSubject": None, - "eventId": "0798_6go4TW7QFQxF5UuBdCFddpX7ZbB9_1_13b87573-f82f-47aa-891c-6966f6e4ec54", - "eventTimestamp": "2019-10-02T15:00:09.745Z", - "eventType": "CREATED", - "exposure": [], - "fileCategory": "IMAGE", - "fileId": "0798_6go4TW7QFQxF5UuBdCFddpX7ZbB9", - "fileName": "Kitties", - "fileOwner": "user2@example.com", - "filePath": None, - "fileSize": 333114, - "fileType": "FILE", - "insertionTimestamp": "2020-10-02T15:02:18.390Z", - "md5Checksum": "eef8b12d2ed0d6a69fe77699d5640c7b", - "modifyTimestamp": "2019-10-02T14:55:26.389Z", - "osHostName": None, - "privateIpAddresses": [], - "processName": None, - "processOwner": None, - "publicIpAddress": None, - "removableMediaBusType": None, - "removableMediaCapacity": None, - "removableMediaMediaName": None, - "removableMediaName": None, - "removableMediaPartitionId": [], - "removableMediaSerialNumber": None, - "removableMediaVendor": None, - "removableMediaVolumeName": [], - "sha256Checksum": "5e25e54e1cc43ed07c6e888464cb98e5f5343aa7aa485d174d9649be780a17b9", - "shared": "FALSE", - "sharedWith": [], - "sharingTypeAdded": [], - "source": "GoogleDrive", - "syncDestination": None, - "tabUrl": None, - "url": "https://drive.google.com/a/c42se.com/file/d/1tm4_6go4TW7QFQxF5UuBdCFddpX7ZbB9/view?usp=drivesdk", - "userUid": "UNKNOWN", - "windowTitle": [] + "eventId":"0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "eventType":"READ_BY_APP", + "eventTimestamp":"2020-05-28T12:46:39.838Z", + "insertionTimestamp":"2020-05-28T12:51:50.040Z", + "fieldErrors":[], + "filePath":"C:/Users/QA/Downloads/", + "fileName":"data.jpg", + "fileType":"FILE", + "fileCategory":"IMAGE", + "fileCategoryByBytes":"Image", + "fileCategoryByExtension":"Image", + "fileSize":265122, + "fileOwner":"QA", + "md5Checksum":"9cea266b4e07974df1982ae3b9de92ce", + "sha256Checksum":"34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "createTimestamp":"2020-05-28T12:43:34.902Z", + "modifyTimestamp":"2020-05-28T12:43:35.105Z", + "deviceUserName":"test@example.com", + "osHostName":"TEST'S MAC", + "domainName":"host.docker.internal", + "publicIpAddress":"162.222.47.183", + "privateIpAddresses":["127.0.0.1"], + "deviceUid":"935873453596901068", + "userUid":"912098363086307495", + "actor":null, + "directoryId":[], + "source":"Endpoint", + "url":null, + "shared":null, + "sharedWith":[], + "sharingTypeAdded":[], + "cloudDriveId":null, + "detectionSourceAlias":null, + "fileId":null, + "exposure":["ApplicationRead"], + "processOwner":"QA", + "processName":"chrome.exe", + "windowTitle":["Jira"], + "tabUrl":"example.com/test", + "removableMediaVendor":null, + "removableMediaName":null, + "removableMediaSerialNumber":null, + "removableMediaCapacity":null, + "removableMediaBusType":null, + "removableMediaMediaName":null, + "removableMediaVolumeName":[], + "removableMediaPartitionId":[], + "syncDestination":null, + "emailDlpPolicyNames":null, + "emailSubject":null, + "emailSender":null, + "emailFrom":null, + "emailRecipients":null, + "outsideActiveHours":false, + "mimeTypeByBytes":"image/png", + "mimeTypeByExtension":"image/png", + "mimeTypeMismatch":false, + "printJobName":null, + "printerName":null, + "printedFilesBackupPath":null, + "remoteActivity":"UNKNOWN", + "trusted":false }, { - "actor": None, - "cloudDriveId": None, - "createTimestamp": "2019-08-10T22:22:07.460Z", - "detectionSourceAlias": None, - "deviceUid": "920258207244664650", - "deviceUserName": "user3@example.com", - "directoryId": [ - ], - "domainName": "USER3-DEMO01", - "emailDlpPolicyNames": None, - "emailFrom": None, - "emailRecipients": None, - "emailSender": None, - "emailSubject": None, - "eventId": "0_25e609cd-ccee-4b40-ba62-165f312ed8f4_920258207243264650_940574180289182590_4", - "eventTimestamp": "2019-10-02T16:55:08.772Z", - "eventType": "MODIFIED", - "exposure": [ - "RemovableMedia" - ], - "fileCategory": "PDF", - "fileId": None, - "fileName": "Blueprints.pdf", - "fileOwner": "Everyone", - "filePath": "F:/", - "fileSize": 946814, - "fileType": "FILE", - "insertionTimestamp": "2019-10-02T17:00:01.806Z", - "md5Checksum": "f61e05de73f798b9f43c11b299653894", - "modifyTimestamp": "2019-09-10T22:22:08Z", - "osHostName": "USER3-DEMO01", - "privateIpAddresses": [ - "0:0:0:0:0:0:0:1", - "127.0.0.1", - "172.16.1.1" - ], - "processName": None, - "processOwner": None, - "publicIpAddress": "8.8.14.14", - "removableMediaBusType": "USB", - "removableMediaCapacity": 30751588352, - "removableMediaMediaName": "SanDisk Ultra USB 3.0 Media", - "removableMediaName": "Ultra USB 3.0", - "removableMediaPartitionId": [ - "5b0acc46-0000-0000-0000-100000000000" - ], - "removableMediaSerialNumber": "4C532378360368700544", - "removableMediaVendor": "SanDisk", - "removableMediaVolumeName": [ - "DIGI (F:)" - ], - "sha256Checksum": "92e5bcca6b7d2c081e4169ee293098a76d5887081b6db33b841ab6440dfc08a0", - "shared": None, - "sharedWith": [ - ], - "sharingTypeAdded": [ - ], - "source": "Endpoint", - "syncDestination": None, - "tabUrl": None, - "url": None, - "userUid": "920256648733700844", - "windowTitle": [ - ] + "eventId":"0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "eventType":"READ_BY_APP", + "eventTimestamp":"2020-05-28T12:46:39.838Z", + "insertionTimestamp":"2020-05-28T12:51:50.040Z", + "fieldErrors":[], + "filePath":"C:/Users/QA/Downloads/", + "fileName":"confidential.pdf", + "fileType":"FILE", + "fileCategory":"IMAGE", + "fileCategoryByBytes":"Image", + "fileCategoryByExtension":"Image", + "fileSize":265122, + "fileOwner":"Mock", + "md5Checksum":"9cea266b4e07974df1982ae3b9de92ce", + "sha256Checksum":"34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "createTimestamp":"2020-05-28T12:43:34.902Z", + "modifyTimestamp":"2020-05-28T12:43:35.105Z", + "deviceUserName":"test@example.com", + "osHostName":"Test's Windows", + "domainName":"host.docker.internal", + "publicIpAddress":"162.222.47.183", + "privateIpAddresses":["0:0:0:0:0:0:0:1","127.0.0.1"], + "deviceUid":"935873453596901068", + "userUid":"912098363086307495", + "actor":null, + "directoryId":[], + "source":"Endpoint", + "url":null, + "shared":null, + "sharedWith":[], + "sharingTypeAdded":[], + "cloudDriveId":null, + "detectionSourceAlias":null, + "fileId":null, + "exposure":["ApplicationRead"], + "processOwner":"QA", + "processName":"chrome.exe", + "windowTitle":["Jira"], + "tabUrl":"example.com/foo", + "removableMediaVendor":null, + "removableMediaName":null, + "removableMediaSerialNumber":null, + "removableMediaCapacity":null, + "removableMediaBusType":null, + "removableMediaMediaName":null, + "removableMediaVolumeName":[], + "removableMediaPartitionId":[], + "syncDestination":null, + "emailDlpPolicyNames":null, + "emailSubject":null, + "emailSender":null, + "emailFrom":null, + "emailRecipients":null, + "outsideActiveHours":false, + "mimeTypeByBytes":"image/png", + "mimeTypeByExtension":"image/png", + "mimeTypeMismatch":false, + "printJobName":null, + "printerName":null, + "printedFilesBackupPath":null, + "remoteActivity":"UNKNOWN", + "trusted":false } ] } +""" MOCK_CODE42_EVENT_CONTEXT = [ { - "DevicePrivateIPAddress": ["10.0.1.24", - "0:0:0:0:0:0:0:1", - "127.0.0.1", - "fe80:0:0:0:bd2b:9ac6:5b3a:b47f%eth0"], - "DeviceUsername": "user1@example.com", - "EndpointID": "902443375841117412", - "EventID": "0_39550347-381e-490e-8397-46629a0e7af6_902443373841117412_941153704842724615_952", - "EventTimestamp": "2019-10-02T16:57:31.990Z", + "ApplicationTabURL": "example.com", + "DevicePrivateIPAddress": ["172.20.128.36", "127.0.0.1"], + "DeviceUsername": "test@example.com", + "EndpointID": "935873453596901068", + "EventID": "0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "EventTimestamp": "2020-05-28T12:46:39.838Z", "EventType": "READ_BY_APP", "Exposure": ["ApplicationRead"], "FileCategory": "IMAGE", - "FileCreated": "2019-02-14T22:16:32.977Z", - "FileHostname": "LAPTOP-012", - "FileMD5": "8cfe4d76431ee20dd82fbd3778b6396f", - "FileModified": "2019-02-14T22:16:34.664Z", - "FileName": "data.jpg", - "FileOwner": "user1", - "FilePath": "C:/Users/user1/Pictures/", - "FileSHA256": "adb54dbe1f8268ce39351bad43eddbc419a08e6db3bbf7eb7b601a5d88b8d03b", - "FileSize": 9875, - "ProcessName": "\\Device\\HarddiskVolume2\\Users\\user1\\AppData\\Local\\slack\\app-4.3.4\\slack.exe", - "ProcessOwner": "user1", + "FileCreated": "2020-05-28T12:43:34.902Z", + "FileHostname": "HOSTNAME", + "FileMD5": "9cea266b4e07974df1982ae3b9de92ce", + "FileModified": "2020-05-28T12:43:35.105Z", + "FileName": "company_secrets.txt", + "FileOwner": "Test", + "FilePath": "C:/Users/QA/Downloads/", + "FileSHA256": "34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "FileSize": 265122, + "ProcessName": "chrome.exe", + "ProcessOwner": "QA", "Source": "Endpoint", - "WindowTitle": ["Slack | cats_omg | Sysadmin buddies"] + "WindowTitle": ["Jira"], }, { - "DeviceUsername": "NAME_NOT_AVAILABLE", - "EventID": "0798_6go4TW7QFQxF5UuBdCFddpX7ZbB9_1_13b87573-f82f-47aa-891c-6966f6e4ec54", - "EventTimestamp": "2019-10-02T15:00:09.745Z", - "EventType": "CREATED", + "ApplicationTabURL": "example.com/test", + "DevicePrivateIPAddress": ["127.0.0.1"], + "DeviceUsername": "test@example.com", + "EndpointID": "935873453596901068", + "EventID": "0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "EventTimestamp": "2020-05-28T12:46:39.838Z", + "EventType": "READ_BY_APP", + "Exposure": ["ApplicationRead"], "FileCategory": "IMAGE", - "FileCreated": "2019-09-02T19:55:26.389Z", - "FileMD5": "eef8b12d2ed0d6a69fe77699d5640c7b", - "FileModified": "2019-10-02T14:55:26.389Z", - "FileName": "Kitties", - "FileOwner": "user2@example.com", - "FileSHA256": "5e25e54e1cc43ed07c6e888464cb98e5f5343aa7aa485d174d9649be780a17b9", - "FileShared": "FALSE", - "FileSize": 333114, - "FileURL": "https://drive.google.com/a/c42se.com/file/d/1tm4_6go4TW7QFQxF5UuBdCFddpX7ZbB9/view?usp=drivesdk", - "Source": "GoogleDrive" + "FileCreated": "2020-05-28T12:43:34.902Z", + "FileHostname": "TEST'S MAC", + "FileMD5": "9cea266b4e07974df1982ae3b9de92ce", + "FileModified": "2020-05-28T12:43:35.105Z", + "FileName": "data.jpg", + "FileOwner": "QA", + "FilePath": "C:/Users/QA/Downloads/", + "FileSHA256": "34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "FileSize": 265122, + "ProcessName": "chrome.exe", + "ProcessOwner": "QA", + "Source": "Endpoint", + "WindowTitle": ["Jira"], }, { - "DevicePrivateIPAddress": ["0:0:0:0:0:0:0:1", "127.0.0.1", "172.16.1.1"], - "DeviceUsername": "user3@example.com", - "EndpointID": "920258207244664650", - "EventID": "0_25e609cd-ccee-4b40-ba62-165f312ed8f4_920258207243264650_940574180289182590_4", - "EventTimestamp": "2019-10-02T16:55:08.772Z", - "EventType": "MODIFIED", - "Exposure": ["RemovableMedia"], - "FileCategory": "PDF", - "FileCreated": "2019-08-10T22:22:07.460Z", - "FileHostname": "USER3-DEMO01", - "FileMD5": "f61e05de73f798b9f43c11b299653894", - "FileModified": "2019-09-10T22:22:08Z", - "FileName": "Blueprints.pdf", - "FileOwner": "Everyone", - "FilePath": "F:/", - "FileSHA256": "92e5bcca6b7d2c081e4169ee293098a76d5887081b6db33b841ab6440dfc08a0", - "FileSize": 946814, - "RemovableMediaCapacity": 30751588352, - "RemovableMediaMediaName": "SanDisk Ultra USB 3.0 Media", - "RemovableMediaName": "Ultra USB 3.0", - "RemovableMediaSerialNumber": "4C532378360368700544", - "RemovableMediaType": "USB", - "RemovableMediaVendor": "SanDisk", - "Source": "Endpoint" - } + "ApplicationTabURL": "example.com/foo", + "DevicePrivateIPAddress": ["0:0:0:0:0:0:0:1", "127.0.0.1"], + "DeviceUsername": "test@example.com", + "EndpointID": "935873453596901068", + "EventID": "0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "EventTimestamp": "2020-05-28T12:46:39.838Z", + "EventType": "READ_BY_APP", + "Exposure": ["ApplicationRead"], + "FileCategory": "IMAGE", + "FileCreated": "2020-05-28T12:43:34.902Z", + "FileHostname": "Test's Windows", + "FileMD5": "9cea266b4e07974df1982ae3b9de92ce", + "FileModified": "2020-05-28T12:43:35.105Z", + "FileName": "confidential.pdf", + "FileOwner": "Mock", + "FilePath": "C:/Users/QA/Downloads/", + "FileSHA256": "34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "FileSize": 265122, + "ProcessName": "chrome.exe", + "ProcessOwner": "QA", + "Source": "Endpoint", + "WindowTitle": ["Jira"], + }, ] MOCK_FILE_CONTEXT = [ { - "Hostname": "LAPTOP-012", - "MD5": "8cfe4d76431ee20dd82fbd3778b6396f", - "Name": "data.jpg", - "Path": "C:/Users/user1/Pictures/", - "SHA256": "adb54dbe1f8268ce39351bad43eddbc419a08e6db3bbf7eb7b601a5d88b8d03b", - "Size": 9875 + "Hostname": "HOSTNAME", + "MD5": "9cea266b4e07974df1982ae3b9de92ce", + "Name": "company_secrets.txt", + "Path": "C:/Users/QA/Downloads/", + "SHA256": "34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "Size": 265122, }, { - "MD5": "eef8b12d2ed0d6a69fe77699d5640c7b", - "Name": "Kitties", - "SHA256": "5e25e54e1cc43ed07c6e888464cb98e5f5343aa7aa485d174d9649be780a17b9", - "Size": 333114 + "Hostname": "TEST'S MAC", + "MD5": "9cea266b4e07974df1982ae3b9de92ce", + "Name": "data.jpg", + "Path": "C:/Users/QA/Downloads/", + "SHA256": "34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "Size": 265122, }, { - "Hostname": "USER3-DEMO01", - "MD5": "f61e05de73f798b9f43c11b299653894", - "Name": "Blueprints.pdf", - "Path": "F:/", - "SHA256": "92e5bcca6b7d2c081e4169ee293098a76d5887081b6db33b841ab6440dfc08a0", - "Size": 946814 - } + "Hostname": "Test's Windows", + "MD5": "9cea266b4e07974df1982ae3b9de92ce", + "Name": "confidential.pdf", + "Path": "C:/Users/QA/Downloads/", + "SHA256": "34d0c9fc9c907ec374cf7e8ca1ff8a172e36eccee687f0a9b69dd169debb81e1", + "Size": 265122, + }, ] -MOCK_ALERT_RESPONSE = { - "alerts": [ - { - "actor": "user1@example.com", - "createdAt": "2019-10-02T17:02:23.5867670Z", - "description": "", - "id": "36fb8ca5-0533-4d25-9763-e09d35d60610", - "name": "Departing Employee Alert", - "severity": "HIGH", - "state": "OPEN", - "target": "N/A", - "tenantId": "fef27d1d-e835-465c-be8f-ac9db7a54684", - "type": "FED_ENDPOINT_EXFILTRATION", - "type$": "ALERT_SUMMARY" - }, - { - "actor": "user2@example.com", - "createdAt": "2019-10-02T17:02:24.2071980Z", - "description": "", - "id": "18ac641d-7d9c-4d37-a48f-c89396c07d03", - "name": "High-Risk Employee Alert", - "severity": "MEDIUM", - "state": "OPEN", - "target": "N/A", - "tenantId": "fef27d1d-e835-465c-be8f-ac9db7a54684", - "type": "FED_CLOUD_SHARE_PERMISSIONS", - "type$": "ALERT_SUMMARY" - }, - { - "actor": "user3@exmaple.com", - "createdAt": "2019-10-02T17:03:28.2885720Z", - "description": "", - "id": "3137ff1b-b824-42e4-a476-22bccdd8ddb8", - "name": "Custom Alert 1", - "severity": "LOW", - "state": "OPEN", - "target": "N/A", - "tenantId": "fef27d1d-e835-465c-be8f-ac9db7a54684", - "type": "FED_ENDPOINT_EXFILTRATION", - "type$": "ALERT_SUMMARY" - } - ], - "type$": "ALERT_QUERY_RESPONSE" -} - -MOCK_ALERT_DETAILS_RESPONSE = [ +MOCK_ALERTS_RESPONSE = """{ + "type$": "ALERT_QUERY_RESPONSE", + "alerts": [ { - "alerts": [ - { - "actor": "user1@example.com", - "createdAt": "2019-10-02T17:02:23.5867670Z", - "description": "", - "id": "36fb8ca5-0533-4d25-9763-e09d35d60610", - "name": "Departing Employee Alert", - "notes": "Departing Employee Notes", - "observations": [ - { - "data": r"""{"type$":"OBSERVED_ENDPOINT_ACTIVITY","id":"e940d9de-bd73-4665-8b3c-196aca6b8a53", - "sources":["Endpoint"],"exposureTypes":["ApplicationRead"],"firstActivityAt": - "2019-10-02T16:50:00.0000000Z","lastActivityAt":"2019-10-02T16:55:00.0000000Z", - "fileCount":7,"totalFileSize":66119,"fileCategories":[{"type$":"OBSERVED_FILE_CATEGORY", - "category":"SourceCode","fileCount":7, - "totalFileSize":66119,"isSignificant":false}],"syncToServices":[]}""", - "id": "e940d9de-bd73-4665-8b3c-196aca6b8a53", - "observedAt": "2019-10-02T17:00:00.0000000Z", - "type": "FedEndpointExfiltration", - "type$": "OBSERVATION" - } - ], - "ruleId": "c4404ee8-503c-4a21-98f5-37561ee4caf0", - "ruleSource": "Departing Employee", - "severity": "HIGH", - "state": "OPEN", - "target": "N/A", - "tenantId": "fef27d1d-e835-465c-be8f-ac9db7a54684", - "type": "FED_ENDPOINT_EXFILTRATION", - "type$": "ALERT_SUMMARY" - } - ] + "type$": "ALERT_SUMMARY", + "tenantId": "1d700000-af5b-4231-9d8e-df6434d00000", + "type": "FED_ENDPOINT_EXFILTRATION", + "name": "Exposure on an endpoint", + "description": "This default rule alerts you when departing employees move data from an endpoint.", + "actor": "test.testerson@example.com", + "target": "N/A", + "severity": "HIGH", + "ruleId": "9befe477-3487-40b7-89a6-bbcced4cf1fe", + "ruleSource": "Departing Employee", + "id": "fbeaabc1-9205-4620-ad53-95d0633429a3", + "createdAt": "2020-05-04T20:46:45.8106280Z", + "state": "OPEN" }, { - "alerts": [ - { - "actor": "user2@example.com", - "createdAt": "2019-10-02T17:02:24.2071980Z", - "description": "", - "id": "18ac641d-7d9c-4d37-a48f-c89396c07d03", - "name": "High-Risk Employee Alert", - "notes": "High-Risk Employee Notes", - "observations": [ - { - "data": r"""{"type$":"OBSERVED_CLOUD_SHARE_ACTIVITY","id":"495fb9a2-ab18-4b62-98bd-c141ed776de5", - "sources":["GoogleDrive"],"exposureTypes":["PublicSearchableShare","PublicLinkShare"], - "firstActivityAt":"2019-10-02T16:50:00.0000000Z","lastActivityAt": - "2019-10-02T16:55:00.0000000Z","fileCount":1,"totalFileSize":8089, - "fileCategories":[{"type$":"OBSERVED_FILE_CATEGORY", - "category":"Document","fileCount":1,"totalFileSize":8089,"isSignificant":false}]}""", - "id": "495fb9a2-ab18-4b62-98bd-c141ed776de5", - "observedAt": "2019-10-02T16:57:00.0000000Z", - "type": "FedCloudSharePermissions", - "type$": "OBSERVATION" - } - ], - "ruleId": "c4404ee8-503c-4a21-98f5-37561ee4caf0", - "ruleSource": "High-Risk Employee", - "severity": "MEDIUM", - "state": "OPEN", - "target": "N/A", - "tenantId": "fef27d1d-e835-465c-be8f-ac9db7a54684", - "type": "FED_CLOUD_SHARE_PERMISSIONS", - "type$": "ALERT_SUMMARY" - } - ] + "type$": "ALERT_SUMMARY", + "tenantId": "1d700000-af5b-4231-9d8e-df6434d00000", + "type": "FED_ENDPOINT_EXFILTRATION", + "name": "Exposure on an endpoint", + "description": "This default rule alerts you when departing employees move data from an endpoint.", + "actor": "test.testerson@example.com", + "target": "N/A", + "severity": "LOW", + "ruleId": "9befe477-3487-40b7-89a6-bbcced4cf1fe", + "ruleSource": "Departing Employee", + "id": "6bb7ca1e-c8cf-447d-a732-9652869e42d0", + "createdAt": "2020-05-04T20:35:54.2400240Z", + "state": "OPEN" }, { - "alerts": [ - { - "actor": "user3@example.com", - "createdAt": "2019-10-02T17:03:28.2885720Z", - "description": "", - "id": "3137ff1b-b824-42e4-a476-22bccdd8ddb8", - "name": "Custom Alert 1", - "notes": "Removable Media Alert", - "observations": [ - { - "data": r"""{"type$":"OBSERVED_ENDPOINT_ACTIVITY","id":"a1fac38d-4816-4090-bf1c-9c429f6265f0", - "sources":["Endpoint"],"exposureTypes":["RemovableMedia"],"firstActivityAt": - "2019-10-02T16:45:00.0000000Z","lastActivityAt":"2019-10-02T16:50:00.0000000Z","fileCount":4, - "totalFileSize":997653,"fileCategories":[{"type$":"OBSERVED_FILE_CATEGORY", - "category":"Document","fileCount":2,"totalFileSize":50839,"isSignificant":false}, - {"type$":"OBSERVED_FILE_CATEGORY","category":"Pdf","fileCount":2, - "totalFileSize":946814,"isSignificant":false}],"syncToServices":[]}""", - "id": "a1fac38d-4816-4090-bf1c-9c429f6265f0", - "observedAt": "2019-10-02T17:00:00.0000000Z", - "type": "FedEndpointExfiltration", - "type$": "OBSERVATION" - } - ], - "ruleId": "dcee3e9c-c914-424a-bb23-bb60f0ae9f0f", - "ruleSource": "Alerting", - "severity": "LOW", - "state": "OPEN", - "target": "N/A", - "tenantId": "fef27d1d-e835-465c-be8f-ac9db7a54684", - "type": "FED_ENDPOINT_EXFILTRATION", - "type$": "ALERT_SUMMARY" - } - ] + "type$": "ALERT_SUMMARY", + "tenantId": "1d700000-af5b-4231-9d8e-df6434d00000", + "type": "FED_ENDPOINT_EXFILTRATION", + "name": "Exposure on an endpoint", + "description": "This default rule alerts you when departing employees move data from an endpoint.", + "actor": "test.testerson@example.com", + "target": "N/A", + "severity": "HIGH", + "ruleId": "9befe477-3487-40b7-89a6-bbcced4cf1fe", + "ruleSource": "Departing Employee", + "id": "c2c3aef3-8fd9-4e7a-a04e-16bec9e27625", + "createdAt": "2020-05-04T20:19:34.7121300Z", + "state": "OPEN" } -] + ], + "totalCount": 3, + "problems": [] +}""" +MOCK_ALERT_DETAILS_RESPONSE = """{ + "type$": "ALERT_DETAILS_RESPONSE", + "alerts": [ + {"type$": "ALERT_DETAILS", + "tenantId": "1d71796f-af5b-4231-9d8e-df6434da4663", + "type": "FED_ENDPOINT_EXFILTRATION", + "name": "Departing Employee Alert", + "description": "Cortex XSOAR is cool.", + "actor": "user1@example.com", + "actorId": "912098363086307495", + "target": "N/A", + "severity": "HIGH", + "ruleId": "4576576e-13cb-4f88-be3a-ee77739de649", + "ruleSource": "Alerting", + "id": "36fb8ca5-0533-4d25-9763-e09d35d60610", + "createdAt": "2019-10-02T17:02:23.5867670Z", + "state": "OPEN", + "observations": [ + { + "type$": "OBSERVATION", + "id": "240526fc-3a32-4755-85ab-c6ee6e7f31ce", + "observedAt": "2020-05-28T12:50:00.0000000Z", + "type": "FedEndpointExfiltration", + "data": { + "type$": "OBSERVED_ENDPOINT_ACTIVITY", + "id": "240526fc-3a32-4755-85ab-c6ee6e7f31ce", + "sources": ["Endpoint"], + "exposureTypes": ["ApplicationRead"], + "firstActivityAt": "2020-05-28T12:50:00.0000000Z", + "lastActivityAt": "2020-05-28T12:50:00.0000000Z", + "fileCount": 3, + "totalFileSize": 533846, + "fileCategories": [ + { + "type$": "OBSERVED_FILE_CATEGORY", + "category": "Image", + "fileCount": 3, + "totalFileSize": 533846, + "isSignificant": true + } + ], + "files": [ + { + "type$": "OBSERVED_FILE", + "eventId": "0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_5", + "path": "C:/Users/QA/Downloads/", + "name": "Customers.jpg", + "category": "Image", + "size": 265122 + }, + { + "type$": "OBSERVED_FILE", + "eventId": "0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_6", + "path": "C:/Users/QA/Downloads/", + "name": "data.png", + "category": "Image", + "size": 129129 + }, + { + "type$": "OBSERVED_FILE", + "eventId": "0_1d71796f-af5b-4231-9d8e-df6434da4663_935873453596901068_956171635867906205_7", + "path": "C:/Users/QA/Downloads/", + "name": "company_secrets.ps", + "category": "Image", + "size": 139595 + } + ], + "syncToServices": [], + "sendingIpAddresses": ["127.0.0.1"] + } + } + ] + } + ] +}""" MOCK_CODE42_ALERT_CONTEXT = [ { "ID": "36fb8ca5-0533-4d25-9763-e09d35d60610", "Name": "Departing Employee Alert", + "Description": "Cortex XSOAR is cool.", "Occurred": "2019-10-02T17:02:23.5867670Z", "Severity": "HIGH", "State": "OPEN", "Type": "FED_ENDPOINT_EXFILTRATION", - "Username": "user1@example.com" + "Username": "user1@example.com", }, { "ID": "18ac641d-7d9c-4d37-a48f-c89396c07d03", @@ -483,7 +482,7 @@ "Severity": "MEDIUM", "State": "OPEN", "Type": "FED_CLOUD_SHARE_PERMISSIONS", - "Username": "user2@example.com" + "Username": "user2@example.com", }, { "ID": "3137ff1b-b824-42e4-a476-22bccdd8ddb8", @@ -492,11 +491,11 @@ "Severity": "LOW", "State": "OPEN", "Type": "FED_ENDPOINT_EXFILTRATION", - "Username": "user3@example.com" - } + "Username": "user3@example.com", + }, ] -MOCK_QUERY_PAYLOAD = { +MOCK_FILE_EVENT_QUERY_PAYLOAD = { "groupClause": "AND", "groups": [ { @@ -505,45 +504,27 @@ { "operator": "IS", "term": "md5Checksum", - "value": "d41d8cd98f00b204e9800998ecf8427e" + "value": "d41d8cd98f00b204e9800998ecf8427e", } - ] + ], }, { "filterClause": "AND", - "filters": [ - { - "operator": "IS", - "term": "osHostName", - "value": "DESKTOP-0001" - } - ] + "filters": [{"operator": "IS", "term": "osHostName", "value": "DESKTOP-0001"}], }, { "filterClause": "AND", - "filters": [ - { - "operator": "IS", - "term": "deviceUserName", - "value": "user3@example.com" - } - ] + "filters": [{"operator": "IS", "term": "deviceUserName", "value": "user3@example.com"}], }, { "filterClause": "AND", - "filters": [ - { - "operator": "IS", - "term": "exposure", - "value": "ApplicationRead" - } - ] - } + "filters": [{"operator": "IS", "term": "exposure", "value": "ApplicationRead"}], + }, ], "pgNum": 1, "pgSize": 50, "srtDir": "asc", - "srtKey": "eventId" + "srtKey": "eventId", } MOCK_OBSERVATION_QUERIES = [ @@ -553,12 +534,8 @@ { "filterClause": "AND", "filters": [ - { - "operator": "IS", - "term": "deviceUserName", - "value": "user1@example.com" - } - ] + {"operator": "IS", "term": "deviceUserName", "value": "user1@example.com"} + ], }, { "filterClause": "AND", @@ -566,9 +543,9 @@ { "operator": "ON_OR_AFTER", "term": "eventTimestamp", - "value": "2019-10-02T16:50:00.000Z" + "value": "2020-05-28T12:50:00.000Z", } - ] + ], }, { "filterClause": "AND", @@ -576,58 +553,44 @@ { "operator": "ON_OR_BEFORE", "term": "eventTimestamp", - "value": "2019-10-02T16:55:00.000Z" + "value": "2020-05-28T12:50:00.000Z", } - ] + ], }, { "filterClause": "OR", "filters": [ - { - "operator": "IS", - "term": "eventType", - "value": "CREATED" - }, - { - "operator": "IS", - "term": "eventType", - "value": "MODIFIED" - }, - { - "operator": "IS", - "term": "eventType", - "value": "READ_BY_APP" - } - ] + {"operator": "IS", "term": "eventType", "value": "CREATED"}, + {"operator": "IS", "term": "eventType", "value": "MODIFIED"}, + {"operator": "IS", "term": "eventType", "value": "READ_BY_APP"}, + ], + }, + { + "filterClause": "AND", + "filters": [{"operator": "IS", "term": "exposure", "value": "ApplicationRead"}], }, { "filterClause": "AND", "filters": [ { "operator": "IS", - "term": "exposure", - "value": "ApplicationRead" + "term": "fileCategory", + "value": "IMAGE" } ] - }, + } ], "pgNum": 1, - "pgSize": 100, + "pgSize": 10000, "srtDir": "asc", - "srtKey": "eventId" + "srtKey": "eventId", }, { "groupClause": "AND", "groups": [ { "filterClause": "AND", - "filters": [ - { - "operator": "IS", - "term": "actor", - "value": "user2@example.com" - } - ] + "filters": [{"operator": "IS", "term": "actor", "value": "user2@example.com"}], }, { "filterClause": "AND", @@ -635,9 +598,9 @@ { "operator": "ON_OR_AFTER", "term": "eventTimestamp", - "value": "2019-10-02T16:50:00.000Z" + "value": "2019-10-02T16:50:00.000Z", } - ] + ], }, { "filterClause": "AND", @@ -645,30 +608,22 @@ { "operator": "ON_OR_BEFORE", "term": "eventTimestamp", - "value": "2019-10-02T16:55:00.000Z" + "value": "2019-10-02T16:55:00.000Z", } - ] + ], }, { "filterClause": "OR", "filters": [ - { - "operator": "IS", - "term": "exposure", - "value": "IsPublic" - }, - { - "operator": "IS", - "term": "exposure", - "value": "SharedViaLink" - } - ] + {"operator": "IS", "term": "exposure", "value": "IsPublic"}, + {"operator": "IS", "term": "exposure", "value": "SharedViaLink"}, + ], } ], "pgNum": 1, - "pgSize": 100, + "pgSize": 10000, "srtDir": "asc", - "srtKey": "eventId" + "srtKey": "eventId", }, { "groupClause": "AND", @@ -676,12 +631,8 @@ { "filterClause": "AND", "filters": [ - { - "operator": "IS", - "term": "deviceUserName", - "value": "user3@example.com" - } - ] + {"operator": "IS", "term": "deviceUserName", "value": "user3@example.com"} + ], }, { "filterClause": "AND", @@ -689,9 +640,9 @@ { "operator": "ON_OR_AFTER", "term": "eventTimestamp", - "value": "2019-10-02T16:45:00.000Z" + "value": "2019-10-02T16:50:00.000Z", } - ] + ], }, { "filterClause": "AND", @@ -699,268 +650,298 @@ { "operator": "ON_OR_BEFORE", "term": "eventTimestamp", - "value": "2019-10-02T16:50:00.000Z" + "value": "2019-10-02T16:50:00.000Z", } - ] + ], }, { "filterClause": "OR", "filters": [ - { - "operator": "IS", - "term": "eventType", - "value": "CREATED" - }, - { - "operator": "IS", - "term": "eventType", - "value": "MODIFIED" - }, - { - "operator": "IS", - "term": "eventType", - "value": "READ_BY_APP" - } - ] + {"operator": "IS", "term": "eventType", "value": "CREATED"}, + {"operator": "IS", "term": "eventType", "value": "MODIFIED"}, + {"operator": "IS", "term": "eventType", "value": "READ_BY_APP"}, + ], }, { "filterClause": "AND", - "filters": [ - { - "operator": "IS", - "term": "exposure", - "value": "RemovableMedia" - } - ] - } + "filters": [{"operator": "IS", "term": "exposure", "value": "RemovableMedia"}], + }, ], "pgNum": 1, - "pgSize": 100, + "pgSize": 10000, "srtDir": "asc", - "srtKey": "eventId" - } + "srtKey": "eventId", + }, ] -def create_alert_mocks(requests_mock): - requests_mock.get(MOCK_URL + '/c42api/v3/auth/jwt?useBody=true', json={"data": {"v3_user_token": "faketoken"}}) - requests_mock.get(MOCK_URL + '/api/User/my', json={}) - requests_mock.get(MOCK_URL + '/c42api/v3/customer/my', json={"data": {"tenantUid": "123"}}) - requests_mock.get(MOCK_URL + '/api/ServerEnv', json={"stsBaseUrl": MOCK_URL}) - requests_mock.get(MOCK_URL + '/v1/AlertService-API_URL', text=MOCK_URL + '/svc/api') +MOCK_GET_USER_RESPONSE = """ +{ + "totalCount": 1, + "users": [ + { + "userId": 123456, + "userUid": "123412341234123412", + "status": "Active", + "username": "test.testerson@example.com", + "email": "test.testerson@example.com", + "firstName": "Test", + "lastName": "Testerson", + "quotaInBytes": -1, + "orgId": 1111, + "orgUid": "81111247111106706", + "orgName": "Testers", + "userExtRef": null, + "notes": null, + "active": true, + "blocked": false, + "emailPromo": true, + "invited": false, + "orgType": "ENTERPRISE", + "usernameIsAnEmail": true, + "creationDate": "2019-09-30T21:03:08.587Z", + "modificationDate": "2020-04-10T11:49:49.987Z", + "passwordReset": false, + "localAuthenticationOnly": false, + "licenses": ["admin.securityTools"] + } + ] +}""" + + +@pytest.fixture +def code42_sdk_mock(mocker): + c42_sdk_mock = mocker.MagicMock(spec=SDKClient) + # Setup mock alert details + alert_details_response = create_mock_code42_sdk_response(mocker, MOCK_ALERT_DETAILS_RESPONSE) + c42_sdk_mock.alerts.get_details.return_value = alert_details_response -def create_departingemployee_mocks(requests_mock): - requests_mock.get(MOCK_URL + '/c42api/v3/auth/jwt?useBody=true', json={"data": {"v3_user_token": "faketoken"}}) - requests_mock.get(MOCK_URL + '/api/User/my', json={}) - requests_mock.get(MOCK_URL + '/c42api/v3/customer/my', json={"data": {"tenantUid": "123"}}) - requests_mock.get(MOCK_URL + '/api/ServerEnv', json={"stsBaseUrl": MOCK_URL}) - requests_mock.get(MOCK_URL + '/v1/FedObserver-API_URL', text=MOCK_URL + '/svc/api') - requests_mock.get(MOCK_URL + '/v1/employeecasemanagement-API_URL', text=MOCK_URL + '/svc/apiv/v1') - requests_mock.get(MOCK_URL + '/v1/visualization-services-API_URL', text=MOCK_URL + '/svc/apiv/v1') + # Setup alerts for querying + alerts_response = create_mock_code42_sdk_response(mocker, MOCK_ALERTS_RESPONSE) + c42_sdk_mock.alerts.search.return_value = alerts_response + + # Setup mock user + get_user_response = create_mock_code42_sdk_response(mocker, MOCK_GET_USER_RESPONSE) + c42_sdk_mock.users.get_by_username.return_value = get_user_response + + # Setup file events + search_file_events_response = create_mock_code42_sdk_response( + mocker, MOCK_SECURITY_EVENT_RESPONSE + ) + c42_sdk_mock.securitydata.search_file_events.return_value = search_file_events_response + return c42_sdk_mock -def create_securitydata_mocks(requests_mock): - requests_mock.get(MOCK_URL + '/c42api/v3/auth/jwt?useBody=true', json={"data": {"v3_user_token": "faketoken"}}) - requests_mock.get(MOCK_URL + '/api/User/my', json={}) - requests_mock.get(MOCK_URL + '/c42api/v3/customer/my', json={"data": {"tenantUid": "123"}}) - requests_mock.get(MOCK_URL + '/api/ServerEnv', json={"stsBaseUrl": MOCK_URL}) + +def create_mock_code42_sdk_response(mocker, response_text): + response_mock = mocker.MagicMock(spec=Response) + response_mock.text = response_text + return Py42Response(response_mock) + + +"""TESTS""" def test_build_query_payload(): - query = build_query_payload(MOCK_SECURITYDATA_SEARCH_QUERY) - assert json.loads(query) == MOCK_QUERY_PAYLOAD + query = build_query_payload(MOCK_SECURITY_DATA_SEARCH_QUERY) + assert query.sort_key == MOCK_FILE_EVENT_QUERY_PAYLOAD["srtKey"] + assert query.page_number == MOCK_FILE_EVENT_QUERY_PAYLOAD["pgNum"] + assert json.loads((str(query))) == MOCK_FILE_EVENT_QUERY_PAYLOAD def test_map_observation_to_security_query(): - for i in range(0, len(MOCK_ALERT_DETAILS_RESPONSE)): - query = map_observation_to_security_query( - MOCK_ALERT_DETAILS_RESPONSE[i]['alerts'][0]['observations'][0], MOCK_ALERT_DETAILS_RESPONSE[i]['alerts'][0]['actor']) - assert json.loads(query) == MOCK_OBSERVATION_QUERIES[i] + response = json.loads(MOCK_ALERT_DETAILS_RESPONSE) + alerts = response["alerts"] + for i in range(0, len(alerts)): + observation = alerts[i]["observations"][0] + actor = alerts[i]["actor"] + query = map_observation_to_security_query(observation, actor) + assert query.sort_key == MOCK_OBSERVATION_QUERIES[i]["srtKey"] + assert query.page_number == MOCK_OBSERVATION_QUERIES[i]["pgNum"] + assert json.loads(str(query)) == MOCK_OBSERVATION_QUERIES[i] def test_map_to_code42_event_context(): - for i in range(0, len(MOCK_SECURITY_EVENT_RESPONSE['fileEvents'])): - context = map_to_code42_event_context(MOCK_SECURITY_EVENT_RESPONSE['fileEvents'][i]) + response = json.loads(MOCK_SECURITY_EVENT_RESPONSE) + file_events = response["fileEvents"] + for i in range(0, len(file_events)): + context = map_to_code42_event_context(file_events[i]) assert context == MOCK_CODE42_EVENT_CONTEXT[i] def test_map_to_code42_alert_context(): - for i in range(0, len(MOCK_ALERT_DETAILS_RESPONSE)): - context = map_to_code42_alert_context(MOCK_ALERT_DETAILS_RESPONSE[i]['alerts'][0]) + response = json.loads(MOCK_ALERT_DETAILS_RESPONSE) + alerts = response["alerts"] + for i in range(0, len(alerts)): + context = map_to_code42_alert_context(alerts[i]) assert context == MOCK_CODE42_ALERT_CONTEXT[i] def test_map_to_file_context(): - for i in range(0, len(MOCK_SECURITY_EVENT_RESPONSE['fileEvents'])): - context = map_to_file_context(MOCK_SECURITY_EVENT_RESPONSE['fileEvents'][i]) + response = json.loads(MOCK_SECURITY_EVENT_RESPONSE) + file_events = response["fileEvents"] + for i in range(0, len(file_events)): + context = map_to_file_context(file_events[i]) assert context == MOCK_FILE_CONTEXT[i] -def test_alert_get_command(requests_mock): - create_alert_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-details', json=MOCK_ALERT_DETAILS_RESPONSE[0]) +def test_alert_get_command(code42_sdk_mock): client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) - _, _, res = alert_get_command(client, {'id': '36fb8ca5-0533-4d25-9763-e09d35d60610'}) - assert res['ruleId'] == "c4404ee8-503c-4a21-98f5-37561ee4caf0" + _, _, res = alert_get_command(client, {"id": "36fb8ca5-0533-4d25-9763-e09d35d60610"}) + assert res["ruleId"] == "4576576e-13cb-4f88-be3a-ee77739de649" -def test_alert_resolve_command(requests_mock): - create_alert_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/resolve-alert', json={'dummyresponse': True}) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-details', json=MOCK_ALERT_DETAILS_RESPONSE[0]) +def test_alert_resolve_command(code42_sdk_mock): client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) - _, _, res = alert_resolve_command(client, {'id': '36fb8ca5-0533-4d25-9763-e09d35d60610'}) - assert res['id'] == '36fb8ca5-0533-4d25-9763-e09d35d60610' + _, _, res = alert_resolve_command(client, {"id": "36fb8ca5-0533-4d25-9763-e09d35d60610"}) + assert res["id"] == "36fb8ca5-0533-4d25-9763-e09d35d60610" -def test_departingemployee_remove_command(requests_mock): - create_departingemployee_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/departingemployee/search', - json={'cases': [{'username': 'user1@example.com', 'caseId': 123, 'tenantId': '123'}]}) - requests_mock.post(MOCK_URL + '/svc/api/v1/departingemployee/details', - json={'username': 'user1@example.com', 'caseId': 123, 'tenantId': '123'}) - requests_mock.post(MOCK_URL + '/svc/api/v1/departingemployee/resolve') +def test_departing_employee_remove_command(code42_sdk_mock): client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) - _, _, res = departingemployee_remove_command(client, {'username': 'user1@example.com'}) - assert res == 123 + _, _, res = departingemployee_remove_command(client, {"username": "user1@example.com"}) + expected = "123412341234123412" # value found in GET_USER_RESPONSE + assert res == expected + code42_sdk_mock.detectionlists.departing_employee.remove.assert_called_once_with(expected) -def test_departingemployee_add_command(requests_mock): - create_departingemployee_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/departingemployee/create', json={'caseId': 123}) +def test_departing_employee_add_command(code42_sdk_mock): client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) - _, _, res = departingemployee_add_command(client, {'username': 'user1@example.com', - 'departuredate': '2020-01-01', 'notes': 'Dummy note'}) - assert res == 123 + _, _, res = departingemployee_add_command( + client, + {"username": "user1@example.com", "departuredate": "2020-01-01", "note": "Dummy note"}, + ) + expected_user_id = "123412341234123412" # value found in GET_USER_RESPONSE + assert res == expected_user_id + add_func = code42_sdk_mock.detectionlists.departing_employee.add + add_func.assert_called_once_with(expected_user_id, departure_date="2020-01-01") + code42_sdk_mock.detectionlists.update_user_notes.assert_called_once_with(expected_user_id, "Dummy note") -def test_securitydata_search_command(requests_mock): - create_securitydata_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/forensic-search/queryservice/api/v1/fileevent', json=MOCK_SECURITY_EVENT_RESPONSE) +def test_security_data_search_command(code42_sdk_mock): client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) - _, _, res = securitydata_search_command(client, MOCK_SECURITYDATA_SEARCH_QUERY) + _, _, res = securitydata_search_command(client, MOCK_SECURITY_DATA_SEARCH_QUERY) assert len(res) == 3 + actual_query = code42_sdk_mock.securitydata.search_file_events.call_args[0][0] + filter_groups = json.loads(str(actual_query))["groups"] + assert filter_groups[0]["filters"][0]["term"] == "md5Checksum" + assert filter_groups[0]["filters"][0]["value"] == "d41d8cd98f00b204e9800998ecf8427e" + assert filter_groups[1]["filters"][0]["term"] == "osHostName" + assert filter_groups[1]["filters"][0]["value"] == "DESKTOP-0001" + assert filter_groups[2]["filters"][0]["term"] == "deviceUserName" + assert filter_groups[2]["filters"][0]["value"] == "user3@example.com" + assert filter_groups[3]["filters"][0]["term"] == "exposure" + assert filter_groups[3]["filters"][0]["value"] == "ApplicationRead" + + +def test_fetch_incidents_handles_single_severity(code42_sdk_mock): + client = Code42Client( + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None + ) + fetch_incidents( + client=client, + last_run={"last_fetch": None}, + first_fetch_time=MOCK_FETCH_TIME, + event_severity_filter="High", + fetch_limit=10, + include_files=True, + integration_context=None, + ) + assert "HIGH" in str(code42_sdk_mock.alerts.search.call_args[0][0]) + + +def test_fetch_incidents_handles_multi_severity(code42_sdk_mock): + client = Code42Client( + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None + ) + fetch_incidents( + client=client, + last_run={"last_fetch": None}, + first_fetch_time=MOCK_FETCH_TIME, + event_severity_filter=["High", "Low"], + fetch_limit=10, + include_files=True, + integration_context=None, + ) + assert "HIGH" in str(code42_sdk_mock.alerts.search.call_args[0][0]) + assert "LOW" in str(code42_sdk_mock.alerts.search.call_args[0][0]) -def test_fetch_incidents_first_run(requests_mock, mocker): - create_alert_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-alerts', json=MOCK_ALERT_RESPONSE) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-details', json=MOCK_ALERT_DETAILS_RESPONSE[0]) - requests_mock.post(MOCK_URL + '/forensic-search/queryservice/api/v1/fileevent', json=MOCK_SECURITY_EVENT_RESPONSE) +def test_fetch_incidents_first_run(code42_sdk_mock): client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) next_run, incidents, remaining_incidents = fetch_incidents( client=client, - last_run={'last_fetch': None}, - first_fetch_time="24 hours", + last_run={"last_fetch": None}, + first_fetch_time=MOCK_FETCH_TIME, event_severity_filter=None, - fetch_limit=int("10"), + fetch_limit=10, include_files=True, - integration_context=None + integration_context=None, ) assert len(incidents) == 3 - assert next_run['last_fetch'] + assert next_run["last_fetch"] -def test_fetch_incidents_next_run(requests_mock, mocker): +def test_fetch_incidents_next_run(code42_sdk_mock): mock_date = "2020-01-01T00:00:00.000Z" mock_timestamp = int(time.mktime(time.strptime(mock_date, "%Y-%m-%dT%H:%M:%S.000Z"))) - create_alert_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-alerts', json=MOCK_ALERT_RESPONSE) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-details', json=MOCK_ALERT_DETAILS_RESPONSE[0]) - requests_mock.post(MOCK_URL + '/forensic-search/queryservice/api/v1/fileevent', json=MOCK_SECURITY_EVENT_RESPONSE) client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) next_run, incidents, remaining_incidents = fetch_incidents( client=client, - last_run={'last_fetch': mock_timestamp}, - first_fetch_time="24 hours", + last_run={"last_fetch": mock_timestamp}, + first_fetch_time=MOCK_FETCH_TIME, event_severity_filter=None, - fetch_limit=int("10"), + fetch_limit=10, include_files=True, - integration_context=None + integration_context=None, ) assert len(incidents) == 3 - assert next_run['last_fetch'] + assert next_run["last_fetch"] -def test_fetch_incidents_fetch_limit(requests_mock, mocker): +def test_fetch_incidents_fetch_limit(code42_sdk_mock): mock_date = "2020-01-01T00:00:00.000Z" mock_timestamp = int(time.mktime(time.strptime(mock_date, "%Y-%m-%dT%H:%M:%S.000Z"))) - create_alert_mocks(requests_mock) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-alerts', json=MOCK_ALERT_RESPONSE) - requests_mock.post(MOCK_URL + '/svc/api/v1/query-details', json=MOCK_ALERT_DETAILS_RESPONSE[0]) - requests_mock.post(MOCK_URL + '/forensic-search/queryservice/api/v1/fileevent', json=MOCK_SECURITY_EVENT_RESPONSE) client = Code42Client( - sdk=SDK, - base_url=MOCK_URL, - auth=("123", "123"), - verify=False, - proxy=None + sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None ) next_run, incidents, remaining_incidents = fetch_incidents( client=client, - last_run={'last_fetch': mock_timestamp}, - first_fetch_time="24 hours", + last_run={"last_fetch": mock_timestamp}, + first_fetch_time=MOCK_FETCH_TIME, event_severity_filter=None, - fetch_limit=int("2"), + fetch_limit=2, include_files=True, - integration_context=None + integration_context=None, ) assert len(incidents) == 2 - assert next_run['last_fetch'] + assert next_run["last_fetch"] assert len(remaining_incidents) == 1 # Run again to get the last incident next_run, incidents, remaining_incidents = fetch_incidents( client=client, - last_run={'last_fetch': mock_timestamp}, - first_fetch_time="24 hours", + last_run={"last_fetch": mock_timestamp}, + first_fetch_time=MOCK_FETCH_TIME, event_severity_filter=None, - fetch_limit=int("2"), + fetch_limit=2, include_files=True, - integration_context={'remaining_incidents': remaining_incidents} + integration_context={"remaining_incidents": remaining_incidents}, ) assert len(incidents) == 1 - assert next_run['last_fetch'] + assert next_run["last_fetch"] assert not remaining_incidents diff --git a/Packs/Code42/Integrations/Code42/README.md b/Packs/Code42/Integrations/Code42/README.md index 03352df262b..f19d5044bfe 100644 --- a/Packs/Code42/Integrations/Code42/README.md +++ b/Packs/Code42/Integrations/Code42/README.md @@ -20,7 +20,7 @@ This integration was integrated and tested with the fully-hosted SaaS implementa * Management of Departing Employees within Code42 * General file event and metadata search -## Configure Code42 on Demisto +## Configure Code42 on Cortex XSOAR --- 1. Navigate to __Settings__ > __Integrations__ > __Servers & Services__. @@ -30,7 +30,7 @@ This integration was integrated and tested with the fully-hosted SaaS implementa * __credentials__ * __Code42 Console URL for the pod your Code42 instance is running in__: This defaults to console.us.code42.com for U.S. SaaS Pod customers; replace with the domain that you use to log into your Code42 console if located in a different SaaS pod. * __Fetch incidents__: Check this box to enable fetching of incidents - * __Incident type__: Select which Demisto incident type to map ingested Code42 alerts to + * __Incident type__: Select which Cortex XSOAR incident type to map ingested Code42 alerts to * __Alert severities to fetch when fetching incidents__: If desired, select which Alert severities to ingest. * __First fetch time range (<number> <time unit>, e.g., 1 hour, 30 minutes)__: When first run, how long to go back to retrieve alerts. * __Alerts to fetch per run; note that increasing this value may result in slow performance if too many results are returned at once__: Alerts to fetch and process per run. Setting this value too high may have a negative impact on performance. @@ -64,6 +64,7 @@ After you successfully execute a command, a DBot message appears in the War Room --- Search for a file in Security Data by JSON query, hash, username, device hostname, exfiltration type, or a combination of parameters. At least one parameter must be passed to the command. If a JSON parameter is passed, it will be used to the exclusion of other parameters, otherwise parameters will be combined with an AND clause. ##### Required Permissions + This command requires one of the following roles: * Security Center User @@ -282,7 +283,7 @@ This command requires one of the following roles: ### 3. code42-departingemployee-add --- -Add a user to the Departing Employee Lens +Add a user to the Departing Employee List ##### Required Permissions This command requires one of the following roles: @@ -297,8 +298,8 @@ This command requires one of the following roles: | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| username | Username to add to the Departing Employee Lens | Required | -| departuredate | Departure date for the employee in YYYY-MM-DD format | Optional | +| username | Username to add to the Departing Employee List | Required | +| departuredate | Departure date for the employee in yyyy-MM-dd format | Optional | | note | Note to attach to Departing Employee | Optional | @@ -306,7 +307,7 @@ This command requires one of the following roles: | **Path** | **Type** | **Description** | | --- | --- | --- | -| Code42.DepartingEmployee.CaseID | string | Internal Code42 Case ID for Departing Employee | +| Code42.DepartingEmployee.UserID | string | Internal Code42 Case ID for Departing Employee | | Code42.DepartingEmployee.Username | string | Username for Departing Employee | | Code42.DepartingEmployee.Note | string | Note associated with Departing Employee | | Code42.DepartingEmployee.DepartureDate | unknown | Departure date for Departing Employee | @@ -321,7 +322,7 @@ This command requires one of the following roles: ``` { "DepartingEmployee": { - "CaseID": "892", + "UserID": "892", "DepartureDate": "2020-02-28", "Note": "Leaving for competitor", "Username": "john.user@123.org" @@ -331,14 +332,14 @@ This command requires one of the following roles: ##### Human Readable Output -| **CaseID** | **DepartureDate** | **Note** | **Username** | +| **UserID** | **DepartureDate** | **Note** | **Username** | | --- | --- | --- | --- | | 123 | 2020-02-28 | Leaving for competitor | john.user@123.org | ### 4. code42-departingemployee-remove --- -Remove a user from the Departing Employee Lens +Remove a user from the Departing Employee List ##### Required Permissions This command requires one of the following roles: @@ -353,14 +354,14 @@ This command requires one of the following roles: | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| username | Username to remove from the Departing Employee Lens | Optional | +| username | Username to remove from the Departing Employee List | Optional | ##### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| Code42.DepartingEmployee.CaseID | unknown | Internal Code42 Case ID for Departing Employee | +| Code42.DepartingEmployee.UserID | unknown | Internal Code42 User ID for Departing Employee | | Code42.DepartingEmployee.Username | unknown | Username for Departing Employee | @@ -373,7 +374,7 @@ This command requires one of the following roles: ``` { "DepartingEmployee": { - "CaseID": "892", + "UserID": "892", "Username": "john.user@123.org" } } @@ -381,7 +382,7 @@ This command requires one of the following roles: ##### Human Readable Output -| **CaseID** | **Username** | +| **UserID** | **Username** | | --- | --- | | 123 | john.user@123.org | @@ -434,7 +435,7 @@ This command requires one of the following roles: ## Additional Information --- -For additional infromation on Code42 features and functionality please visit [https://support.code42.com/Administrator/Cloud/Monitoring\_and\_managing](https://support.code42.com/Administrator/Cloud/Monitoring_and_managing) +For additional information on Code42 features and functionality please visit [https://support.code42.com/Administrator/Cloud/Monitoring\_and\_managing](https://support.code42.com/Administrator/Cloud/Monitoring_and_managing) ## Known Limitations --- diff --git a/Packs/Code42/Integrations/Code42/integration-Code42.yml b/Packs/Code42/Integrations/Code42/integration-Code42.yml new file mode 100644 index 00000000000..3668c3a97b7 --- /dev/null +++ b/Packs/Code42/Integrations/Code42/integration-Code42.yml @@ -0,0 +1,1037 @@ +commonfields: + id: Code42 + version: -1 +name: Code42 +display: Code42 +category: Endpoint +description: Use the Code42 integration to identify potential data exfiltration from insider threats while speeding investigation and response by providing fast access to file events and metadata across physical and cloud environments. +configuration: +- display: Code42 Console URL for the pod your Code42 instance is running in + name: console_url + defaultvalue: console.us.code42.com + type: 0 + required: true +- display: "" + name: credentials + defaultvalue: "" + type: 9 + required: true +- display: Fetch incidents + name: isFetch + type: 8 + required: false +- display: Incident type + name: incidentType + type: 13 + required: false +- display: Alert severities to fetch when fetching incidents + name: alert_severity + defaultvalue: "" + type: 16 + required: false + options: + - High + - Medium + - Low +- display: First fetch time range (