diff --git a/Packs/Code42/Integrations/Code42/CHANGELOG.md b/Packs/Code42/Integrations/Code42/CHANGELOG.md index c9cd92c44c86..244b5e76f237 100644 --- a/Packs/Code42/Integrations/Code42/CHANGELOG.md +++ b/Packs/Code42/Integrations/Code42/CHANGELOG.md @@ -1,5 +1,13 @@ ## [Unreleased] - +Added new commands: + - **code42-departingemployee-get-all** that gets all the employees on the Departing Employee List. + - **code42-highriskemployee-add** that takes a username and adds the employee to the High Risk Employee List. + - **code42-highriskemployee-remove** that takes a username and remove the employee from the High Risk Employee List. + - **code42-highriskemployee-get-all** that gets all the employees on the High Risk Employee List. + Optionally takes a list of risk tags and only gets employees who have those risk tags. + - **code42-highriskemployee-add-risk-tags** that takes a username and risk tags and associates the risk tags with the user. + - **code42-highriskemployee-remove-risk-tags** that takes a username and risk tags and disassociates the risk tags from the user. +Improve error messages for all Commands to include exception detail. ## [20.3.3] - 2020-03-18 #### New Integration diff --git a/Packs/Code42/Integrations/Code42/Code42.py b/Packs/Code42/Integrations/Code42/Code42.py index aa0260bf4a82..8d83633fe331 100644 --- a/Packs/Code42/Integrations/Code42/Code42.py +++ b/Packs/Code42/Integrations/Code42/Code42.py @@ -109,6 +109,7 @@ "FileCategory", "DeviceUsername", ] + SECURITY_ALERT_HEADERS = ["Type", "Occurred", "Username", "Name", "Description", "State", "ID"] @@ -133,6 +134,20 @@ def _create_alert_query(event_severity_filter, start_time): return alert_query +def _get_all_high_risk_employees_from_page(page, risk_tags): + res = [] + for employee in page["items"]: + if not risk_tags: + res.append(employee) + continue + + employee_tags = employee.get("riskFactors") + # If the employee risk tags contain all the given risk tags + if employee_tags and set(risk_tags) <= set(employee_tags): + res.append(employee) + return res + + class Code42Client(BaseClient): """ Client will implement the service API, should not contain Cortex XSOAR logic. @@ -146,64 +161,83 @@ def __init__(self, sdk, base_url, auth, verify=True, proxy=False): py42.settings.set_user_agent_suffix("Cortex XSOAR") def add_user_to_departing_employee(self, username, departure_date=None, note=None): - try: - 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 + 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) return user_id - def fetch_alerts(self, start_time, event_severity_filter): - try: - query = _create_alert_query(event_severity_filter, start_time) - res = self._sdk.alerts.search(query) - except Exception: - return None - return res["alerts"] - - def get_alert_details(self, alert_id): - try: - res = self._sdk.alerts.get_details(alert_id) - except Exception: - return None - return res["alerts"][0] + def remove_user_from_departing_employee(self, username): + user_id = self.get_user_id(username) + self._sdk.detectionlists.departing_employee.remove(user_id) + return user_id - def get_current_user(self): - try: - res = self._sdk.users.get_current() - except Exception: - return None + def get_all_departing_employees(self): + res = [] + pages = self._sdk.detectionlists.departing_employee.get_all() + for page in pages: + employees = page["items"] + res.extend(employees) return res - def remove_user_from_departing_employee(self, username): - try: - user_id = self.get_user_id(username) - self._sdk.detectionlists.departing_employee.remove(user_id) - except Exception: - return None + def add_user_to_high_risk_employee(self, username, note=None): + user_id = self.get_user_id(username) + self._sdk.detectionlists.high_risk_employee.add(user_id) + if note: + self._sdk.detectionlists.update_user_notes(user_id, note) + return user_id + + def remove_user_from_high_risk_employee(self, username): + user_id = self.get_user_id(username) + self._sdk.detectionlists.high_risk_employee.remove(user_id) + return user_id + + def add_user_risk_tags(self, username, risk_tags): + user_id = self.get_user_id(username) + self._sdk.detectionlists.add_user_risk_tags(user_id, risk_tags) return user_id + def remove_user_risk_tags(self, username, risk_tags): + user_id = self.get_user_id(username) + self._sdk.detectionlists.remove_user_risk_tags(user_id, risk_tags) + return user_id + + def get_all_high_risk_employees(self, risk_tags=None): + if isinstance(risk_tags, str): + risk_tags = [risk_tags] + res = [] + pages = self._sdk.detectionlists.high_risk_employee.get_all() + for page in pages: + res.extend(_get_all_high_risk_employees_from_page(page, risk_tags)) + return res + + def fetch_alerts(self, start_time, event_severity_filter): + query = _create_alert_query(event_severity_filter, start_time) + res = self._sdk.alerts.search(query) + return res["alerts"] + + def get_alert_details(self, alert_id): + res = self._sdk.alerts.get_details(alert_id)["alerts"] + if not res: + raise Exception("No alert found with ID {0}.".format(alert_id)) + return res[0] + def resolve_alert(self, id): - try: - self._sdk.alerts.resolve(id) - except Exception: - return None + self._sdk.alerts.resolve(id) return id + def get_current_user(self): + res = self._sdk.users.get_current() + return res + def get_user_id(self, username): - try: - res = self._sdk.users.get_by_username(username) - except Exception: - return None - return res["users"][0]["userUid"] - - def search_json(self, payload): - try: - res = self._sdk.securitydata.search_file_events(payload) - except Exception: - return None + res = self._sdk.users.get_by_username(username)["users"] + if not res: + raise Exception("No user found with username {0}.".format(username)) + return res[0]["userUid"] + + def search_file_events(self, payload): + res = self._sdk.securitydata.search_file_events(payload) return res["fileEvents"] @@ -251,6 +285,7 @@ def to_all_query(self): 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 @@ -416,48 +451,62 @@ def _map_obj_to_context(obj, context_mapper): return {v: obj.get(k) for k, v in context_mapper.items() if obj.get(k)} +def create_command_error_message(cmd, ex): + return "Failed to execute command {0} command. Error: {1}".format(cmd, str(ex)) + + +"""Commands""" + + @logger def alert_get_command(client, args): code42_securityalert_context = [] - alert = client.get_alert_details(args["id"]) - if alert: + try: + alert = client.get_alert_details(args["id"]) + if not alert: + return "No results found", {}, {} + code42_context = map_to_code42_alert_context(alert) code42_securityalert_context.append(code42_context) readable_outputs = tableToMarkdown( - f"Code42 Security Alert Results", + "Code42 Security Alert Results", code42_securityalert_context, headers=SECURITY_ALERT_HEADERS, ) return readable_outputs, {"Code42.SecurityAlert": code42_securityalert_context}, alert - else: - return "No results found", {}, {} + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) @logger def alert_resolve_command(client, args): code42_security_alert_context = [] - alert_id = client.resolve_alert(args["id"]) - if not alert_id: - return "No results found", {}, {} + try: + alert_id = client.resolve_alert(args["id"]) - # 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, - ) + 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( + "Code42 Security Alert Resolved", + code42_security_alert_context, + headers=SECURITY_ALERT_HEADERS, + ) + return ( + readable_outputs, + {"Code42.SecurityAlert": code42_security_alert_context}, + alert_details, + ) + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) @logger @@ -465,32 +514,127 @@ def departingemployee_add_command(client, args): 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 + try: + user_id = client.add_user_to_departing_employee(username, departing_date, note) + de_context = { + "UserID": user_id, + "Username": username, + "DepartureDate": departing_date, + "Note": note, + } + readable_outputs = tableToMarkdown("Code42 Departing Employee List User Added", de_context) + return readable_outputs, {"Code42.DepartingEmployee": de_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) @logger def departingemployee_remove_command(client, args): username = args["username"] - user_id = client.remove_user_from_departing_employee(username) - if user_id: + try: + user_id = client.remove_user_from_departing_employee(username) de_context = {"UserID": user_id, "Username": username} readable_outputs = tableToMarkdown( - f"Code42 Departing Employee List User Removed", de_context + "Code42 Departing Employee List User Removed", de_context ) return readable_outputs, {"Code42.DepartingEmployee": de_context}, user_id - else: - return_error(message="Could not remove user from Departing Employee List") + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + +@logger +def departingemployee_get_all_command(client, args): + try: + employees = client.get_all_departing_employees() + employees_context = [ + { + "UserID": e["userId"], + "Username": e["userName"], + "DepartureDate": e.get("departureDate"), + "Note": e["notes"], + } + for e in employees + ] + readable_outputs = tableToMarkdown("All Departing Employees", employees_context) + return ( + readable_outputs, + {"Code42.DepartingEmployee(val.UserID && val.UserID == obj.UserID)": employees_context}, + employees, + ) + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + +@logger +def highriskemployee_add_command(client, args): + username = args["username"] + note = args.get("note") + try: + user_id = client.add_user_to_high_risk_employee(username, note) + hr_context = {"UserID": user_id, "Username": username} + readable_outputs = tableToMarkdown("Code42 High Risk Employee List User Added", hr_context) + return readable_outputs, {"Code42.HighRiskEmployee": hr_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + +@logger +def highriskemployee_remove_command(client, args): + username = args["username"] + try: + user_id = client.remove_user_from_high_risk_employee(username) + hr_context = {"UserID": user_id, "Username": username} + readable_outputs = tableToMarkdown( + "Code42 High Risk Employee List User Removed", hr_context + ) + return readable_outputs, {"Code42.HighRiskEmployee": hr_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + +@logger +def highriskemployee_get_all_command(client, args): + tags = args.get("risktags") + try: + employees = client.get_all_high_risk_employees(tags) + employees_context = [ + {"UserID": e["userId"], "Username": e["userName"], "Note": e["notes"]} + for e in employees + ] + readable_outputs = tableToMarkdown("Retrieved All High Risk Employees", employees_context) + return ( + readable_outputs, + {"Code42.HighRiskEmployee(val.UserID && val.UserID == obj.UserID)": employees_context}, + employees, + ) + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + +@logger +def highriskemployee_add_risk_tags_command(client, args): + username = args["username"] + tags = args["risktags"] + try: + user_id = client.add_user_risk_tags(username, tags) + rt_context = {"UserID": user_id, "Username": username, "RiskTags": tags} + readable_outputs = tableToMarkdown("Code42 Risk Tags Added", rt_context) + return readable_outputs, {"Code42.HighRiskEmployee": rt_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + +@logger +def highriskemployee_remove_risk_tags_command(client, args): + username = args["username"] + tags = args["risktags"] + try: + user_id = client.remove_user_risk_tags(username, tags) + rt_context = {"UserID": user_id, "Username": username, "RiskTags": tags} + readable_outputs = tableToMarkdown("Code42 Risk Tags Removed", rt_context) + return readable_outputs, {"Code42.HighRiskEmployee": rt_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) def _create_incident_from_alert_details(details): @@ -587,7 +731,7 @@ def _relate_files_to_alert(self, alert_details): 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) + return self._client.search_file_events(security_data_query) def fetch_incidents( @@ -618,11 +762,11 @@ def securitydata_search_command(client, args): file_context = [] # If JSON payload is passed as an argument, ignore all other args and search by JSON payload if _json is not None: - file_events = client.search_json(_json) + file_events = client.search_file_events(_json) else: # Build payload payload = build_query_payload(args) - file_events = client.search_json(payload) + file_events = client.search_file_events(payload) if file_events: for file_event in file_events: code42_context_event = map_to_code42_event_context(file_event) @@ -630,7 +774,7 @@ def securitydata_search_command(client, args): file_context_event = map_to_file_context(file_event) file_context.append(file_context_event) readable_outputs = tableToMarkdown( - f"Code42 Security Data Results", + "Code42 Security Data Results", code42_security_data_context, headers=SECURITY_EVENT_HEADERS, ) @@ -674,6 +818,12 @@ def main(): "code42-securitydata-search": securitydata_search_command, "code42-departingemployee-add": departingemployee_add_command, "code42-departingemployee-remove": departingemployee_remove_command, + "code42-departingemployee-get-all": departingemployee_get_all_command, + "code42-highriskemployee-add": highriskemployee_add_command, + "code42-highriskemployee-remove": highriskemployee_remove_command, + "code42-highriskemployee-get-all": highriskemployee_get_all_command, + "code42-highriskemployee-add-risk-tags": highriskemployee_add_risk_tags_command, + "code42-highriskemployee-remove-risk-tags": highriskemployee_remove_risk_tags_command, } command = demisto.command() if command == "test-module": diff --git a/Packs/Code42/Integrations/Code42/Code42.yml b/Packs/Code42/Integrations/Code42/Code42.yml index e114581bcdd1..e914c2b7feaa 100644 --- a/Packs/Code42/Integrations/Code42/Code42.yml +++ b/Packs/Code42/Integrations/Code42/Code42.yml @@ -241,6 +241,17 @@ script: description: The severity of the alert. type: string description: Retrieve alert details by alert ID + - name: code42-alert-resolve + arguments: + - name: id + required: true + description: The alert ID to resolve. Alert IDs are associated with alerts that + are fetched via fetch-incidents. + outputs: + - contextPath: Code42.SecurityAlert.ID + description: The alert ID of the resolved alert. + type: string + description: Resolves a Code42 Security alert. - name: code42-departingemployee-add arguments: - name: username @@ -266,24 +277,108 @@ script: - name: code42-departingemployee-remove arguments: - name: username + required: true description: The username to remove from the Departing Employee List. outputs: - 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. + type: string description: Removes a user from the Departing Employee List. - - name: code42-alert-resolve + - name: code42-departingemployee-get-all + outputs: + - 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. + type: string + - contextPath: Code42.DepartingEmployee.Note + description: Note associated with the Departing Employee. + type: string + - contextPath: Code42.DepartingEmployee.DepartureDate + description: The departure date for the Departing Employee. + description: Get all employees on the Departing Employee List. + - name: code42-highriskemployee-add arguments: - - name: id + - name: username required: true - description: The alert ID to resolve. Alert IDs are associated with alerts that - are fetched via fetch-incidents. + description: The username to add to the High Risk Employee List. + - name: note + description: Note to attach to the High Risk Employee. outputs: - - contextPath: Code42.SecurityAlert.ID - description: The alert ID of the resolved alert. + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the High Risk Employee. type: string - description: Resolves a Code42 Security alert. + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Note + description: Note associated with the High Risk Employee. + type: string + description: Removes a user from the High Risk Employee List. + - name: code42-highriskemployee-remove + arguments: + - name: username + required: true + description: The username to remove from the High Risk Employee List. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the High Risk Employee. + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + description: Removes a user from the High Risk Employee List. + - name: code42-highriskemployee-get-all + arguments: + - name: risktags + description: To filter results by employees who have these risk tags. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Note + description: Note associated with the High Risk Employee. + type: string + description: Get all employees on the High Risk Employee List. + - name: code42-highriskemployee-add-risk-tags + arguments: + - name: username + required: true + description: The username of the High Risk Employee. + - name: risktags + required: true + description: Risk tags to associate with the High Risk Employee. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the Departing Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.RiskTags + description: Risk tags to associate with the High Risk Employee. + - name: code42-highriskemployee-remove-risk-tags + arguments: + - name: username + required: true + description: The username of the High Risk Employee. + - name: risktags + required: true + description: Risk tags to disassociate from the High Risk Employee. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the Departing Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.RiskTags + description: Risk tags to disassociate from the High Risk Employee. dockerimage: demisto/py42:1.0.0.9242 isfetch: true runonce: false diff --git a/Packs/Code42/Integrations/Code42/Code42_test.py b/Packs/Code42/Integrations/Code42/Code42_test.py index a02b98e779b4..e2d7dba7ce58 100644 --- a/Packs/Code42/Integrations/Code42/Code42_test.py +++ b/Packs/Code42/Integrations/Code42/Code42_test.py @@ -14,6 +14,12 @@ alert_resolve_command, departingemployee_add_command, departingemployee_remove_command, + departingemployee_get_all_command, + highriskemployee_add_command, + highriskemployee_remove_command, + highriskemployee_get_all_command, + highriskemployee_add_risk_tags_command, + highriskemployee_remove_risk_tags_command, fetch_incidents, securitydata_search_command, ) @@ -571,14 +577,8 @@ }, { "filterClause": "AND", - "filters": [ - { - "operator": "IS", - "term": "fileCategory", - "value": "IMAGE" - } - ] - } + "filters": [{"operator": "IS", "term": "fileCategory", "value": "IMAGE"}], + }, ], "pgNum": 1, "pgSize": 10000, @@ -618,7 +618,7 @@ {"operator": "IS", "term": "exposure", "value": "IsPublic"}, {"operator": "IS", "term": "exposure", "value": "SharedViaLink"}, ], - } + }, ], "pgNum": 1, "pgSize": 10000, @@ -708,41 +708,237 @@ ] }""" +MOCK_GET_ALL_DEPARTING_EMPLOYEES_RESPONSE = """ +{ + "items": [ + { + "type$": "DEPARTING_EMPLOYEE_V2", + "tenantId": 1000, + "userId": "890973079883949999", + "userName": "test@example.com", + "displayName": "Name", + "notes": "", + "createdAt": "2019-10-25T13:31:14.1199010Z", + "status": "OPEN", + "cloudUsernames": ["test@cloud.com"], + "totalBytes": 139856482, + "numEvents": 11 + }, + { + "type$": "DEPARTING_EMPLOYEE_V2", + "tenantId": 1000, + "userId": "123412341234123412", + "userName": "user1@example.com", + "displayName": "Name", + "notes": "", + "createdAt": "2019-10-25T13:31:14.1199010Z", + "status": "OPEN", + "cloudUsernames": ["test@example.com"], + "totalBytes": 139856482, + "numEvents": 11 + }, + { + "type$": "DEPARTING_EMPLOYEE_V2", + "tenantId": 1000, + "userId": "890973079883949999", + "userName": "test@example.com", + "displayName": "Name", + "notes": "", + "createdAt": "2019-10-25T13:31:14.1199010Z", + "status": "OPEN", + "cloudUsernames": ["test@example.com"], + "totalBytes": 139856482, + "numEvents": 11 + } + ], + "totalCount": 3 +} +""" + +MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE = """ +{ + "type$": "HIGH_RISK_SEARCH_RESPONSE_V2", + "items": [ + { + "type$": "HIGH_RISK_EMPLOYEE_V2", + "tenantId": "1d71796f-af5b-4231-9d8e-df6434da4663", + "userId": "91209844444444444", + "userName": "karen@example.com", + "displayName": "Karen", + "notes": "High risk notes", + "createdAt": "2020-05-22T17:47:42.7054310Z", + "status": "OPEN", + "cloudUsernames": [ + "karen+test@example.com", + "karen+manager@example.com" + ], + "totalBytes": 816122, + "numEvents": 13, + "riskFactors": [ + "PERFORMANCE_CONCERNS", + "SUSPICIOUS_SYSTEM_ACTIVITY", + "POOR_SECURITY_PRACTICES" + ] + }, + { + "type$": "HIGH_RISK_EMPLOYEE_V2", + "tenantId": "1d71796f-af5b-4231-9d8e-df6434da4663", + "userId": "94222222975202822222", + "userName": "james.test@example.com", + "displayName": "James Test", + "notes": "tests and more tests", + "createdAt": "2020-05-28T12:39:57.2058370Z", + "status": "OPEN", + "cloudUsernames": [ + "james.test+test@example.com" + ] + }, + { + "type$": "HIGH_RISK_EMPLOYEE_V2", + "tenantId": "1d71796f-af5b-4231-9d8e-df6434da4663", + "userId": "123412341234123412", + "userName": "user1@example.com", + "displayName": "User 1", + "notes": "Test Notes", + "createdAt": "2020-05-22T17:47:42.4836920Z", + "status": "OPEN", + "cloudUsernames": [ + "test@example.com", + "abc123@example.com" + ], + "riskFactors": [ + "PERFORMANCE_CONCERNS" + ] + } + ], + "totalCount": 3, + "rollups": [ + { + "type$": "HIGH_RISK_FILTER_ROLLUP_V2", + "filterType": "OPEN", + "totalCount": 3 + }, + { + "type$": "HIGH_RISK_FILTER_ROLLUP_V2", + "filterType": "EXFILTRATION_24_HOURS", + "totalCount": 0 + }, + { + "type$": "HIGH_RISK_FILTER_ROLLUP_V2", + "filterType": "EXFILTRATION_30_DAYS", + "totalCount": 1 + } + ], + "filterType": "OPEN", + "pgSize": 10, + "pgNum": 1, + "srtKey": "NUM_EVENTS", + "srtDirection": "DESC" +} +""" + @pytest.fixture def code42_sdk_mock(mocker): - c42_sdk_mock = mocker.MagicMock(spec=SDKClient) + code42_mock = mocker.MagicMock(spec=SDKClient) + get_user_response = create_mock_code42_sdk_response(mocker, MOCK_GET_USER_RESPONSE) + code42_mock.users.get_by_username.return_value = get_user_response + return code42_mock + + +@pytest.fixture +def code42_alerts_mock(code42_sdk_mock, mocker): + return create_alerts_mock(code42_sdk_mock, mocker) - # Setup mock alert details + +@pytest.fixture +def code42_file_events_mock(code42_sdk_mock, mocker): + return create_file_events_mock(code42_sdk_mock, mocker) + + +@pytest.fixture +def code42_fetch_incidents_mock(code42_sdk_mock, mocker): + code42_mock = create_alerts_mock(code42_sdk_mock, mocker) + code42_mock = create_file_events_mock(code42_mock, mocker) + return code42_mock + + +def create_alerts_mock(c42_sdk_mock, mocker): alert_details_response = create_mock_code42_sdk_response(mocker, MOCK_ALERT_DETAILS_RESPONSE) c42_sdk_mock.alerts.get_details.return_value = alert_details_response - - # Setup alerts for querying alerts_response = create_mock_code42_sdk_response(mocker, MOCK_ALERTS_RESPONSE) c42_sdk_mock.alerts.search.return_value = alerts_response + return c42_sdk_mock - # 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 +def create_file_events_mock(c42_sdk_mock, mocker): 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 +@pytest.fixture +def code42_departing_employee_mock(code42_sdk_mock, mocker): + all_departing_employees_response = create_mock_code42_sdk_response_generator( + mocker, [MOCK_GET_ALL_DEPARTING_EMPLOYEES_RESPONSE] + ) + code42_sdk_mock.detectionlists.departing_employee.get_all.return_value = ( + all_departing_employees_response + ) + return code42_sdk_mock + + +@pytest.fixture +def code42_high_risk_employee_mock(code42_sdk_mock, mocker): + all_high_risk_employees_response = create_mock_code42_sdk_response_generator( + mocker, [MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE] + ) + code42_sdk_mock.detectionlists.high_risk_employee.get_all.return_value = ( + all_high_risk_employees_response + ) + return code42_sdk_mock + + def create_mock_code42_sdk_response(mocker, response_text): response_mock = mocker.MagicMock(spec=Response) response_mock.text = response_text return Py42Response(response_mock) +def create_mock_code42_sdk_response_generator(mocker, response_pages): + return (create_mock_code42_sdk_response(mocker, page) for page in response_pages) + + +def create_client(sdk): + return Code42Client(sdk=sdk, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None) + + +def get_empty_detectionlist_response(mocker, base_text): + no_employees_response_text = json.loads(base_text) + no_employees_response_text["items"] = [] + no_employees_response_text = json.dumps(no_employees_response_text) + return create_mock_code42_sdk_response_generator(mocker, [no_employees_response_text]) + + """TESTS""" +def test_client_when_no_alert_found_raises_exception(code42_sdk_mock): + code42_sdk_mock.alerts.get_details.return_value = """{'type$': 'ALERT_DETAILS_RESPONSE', 'alerts': []}""" + client = create_client(code42_sdk_mock) + with pytest.raises(Exception): + client.get_alert_details("mock-id") + + +def test_client_when_no_user_found_raises_exception(code42_sdk_mock): + code42_sdk_mock.users.get_by_username.return_value = """{'totalCount': 0, 'users': []}""" + client = create_client(code42_sdk_mock) + with pytest.raises(Exception): + client.get_user_id("test@example.com") + + def test_build_query_payload(): query = build_query_payload(MOCK_SECURITY_DATA_SEARCH_QUERY) assert query.sort_key == MOCK_FILE_EVENT_QUERY_PAYLOAD["srtKey"] @@ -786,54 +982,223 @@ def test_map_to_file_context(): assert context == MOCK_FILE_CONTEXT[i] -def test_alert_get_command(code42_sdk_mock): - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None - ) +def test_alert_get_command(code42_alerts_mock): + client = create_client(code42_alerts_mock) _, _, res = alert_get_command(client, {"id": "36fb8ca5-0533-4d25-9763-e09d35d60610"}) assert res["ruleId"] == "4576576e-13cb-4f88-be3a-ee77739de649" -def test_alert_resolve_command(code42_sdk_mock): - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None - ) +def test_alert_resolve_command(code42_alerts_mock): + client = create_client(code42_alerts_mock) _, _, res = alert_resolve_command(client, {"id": "36fb8ca5-0533-4d25-9763-e09d35d60610"}) assert res["id"] == "36fb8ca5-0533-4d25-9763-e09d35d60610" -def test_departing_employee_remove_command(code42_sdk_mock): - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None +def test_departingemployee_add_command(code42_sdk_mock): + client = create_client(code42_sdk_mock) + _, _, 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_departingemployee_remove_command(code42_sdk_mock): + client = create_client(code42_sdk_mock) _, _, 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_departing_employee_add_command(code42_sdk_mock): - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None +def test_departingemployee_get_all_command(code42_departing_employee_mock): + client = create_client(code42_departing_employee_mock) + _, _, res = departingemployee_get_all_command(client, {"username": "user1@example.com"}) + expected = json.loads(MOCK_GET_ALL_DEPARTING_EMPLOYEES_RESPONSE)["items"] + assert res == expected + assert code42_departing_employee_mock.detectionlists.departing_employee.get_all.call_count == 1 + + +def test_departingemployee_get_all_command_gets_employees_from_multiple_pages( + code42_departing_employee_mock, mocker +): + # Setup get all departing employees + page = MOCK_GET_ALL_DEPARTING_EMPLOYEES_RESPONSE + # Setup 3 pages of employees + employee_page_generator = ( + create_mock_code42_sdk_response(mocker, page) for page in [page, page, page] ) - _, _, res = departingemployee_add_command( + code42_departing_employee_mock.detectionlists.departing_employee.get_all.return_value = ( + employee_page_generator + ) + client = create_client(code42_departing_employee_mock) + + _, _, res = departingemployee_get_all_command(client, {"username": "user1@example.com"}) + + # Expect to have employees from 3 pages in the result + expected_page = json.loads(MOCK_GET_ALL_DEPARTING_EMPLOYEES_RESPONSE)["items"] + expected = expected_page + expected_page + expected_page + assert res == expected + + +def test_departingemployee_get_all_command_when_no_employees( + code42_departing_employee_mock, mocker +): + no_employees_response = get_empty_detectionlist_response( + mocker, MOCK_GET_ALL_DEPARTING_EMPLOYEES_RESPONSE + ) + code42_departing_employee_mock.detectionlists.departing_employee.get_all.return_value = ( + no_employees_response + ) + client = create_client(code42_departing_employee_mock) + _, _, res = departingemployee_get_all_command( client, - {"username": "user1@example.com", "departuredate": "2020-01-01", "note": "Dummy note"}, + { + "risktags": [ + "PERFORMANCE_CONCERNS", + "SUSPICIOUS_SYSTEM_ACTIVITY", + "POOR_SECURITY_PRACTICES", + ] + }, + ) + # Only first employee has the given risk tags + expected = [] + assert res == expected + assert code42_departing_employee_mock.detectionlists.departing_employee.get_all.call_count == 1 + + +def test_highriskemployee_add_command(code42_high_risk_employee_mock): + client = create_client(code42_high_risk_employee_mock) + _, _, res = highriskemployee_add_command( + client, {"username": "user1@example.com", "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") + code42_high_risk_employee_mock.detectionlists.high_risk_employee.add.assert_called_once_with( + expected_user_id + ) + code42_high_risk_employee_mock.detectionlists.update_user_notes.assert_called_once_with( + expected_user_id, "Dummy note" + ) -def test_security_data_search_command(code42_sdk_mock): - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None +def test_highriskemployee_remove_command(code42_sdk_mock): + client = create_client(code42_sdk_mock) + _, _, res = highriskemployee_remove_command(client, {"username": "user1@example.com"}) + expected = "123412341234123412" # value found in GET_USER_RESPONSE + assert res == expected + code42_sdk_mock.detectionlists.high_risk_employee.remove.assert_called_once_with(expected) + + +def test_highriskemployee_get_all_command(code42_high_risk_employee_mock): + client = create_client(code42_high_risk_employee_mock) + _, _, res = highriskemployee_get_all_command(client, {}) + expected = json.loads(MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE)["items"] + assert res == expected + assert code42_high_risk_employee_mock.detectionlists.high_risk_employee.get_all.call_count == 1 + + +def test_highriskemployee_get_all_command_gets_employees_from_multiple_pages( + code42_high_risk_employee_mock, mocker +): + # Setup get all high risk employees + page = MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE + # Setup 3 pages of employees + employee_page_generator = ( + create_mock_code42_sdk_response(mocker, page) for page in [page, page, page] ) + code42_high_risk_employee_mock.detectionlists.high_risk_employee.get_all.return_value = ( + employee_page_generator + ) + client = create_client(code42_high_risk_employee_mock) + + _, _, res = highriskemployee_get_all_command(client, {"username": "user1@example.com"}) + + # Expect to have employees from 3 pages in the result + expected_page = json.loads(MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE)["items"] + expected = expected_page + expected_page + expected_page + assert res == expected + + +def test_highriskemployee_get_all_command_when_given_risk_tags_only_gets_employees_with_tags( + code42_high_risk_employee_mock +): + client = create_client(code42_high_risk_employee_mock) + _, _, res = highriskemployee_get_all_command( + client, + { + "risktags": [ + "PERFORMANCE_CONCERNS", + "SUSPICIOUS_SYSTEM_ACTIVITY", + "POOR_SECURITY_PRACTICES", + ] + }, + ) + # Only first employee has the given risk tags + expected = [json.loads(MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE)["items"][0]] + assert res == expected + assert code42_high_risk_employee_mock.detectionlists.high_risk_employee.get_all.call_count == 1 + + +def test_highriskemployee_get_all_command_when_no_employees(code42_high_risk_employee_mock, mocker): + no_employees_response = get_empty_detectionlist_response( + mocker, MOCK_GET_ALL_HIGH_RISK_EMPLOYEES_RESPONSE + ) + code42_high_risk_employee_mock.detectionlists.high_risk_employee.get_all.return_value = ( + no_employees_response + ) + client = create_client(code42_high_risk_employee_mock) + _, _, res = highriskemployee_get_all_command( + client, + { + "risktags": [ + "PERFORMANCE_CONCERNS", + "SUSPICIOUS_SYSTEM_ACTIVITY", + "POOR_SECURITY_PRACTICES", + ] + }, + ) + # Only first employee has the given risk tags + expected = [] + assert res == expected + assert code42_high_risk_employee_mock.detectionlists.high_risk_employee.get_all.call_count == 1 + + +def test_highriskemployee_add_risk_tags_command(code42_sdk_mock): + client = create_client(code42_sdk_mock) + _, _, res = highriskemployee_add_risk_tags_command( + client, {"username": "user1@example.com", "risktags": "FLIGHT_RISK"} + ) + expected_user_id = "123412341234123412" # value found in GET_USER_RESPONSE + assert res == expected_user_id + code42_sdk_mock.detectionlists.add_user_risk_tags.assert_called_once_with( + expected_user_id, "FLIGHT_RISK" + ) + + +def test_highriskemployee_remove_risk_tags_command(code42_sdk_mock): + client = create_client(code42_sdk_mock) + _, _, res = highriskemployee_remove_risk_tags_command( + client, {"username": "user1@example.com", "risktags": ["FLIGHT_RISK", "CONTRACT_EMPLOYEE"]} + ) + expected_user_id = "123412341234123412" # value found in GET_USER_RESPONSE + assert res == expected_user_id + code42_sdk_mock.detectionlists.remove_user_risk_tags.assert_called_once_with( + expected_user_id, ["FLIGHT_RISK", "CONTRACT_EMPLOYEE"] + ) + + +def test_security_data_search_command(code42_file_events_mock): + client = create_client(code42_file_events_mock) _, _, 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] + actual_query = code42_file_events_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" @@ -846,9 +1211,7 @@ def test_security_data_search_command(code42_sdk_mock): 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 - ) + client = create_client(code42_sdk_mock) fetch_incidents( client=client, last_run={"last_fetch": None}, @@ -861,10 +1224,8 @@ def test_fetch_incidents_handles_single_severity(code42_sdk_mock): 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 - ) +def test_fetch_incidents_handles_multi_severity(code42_fetch_incidents_mock): + client = create_client(code42_fetch_incidents_mock) fetch_incidents( client=client, last_run={"last_fetch": None}, @@ -874,14 +1235,12 @@ def test_fetch_incidents_handles_multi_severity(code42_sdk_mock): 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]) + assert "HIGH" in str(code42_fetch_incidents_mock.alerts.search.call_args[0][0]) + assert "LOW" in str(code42_fetch_incidents_mock.alerts.search.call_args[0][0]) -def test_fetch_incidents_first_run(code42_sdk_mock): - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None - ) +def test_fetch_incidents_first_run(code42_fetch_incidents_mock): + client = create_client(code42_fetch_incidents_mock) next_run, incidents, remaining_incidents = fetch_incidents( client=client, last_run={"last_fetch": None}, @@ -895,12 +1254,10 @@ def test_fetch_incidents_first_run(code42_sdk_mock): assert next_run["last_fetch"] -def test_fetch_incidents_next_run(code42_sdk_mock): +def test_fetch_incidents_next_run(code42_fetch_incidents_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"))) - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None - ) + client = create_client(code42_fetch_incidents_mock) next_run, incidents, remaining_incidents = fetch_incidents( client=client, last_run={"last_fetch": mock_timestamp}, @@ -914,12 +1271,10 @@ def test_fetch_incidents_next_run(code42_sdk_mock): assert next_run["last_fetch"] -def test_fetch_incidents_fetch_limit(code42_sdk_mock): +def test_fetch_incidents_fetch_limit(code42_fetch_incidents_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"))) - client = Code42Client( - sdk=code42_sdk_mock, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=None - ) + client = create_client(code42_fetch_incidents_mock) next_run, incidents, remaining_incidents = fetch_incidents( client=client, last_run={"last_fetch": mock_timestamp}, diff --git a/Packs/Code42/Integrations/Code42/README.md b/Packs/Code42/Integrations/Code42/README.md index f19d5044bfe4..f14c1c322cf5 100644 --- a/Packs/Code42/Integrations/Code42/README.md +++ b/Packs/Code42/Integrations/Code42/README.md @@ -10,9 +10,6 @@ Code42 provides simple, fast detection and response to everyday data loss from i This integration was integrated and tested with the fully-hosted SaaS implementation of Code42 and requires a Platinum level subscription. -## Code42 Playbook ---- - ## Use Cases --- @@ -20,425 +17,445 @@ 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 Cortex XSOAR ---- -1. Navigate to __Settings__ > __Integrations__ > __Servers & Services__. +1. Navigate to **Settings** > **Integrations** > **Servers & Services**. 2. Search for Code42. -3. Click __Add instance__ to create and configure a new integration instance. - * __Name__: a textual name for the integration instance. - * __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 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. - * __Include the list of files in returned incidents.__: If checked, will also fetch the file events associated with the alert. -4. Click __Test__ to validate the URLs, token, and connection. - -## Fetched Incidents Data ---- - -* ID -* Occurred -* Username -* Name -* Description -* State -* Type -* Severity +3. Click **Add instance** to create and configure a new integration instance. +| **Parameter** | **Description** | **Required** | +| --- | --- | --- | +| console_url | Code42 Console URL for the pod your Code42 instance is running in | True | +| credentials | | True | +| isFetch | Fetch incidents | False | +| incidentType | Incident type | False | +| alert_severity | Alert severities to fetch when fetching incidents | False | +| fetch_time | First fetch time range \(<number> <time unit>, e.g., 1 hour, 30 minutes\) | False | +| fetch_limit | Alerts to fetch per run; note that increasing this value may result in slow performance if too many results are returned at once | False | +| include_files | Include the list of files in returned incidents. | False | + +4. Click **Test** to validate the URLs, token, and connection. ## Commands ---- You can execute these commands from the Demisto CLI, as part of an automation, or in a playbook. After you successfully execute a command, a DBot message appears in the War Room with the command details. +### code42-securitydata-search +*** +Searches for a file in Security Data by JSON query, hash, username, device hostname, exfiltration type, or a combination of parameters. At least one argument must be passed in the command. If a JSON argument is passed, it will be used to the exclusion of other parameters, otherwise parameters will be combined with an AND clause. -1. code42-securitydata-search -2. code42-alert-get -3. code42-departingemployee-add -4. code42-departingemployee-remove -5. code42-alert-resolve - -### 1. code42-securitydata-search ---- -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 -* Customer Cloud Admin -##### Base Command +#### Base Command `code42-securitydata-search` - -##### Input +#### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| json | JSON query payload using Code42 query syntax | Optional | -| hash | MD5 or SHA256 hash of file to search for | Optional | -| username | Username to search for | Optional | -| hostname | Hostname to search for | Optional | -| exposure | Exposure types to search for | Optional | -| results | Number of results to return, default is 100 | Optional | +| json | JSON query payload using Code42 query syntax. | Optional | +| hash | MD5 or SHA256 hash of the file to search for. | Optional | +| username | Username to search for. | Optional | +| hostname | Hostname to search for. | Optional | +| exposure | Exposure types to search for. Can be "RemovableMedia", "ApplicationRead", "CloudStorage", "IsPublic", "SharedViaLink", or "SharedViaDomain". | Optional | +| results | The number of results to return. The default is 100. | Optional | -##### Context Output +#### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| Code42.SecurityData.EventTimestamp | date | Timestamp for event | -| Code42.SecurityData.FileCreated | date | File creation date | -| Code42.SecurityData.EndpointID | string | Code42 device ID | -| Code42.SecurityData.DeviceUsername | string | Username that device is associated with in Code42 | -| Code42.SecurityData.EmailFrom | string | Sender email address for email exfiltration events | -| Code42.SecurityData.EmailTo | string | Recipient emial address for email exfiltration events | -| Code42.SecurityData.EmailSubject | string | Email subject line for email exfiltration events | -| Code42.SecurityData.EventID | string | Security Data event ID | -| Code42.SecurityData.EventType | string | Type of Security Data event | -| Code42.SecurityData.FileCategory | string | Type of file as determined by Code42 engine | -| Code42.SecurityData.FileOwner | string | Owner of file | -| Code42.SecurityData.FileName | string | File name | -| Code42.SecurityData.FilePath | string | Path to file | -| Code42.SecurityData.FileSize | number | Size of file in bytes | -| Code42.SecurityData.FileModified | date | File modification date | -| Code42.SecurityData.FileMD5 | string | MD5 hash of file | -| Code42.SecurityData.FileHostname | string | Hostname where file event was captured | -| Code42.SecurityData.DevicePrivateIPAddress | string | Private IP addresses of device where event was captured | -| Code42.SecurityData.DevicePublicIPAddress | string | Public IP address of device where event was captured | -| Code42.SecurityData.RemovableMediaType | string | Type of removate media | -| Code42.SecurityData.RemovableMediaCapacity | number | Total capacity of removable media in bytes | -| Code42.SecurityData.RemovableMediaMediaName | string | Full name of removable media | -| Code42.SecurityData.RemovableMediaName | string | Name of removable media | -| Code42.SecurityData.RemovableMediaSerialNumber | string | Serial number for removable medial device | -| Code42.SecurityData.RemovableMediaVendor | string | Vendor name for removable device | -| Code42.SecurityData.FileSHA256 | string | SHA256 hash of file | -| Code42.SecurityData.FileShared | boolean | Whether file is shared using cloud file service | -| Code42.SecurityData.FileSharedWith | string | Accounts that file is shared with on cloud file service | -| Code42.SecurityData.Source | string | Source of file event, Cloud or Endpoint | -| Code42.SecurityData.ApplicationTabURL | string | URL associated with application read event | -| Code42.SecurityData.ProcessName | string | Process name for application read event | -| Code42.SecurityData.ProcessOwner | string | Process owner for application read event | -| Code42.SecurityData.WindowTitle | string | Process name for application read event | -| Code42.SecurityData.FileURL | string | URL of file on cloud file service | -| Code42.SecurityData.Exposure | string | Exposure type for event | -| Code42.SecurityData.SharingTypeAdded | string | Type of sharing added to file | -| File.Name | string | File name | -| File.Path | string | File path | -| File.Size | number | File size in bytes | -| File.MD5 | string | MD5 hash of file | -| File.SHA256 | string | FSHA256 hash of file | -| File.Hostname | string | Hostname where file event was captured | - - -##### Command Example -``` -!code42-securitydata-search hash=eef8b12d2ed0d6a69fe77699d5640c7b exposure=CloudStorage,ApplicationRead -``` - -##### Context Example -``` -{ - "SecurityData": [ - { - "ApplicationTabURL": "https://mail.google.com/mail/u/0/?zx=78517y156trj#inbox", - "DevicePrivateIPAddress": [ - "192.168.7.7", - "0:0:0:0:0:0:0:1", - "127.0.0.1" - ], - "DeviceUsername": "john.user@123.org", - "EndpointID": "922302903141234234", - "EventID": "0_c346c59b-5ea1-4e5d-ac02-92079567a683_922302903141255753_939560749717017940_751", - "EventTimestamp": "2020-02-03T22:32:10.892Z", - "EventType": "READ_BY_APP", - "Exposure": [ - "ApplicationRead" - ], - "FileCategory": "IMAGE", - "FileCreated": "2019-10-07T21:46:09.281Z", - "FileHostname": "DESKTOP-0004", - "FileMD5": "eef8b12d2ed0d6a69fe77699d5640c7b", - "FileModified": "2019-10-07T21:46:09.889Z", - "FileName": "ProductPhoto.jpg", - "FileOwner": "john.user", - "FilePath": "C:/Users/john.user/Documents/", - "FileSHA256": "5e25e54e1cc43ed07c6e888464cb98e5f5343aa7aa485d174d9649be780a17b9", - "FileSize": 333114, - "ProcessName": "\\Device\\HarddiskVolume4\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", - "ProcessOwner": "john.user", - "Source": "Endpoint", - "WindowTitle": [ - "Inbox (1) - john.user@c123.org - 123 Org Mail - Google Chrome" - ] - }, - { - "DevicePrivateIPAddress": [ - "192.168.7.7", - "0:0:0:0:0:0:0:1", - "127.0.0.1" - ], - "DeviceUsername": "john.user@123.org", - "EndpointID": "922302903141234234", - "EventID": "0_a2e51c67-8719-4436-a3b5-c7c3724a3144_922302903141255753_939559658795324756_45", - "EventTimestamp": "2020-02-03T22:22:04.375Z", - "EventType": "READ_BY_APP", - "Exposure": [ - "ApplicationRead" - ], - "FileCategory": "IMAGE", - "FileCreated": "2019-10-07T21:46:09.281Z", - "FileHostname": "DESKTOP-0004", - "FileMD5": "eef8b12d2ed0d6a69fe77699d5640c7b", - "FileModified": "2019-10-07T21:46:09.889Z", - "FileName": "ProductPhoto.jpg", - "FileOwner": "john.user", - "FilePath": "C:/Users/john.user/Documents/", - "FileSHA256": "5e25e54e1cc43ed07c6e888464cb98e5f5343aa7aa485d174d9649be780a17b9", - "FileSize": 333114, - "ProcessName": "\\Device\\HarddiskVolume4\\Windows\\System32\\MicrosoftEdgeCP.exe", - "ProcessOwner": "michelle.goldberg", - "Source": "Endpoint", - "WindowTitle": [ - "Inbox (7) - jju12431983@gmail.com - Gmail ‎- Microsoft Edge" - ] - } - ] -} -``` - -##### Human Readable Output +| Code42.SecurityData.EventTimestamp | date | Timestamp for the event. | +| Code42.SecurityData.FileCreated | date | File creation date. | +| Code42.SecurityData.EndpointID | string | Code42 device ID. | +| Code42.SecurityData.DeviceUsername | string | The username that the device is associated with in Code42. | +| Code42.SecurityData.EmailFrom | string | The sender email address for email exfiltration events. | +| Code42.SecurityData.EmailTo | string | The recipient email address for email exfiltration events. | +| Code42.SecurityData.EmailSubject | string | The email subject line for email exfiltration events. | +| Code42.SecurityData.EventID | string | The Security Data event ID. | +| Code42.SecurityData.EventType | string | The type of Security Data event. | +| Code42.SecurityData.FileCategory | string | The file type, as determined by Code42 engine. | +| Code42.SecurityData.FileOwner | string | The owner of the file. | +| Code42.SecurityData.FileName | string | The file name. | +| Code42.SecurityData.FilePath | string | The path to file. | +| Code42.SecurityData.FileSize | number | The size of the file \(in bytes\). | +| Code42.SecurityData.FileModified | date | The date the file was last modified. | +| Code42.SecurityData.FileMD5 | string | MD5 hash of the file. | +| Code42.SecurityData.FileHostname | string | Hostname where the file event was captured. | +| Code42.SecurityData.DevicePrivateIPAddress | string | Private IP addresses of the device where the event was captured. | +| Code42.SecurityData.DevicePublicIPAddress | string | Public IP address of the device where the event was captured. | +| Code42.SecurityData.RemovableMediaType | string | Type of removable media. | +| Code42.SecurityData.RemovableMediaCapacity | number | Total capacity of removable media \(in bytes\). | +| Code42.SecurityData.RemovableMediaMediaName | string | The full name of the removable media. | +| Code42.SecurityData.RemovableMediaName | string | The name of the removable media. | +| Code42.SecurityData.RemovableMediaSerialNumber | string | The serial number for the removable medial device. | +| Code42.SecurityData.RemovableMediaVendor | string | The vendor name for removable device. | +| Code42.SecurityData.FileSHA256 | string | The SHA256 hash of the file. | +| Code42.SecurityData.FileShared | boolean | Whether the file is shared using a cloud file service. | +| Code42.SecurityData.FileSharedWith | string | Accounts that the file is shared with on a cloud file service. | +| Code42.SecurityData.Source | string | The source of the file event. Can be "Cloud" or "Endpoint". | +| Code42.SecurityData.ApplicationTabURL | string | The URL associated with the application read event. | +| Code42.SecurityData.ProcessName | string | The process name for the application read event. | +| Code42.SecurityData.ProcessOwner | string | The process owner for the application read event. | +| Code42.SecurityData.WindowTitle | string | The process name for the application read event. | +| Code42.SecurityData.FileURL | string | The URL of the file on a cloud file service. | +| Code42.SecurityData.Exposure | string | The event exposure type. | +| Code42.SecurityData.SharingTypeAdded | string | The type of sharing added to the file. | +| File.Name | string | The file name. | +| File.Path | string | The file path. | +| File.Size | number | The file size \(in bytes\). | +| File.MD5 | string | The MD5 hash of the file. | +| File.SHA256 | string | The SHA256 hash of the file. | +| File.Hostname | string | The hostname where the file event was captured. | + + +#### Command Example +```!code42-securitydata-search hash=eef8b12d2ed0d6a69fe77699d5640c7b exposure=CloudStorage,ApplicationRead``` + +#### Human Readable Output | **EventType** | **FileName** | **FileSize** | **FileHostname** | **FileOwner** | **FileCategory** | | --- | --- | --- | --- | --- | --- | | READ\_BY\_APP | ProductPhoto.jpg | 333114 | DESKTOP-001 | john.user | IMAGE | -### 2. code42-alert-get ---- +### code42-alert-get +*** Retrieve alert details by alert ID -##### Required Permissions -This command requires one of the following roles: -* Security Center User -* Customer Cloud Admin - -##### Base Command +#### Base Command `code42-alert-get` - -##### Input +#### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| id | Alert ID to retrieve | Required | +| id | The alert ID to retrieve. Alert IDs are associated with alerts that are fetched via fetch-incidents. | Required | -##### Context Output +#### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| Code42.SecurityAlert.Username | string | Username associated with alert | -| Code42.SecurityAlert.Occurred | date | Alert timestamp | -| Code42.SecurityAlert.Description | string | Description of alert | -| Code42.SecurityAlert.ID | string | Alert ID | -| Code42.SecurityAlert.Name | string | Alert rule name that generated alert | -| Code42.SecurityAlert.State | string | Alert state | -| Code42.SecurityAlert.Type | string | Type of alert | -| Code42.SecurityAlert.Severity | string | Severity of alert | - - -##### Command Example -``` -!code42-alert-get id="a23557a7-8ca9-4ec6-803f-6a46a2aeca62" -``` - -##### Context Example -``` -{ - "SecurityAlert": [ - { - "ID": "a23557a7-8ca9-4ec6-803f-6a46a2aeca62", - "Name": "Google Drive - Public via Direct Link", - "Occurred": "2019-10-08T17:38:19.0801650Z", - "Severity": "LOW", - "State": "OPEN", - "Type": "FED_CLOUD_SHARE_PERMISSIONS", - "Username": "john.user@123.org" - } - ] -} -``` - -##### Human Readable Output +| Code42.SecurityAlert.Username | string | The username associated with the alert. | +| Code42.SecurityAlert.Occurred | date | The timestamp when the alert occurred. | +| Code42.SecurityAlert.Description | string | The description of the alert. | +| Code42.SecurityAlert.ID | string | The alert ID. | +| Code42.SecurityAlert.Name | string | The alert rule name that generated the alert. | +| Code42.SecurityAlert.State | string | The alert state. | +| Code42.SecurityAlert.Type | string | The alert type. | +| Code42.SecurityAlert.Severity | string | The severity of the alert. | + + +#### Command Example +```!code42-alert-get id="a23557a7-8ca9-4ec6-803f-6a46a2aeca62"``` + +#### Human Readable Output | **Type** | **Occurred** | **Username** | **Name** | **Description** | **State** | **ID** | | --- | --- | --- | --- | --- | --- | --- | | FED\_CLOUD\_SHARE_PERMISSIONS | 2019-10-08T17:38:19.0801650Z | john.user@123.org | Google Drive - Public via Direct Link | Alert for public Google Drive files | OPEN | a23557a7-8ca9-4ec6-803f-6a46a2aeca62 | -### 3. code42-departingemployee-add ---- -Add a user to the Departing Employee List -##### Required Permissions +### code42-alert-resolve +*** +Resolves a Code42 Security alert. -This command requires one of the following roles: - -* Customer Cloud Admin -* Security Center User + (Org Security Viewer or Cross Org Security Viewer) -##### Base Command +#### Base Command + +`code42-alert-resolve` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| id | The alert ID to resolve. Alert IDs are associated with alerts that are fetched via fetch-incidents. | Required | + + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Code42.SecurityAlert.ID | string | The alert ID of the resolved alert. | + + +#### Command Example +```!code42-alert-resolve id="eb272d18-bc82-4680-b570-ac5d61c6cca6"``` + +#### Human Readable Output + +| **ID** | +| --- | +| eb272d18-bc82-4680-b570-ac5d61c6cca6 | + + +### code42-departingemployee-add +*** +Adds a user to the Departing Employee List. + + +#### Base Command `code42-departingemployee-add` -##### Input +#### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| 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 | +| username | The username to add to the Departing Employee List. | Required | +| departuredate | The departure date for the employee, in the format YYYY-MM-DD. | Optional | +| note | Note to attach to the Departing Employee. | Optional | -##### Context Output +#### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| 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 | - - -##### Command Example -``` -!code42-departingemployee-add username="john.user@123.org" departuredate="2020-02-28" note="Leaving for competitor" -``` - -##### Context Example -``` -{ - "DepartingEmployee": { - "UserID": "892", - "DepartureDate": "2020-02-28", - "Note": "Leaving for competitor", - "Username": "john.user@123.org" - } -} -``` - -##### Human Readable Output +| Code42.DepartingEmployee.UserID | string | Internal Code42 User ID for the Departing Employee. | +| Code42.DepartingEmployee.Username | string | The username of the Departing Employee. | +| Code42.DepartingEmployee.Note | string | Note associated with the Departing Employee. | +| Code42.DepartingEmployee.DepartureDate | unknown | The departure date for the Departing Employee. | + + +#### Command Example +```!code42-departingemployee-add username="john.user@123.org" departuredate="2020-02-28" note="Leaving for competitor"``` + +#### Human Readable Output | **UserID** | **DepartureDate** | **Note** | **Username** | | --- | --- | --- | --- | -| 123 | 2020-02-28 | Leaving for competitor | john.user@123.org | +| 123 | 2020-02-28 | Leaving for competitor | john.user@example.com | -### 4. code42-departingemployee-remove ---- -Remove a user from the Departing Employee List -##### Required Permissions - -This command requires one of the following roles: +### code42-departingemployee-remove +*** +Removes a user from the Departing Employee List. -* Customer Cloud Admin -* Security Center User + (Org Security Viewer or Cross Org Security Viewer) -##### Base Command +#### Base Command `code42-departingemployee-remove` -##### Input +#### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| username | Username to remove from the Departing Employee List | Optional | +| username | The username to remove from the Departing Employee List. | Required | -##### Context Output +#### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| Code42.DepartingEmployee.UserID | unknown | Internal Code42 User ID for Departing Employee | -| Code42.DepartingEmployee.Username | unknown | Username for Departing Employee | - +| Code42.DepartingEmployee.UserID | string | Internal Code42 User ID for the Departing Employee. | +| Code42.DepartingEmployee.Username | string | The username of the Departing Employee. | -##### Command Example -``` -!code42-departingemployee-remove username="john.user@123.org" -``` -##### Context Example -``` -{ - "DepartingEmployee": { - "UserID": "892", - "Username": "john.user@123.org" - } -} -``` +#### Command Example +```!code42-departingemployee-remove username="john.user@example.com"``` -##### Human Readable Output +#### Human Readable Output | **UserID** | **Username** | | --- | --- | -| 123 | john.user@123.org | +| 123 | john.user@example.com | -### 5. code42-alert-resolve ---- -Resolve a Code42 Security alert -##### Required Permissions -This command requires one of the following roles: +### code42-departingemployee-get-all +*** +Get all employees on the Departing Employee List. -* Security Center User -* Customer Cloud Admin -##### Base Command +#### Base Command -`code42-alert-resolve` -##### Input +`code42-departingemployee-get-all` +#### Input + +There are no input arguments for this command. + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Code42.DepartingEmployee.UserID | string | Internal Code42 User ID for the Departing Employee. | +| Code42.DepartingEmployee.Username | string | The username of the Departing Employee. | +| Code42.DepartingEmployee.Note | string | Note associated with the Departing Employee. | +| Code42.DepartingEmployee.DepartureDate | unknown | The departure date for the Departing Employee. | + + +#### Command Example +```!code42-departingemployee-get-all``` + +#### Human Readable Output + +| **UserID** | **DepartureDate** | **Note** | **Username** | +| --- | --- | --- | --- | +| 123 | 2020-02-28 | Leaving for competitor | john.user@example.com | + + +### code42-highriskemployee-add +*** +Removes a user from the High Risk Employee List. + + +#### Base Command + +`code42-highriskemployee-add` +#### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | -| id | Alert ID to resolve | Required | +| username | The username to add to the High Risk Employee List. | Required | +| note | Note to attach to the High Risk Employee. | Optional | -##### Context Output +#### Context Output | **Path** | **Type** | **Description** | | --- | --- | --- | -| Code42.SecurityAlert.ID | string | Alert ID | +| Code42.HighRiskEmployee.UserID | string | Internal Code42 User ID for the High Risk Employee. | +| Code42.HighRiskEmployee.Username | string | The username of the High Risk Employee. | +| Code42.HighRiskEmployee.Note | string | Note associated with the High Risk Employee. | -##### Command Example -``` -!code42-alert-resolve id="eb272d18-bc82-4680-b570-ac5d61c6cca6" -``` +#### Command Example +```!code42-highriskemployee-add username="john.user@123.org" note="Risky activity"``` -##### Context Example -``` -{ - "SecurityAlert": { - "ID": "eb272d18-bc82-4680-b570-ac5d61c6cca6" - } -} -``` +#### Human Readable Output -##### Human Readable Output +| **UserID** | **Note** | **Username** | +| --- | --- | --- | +| 123 | Leaving for competitor | john.user@example.com | -| **ID** | -| --- | -| eb272d18-bc82-4680-b570-ac5d61c6cca6 | -## Additional Information ---- -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) +### code42-highriskemployee-remove +*** +Removes a user from the High Risk Employee List. -## Known Limitations ---- -## Troubleshooting ---- +#### Base Command + +`code42-highriskemployee-remove` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| username | The username to remove from the High Risk Employee List. | Required | + + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Code42.HighRiskEmployee.UserID | unknown | Internal Code42 User ID for the High Risk Employee. | +| Code42.HighRiskEmployee.Username | unknown | The username of the High Risk Employee. | + + +#### Command Example +```!code42-highriskemployee-remove username="john.user@example.com"``` + +#### Human Readable Output + +| **UserID** | **Username** | +| --- | --- | +| 123 | john.user@example.com | + + +### code42-highriskemployee-get-all +*** +Get all employees on the High Risk Employee List. + + +#### Base Command + +`code42-highriskemployee-get-all` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| risktags | To filter results by employees who have these risk tags. | Optional | + + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Code42.HighRiskEmployee.UserID | string | Internal Code42 User ID for the High Risk Employee. | +| Code42.HighRiskEmployee.Username | string | The username of the High Risk Employee. | +| Code42.HighRiskEmployee.Note | string | Note associated with the High Risk Employee. | + + +#### Command Example +```!code42-highriskemployee-get-all risktags="PERFORMANCE_CONCERNS"``` + +#### Human Readable Output + +| **UserID** | **Note** | **Username** | +| --- | --- | --- | +| 123 | Leaving for competitor | john.user@example.com | + + +### code42-highriskemployee-add-risk-tags +*** + + +#### Base Command + +`code42-highriskemployee-add-risk-tags` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| username | The username of the High Risk Employee. | Required | +| risktags | Risk tags to associate with the High Risk Employee. | Required | + + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Code42.HighRiskEmployee.UserID | string | Internal Code42 User ID for the Departing Employee. | +| Code42.HighRiskEmployee.Username | string | The username of the High Risk Employee. | +| Code42.HighRiskEmployee.RiskTags | unknown | Risk tags to associate with the High Risk Employee. | + + +#### Command Example +```!code42-highriskemployee-add-risk-tags username="john.user@example.com" risktags="PERFORMANCE_CONCERNS"``` + +#### Human Readable Output + +| **UserID** | **RiskTags** | **Username** | +| --- | --- | --- | +| 123 | FLIGHT_RISK | john.user@example.com | + + +### code42-highriskemployee-remove-risk-tags +*** + + + +#### Base Command + +`code42-highriskemployee-remove-risk-tags` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| username | The username of the High Risk Employee. | Required | +| risktags | Risk tags to disassociate from the High Risk Employee. | Required | + + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| Code42.HighRiskEmployee.UserID | string | Internal Code42 User ID for the Departing Employee. | +| Code42.HighRiskEmployee.Username | string | The username of the High Risk Employee. | +| Code42.HighRiskEmployee.RiskTags | unknown | Risk tags to disassociate from the High Risk Employee. | + + +#### Command Example +```!code42-highriskemployee-remove-risk-tags username="john.user@example.com" risktags="PERFORMANCE_CONCERNS"``` + +#### Human Readable Output + +| **UserID** | **RiskTags** | **Username** | +| --- | --- | --- | +| 123 | FLIGHT_RISK | john.user@example.com | diff --git a/Packs/Code42/Integrations/Code42/integration-Code42.yml b/Packs/Code42/Integrations/Code42/integration-Code42.yml index 3668c3a97b75..1bf98b61977e 100644 --- a/Packs/Code42/Integrations/Code42/integration-Code42.yml +++ b/Packs/Code42/Integrations/Code42/integration-Code42.yml @@ -179,6 +179,7 @@ script: "DeviceUsername", ] + SECURITY_ALERT_HEADERS = ["Type", "Occurred", "Username", "Name", "Description", "State", "ID"] @@ -204,6 +205,20 @@ script: return alert_query + def _get_all_high_risk_employees_from_page(page, risk_tags): + res = [] + for employee in page["items"]: + if not risk_tags: + res.append(employee) + continue + + employee_tags = employee.get("riskFactors") + # If the employee risk tags contain all the given risk tags + if employee_tags and set(risk_tags) <= set(employee_tags): + res.append(employee) + return res + + class Code42Client(BaseClient): """ Client will implement the service API, should not contain Cortex XSOAR logic. @@ -217,64 +232,83 @@ script: py42.settings.set_user_agent_suffix("Cortex XSOAR") def add_user_to_departing_employee(self, username, departure_date=None, note=None): - try: - 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 + 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) return user_id - def fetch_alerts(self, start_time, event_severity_filter): - try: - query = _create_alert_query(event_severity_filter, start_time) - res = self._sdk.alerts.search(query) - except Exception: - return None - return res["alerts"] - - def get_alert_details(self, alert_id): - try: - res = self._sdk.alerts.get_details(alert_id) - except Exception: - return None - return res["alerts"][0] + def remove_user_from_departing_employee(self, username): + user_id = self.get_user_id(username) + self._sdk.detectionlists.departing_employee.remove(user_id) + return user_id - def get_current_user(self): - try: - res = self._sdk.users.get_current() - except Exception: - return None + def get_all_departing_employees(self): + res = [] + pages = self._sdk.detectionlists.departing_employee.get_all() + for page in pages: + employees = page["items"] + res.extend(employees) return res - def remove_user_from_departing_employee(self, username): - try: - user_id = self.get_user_id(username) - self._sdk.detectionlists.departing_employee.remove(user_id) - except Exception: - return None + def add_user_to_high_risk_employee(self, username, note=None): + user_id = self.get_user_id(username) + self._sdk.detectionlists.high_risk_employee.add(user_id) + if note: + self._sdk.detectionlists.update_user_notes(user_id, note) + return user_id + + def remove_user_from_high_risk_employee(self, username): + user_id = self.get_user_id(username) + self._sdk.detectionlists.high_risk_employee.remove(user_id) + return user_id + + def add_user_risk_tags(self, username, risk_tags): + user_id = self.get_user_id(username) + self._sdk.detectionlists.add_user_risk_tags(user_id, risk_tags) return user_id + def remove_user_risk_tags(self, username, risk_tags): + user_id = self.get_user_id(username) + self._sdk.detectionlists.remove_user_risk_tags(user_id, risk_tags) + return user_id + + def get_all_high_risk_employees(self, risk_tags=None): + if isinstance(risk_tags, str): + risk_tags = [risk_tags] + res = [] + pages = self._sdk.detectionlists.high_risk_employee.get_all() + for page in pages: + res.extend(_get_all_high_risk_employees_from_page(page, risk_tags)) + return res + + def fetch_alerts(self, start_time, event_severity_filter): + query = _create_alert_query(event_severity_filter, start_time) + res = self._sdk.alerts.search(query) + return res["alerts"] + + def get_alert_details(self, alert_id): + res = self._sdk.alerts.get_details(alert_id)["alerts"] + if not res: + raise Exception("No alert found with ID {0}.".format(alert_id)) + return res[0] + def resolve_alert(self, id): - try: - self._sdk.alerts.resolve(id) - except Exception: - return None + self._sdk.alerts.resolve(id) return id + def get_current_user(self): + res = self._sdk.users.get_current() + return res + def get_user_id(self, username): - try: - res = self._sdk.users.get_by_username(username) - except Exception: - return None - return res["users"][0]["userUid"] - - def search_json(self, payload): - try: - res = self._sdk.securitydata.search_file_events(payload) - except Exception: - return None + res = self._sdk.users.get_by_username(username)["users"] + if not res: + raise Exception("No user found with username {0}.".format(username)) + return res[0]["userUid"] + + def search_file_events(self, payload): + res = self._sdk.securitydata.search_file_events(payload) return res["fileEvents"] @@ -322,6 +356,7 @@ script: 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 @@ -492,50 +527,65 @@ script: return {v: obj.get(k) for k, v in context_mapper.items() if obj.get(k)} + def create_command_error_message(cmd, ex): + return "Failed to execute command {0} command. Error: {1}".format(cmd, str(ex)) + + + """Commands""" + + + @logger def alert_get_command(client, args): code42_securityalert_context = [] - alert = client.get_alert_details(args["id"]) - if alert: + try: + alert = client.get_alert_details(args["id"]) + if not alert: + return "No results found", {}, {} + code42_context = map_to_code42_alert_context(alert) code42_securityalert_context.append(code42_context) readable_outputs = tableToMarkdown( - f"Code42 Security Alert Results", + "Code42 Security Alert Results", code42_securityalert_context, headers=SECURITY_ALERT_HEADERS, ) return readable_outputs, {"Code42.SecurityAlert": code42_securityalert_context}, alert - else: - return "No results found", {}, {} + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) @logger def alert_resolve_command(client, args): code42_security_alert_context = [] - alert_id = client.resolve_alert(args["id"]) - if not alert_id: - return "No results found", {}, {} + try: + alert_id = client.resolve_alert(args["id"]) - # 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, - ) + 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( + "Code42 Security Alert Resolved", + code42_security_alert_context, + headers=SECURITY_ALERT_HEADERS, + ) + return ( + readable_outputs, + {"Code42.SecurityAlert": code42_security_alert_context}, + alert_details, + ) + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) @logger @@ -544,33 +594,134 @@ script: 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 + try: + user_id = client.add_user_to_departing_employee(username, departing_date, note) + de_context = { + "UserID": user_id, + "Username": username, + "DepartureDate": departing_date, + "Note": note, + } + readable_outputs = tableToMarkdown("Code42 Departing Employee List User Added", de_context) + return readable_outputs, {"Code42.DepartingEmployee": de_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) @logger def departingemployee_remove_command(client, args): username = args["username"] - user_id = client.remove_user_from_departing_employee(username) - if user_id: + try: + user_id = client.remove_user_from_departing_employee(username) de_context = {"UserID": user_id, "Username": username} readable_outputs = tableToMarkdown( - f"Code42 Departing Employee List User Removed", de_context + "Code42 Departing Employee List User Removed", de_context ) return readable_outputs, {"Code42.DepartingEmployee": de_context}, user_id - else: - return_error(message="Could not remove user from Departing Employee List") + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + + @logger + + def departingemployee_get_all_command(client, args): + try: + employees = client.get_all_departing_employees() + employees_context = [ + { + "UserID": e["userId"], + "Username": e["userName"], + "DepartureDate": e.get("departureDate"), + "Note": e["notes"], + } + for e in employees + ] + readable_outputs = tableToMarkdown("All Departing Employees", employees_context) + return ( + readable_outputs, + {"Code42.DepartingEmployee(val.UserID && val.UserID == obj.UserID)": employees_context}, + employees, + ) + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + + @logger + + def highriskemployee_add_command(client, args): + username = args["username"] + note = args.get("note") + try: + user_id = client.add_user_to_high_risk_employee(username, note) + hr_context = {"UserID": user_id, "Username": username} + readable_outputs = tableToMarkdown("Code42 High Risk Employee List User Added", hr_context) + return readable_outputs, {"Code42.HighRiskEmployee": hr_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + + @logger + + def highriskemployee_remove_command(client, args): + username = args["username"] + try: + user_id = client.remove_user_from_high_risk_employee(username) + hr_context = {"UserID": user_id, "Username": username} + readable_outputs = tableToMarkdown( + "Code42 High Risk Employee List User Removed", hr_context + ) + return readable_outputs, {"Code42.HighRiskEmployee": hr_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + + @logger + + def highriskemployee_get_all_command(client, args): + tags = args.get("risktags") + try: + employees = client.get_all_high_risk_employees(tags) + employees_context = [ + {"UserID": e["userId"], "Username": e["userName"], "Note": e["notes"]} + for e in employees + ] + readable_outputs = tableToMarkdown("Retrieved All High Risk Employees", employees_context) + return ( + readable_outputs, + {"Code42.HighRiskEmployee(val.UserID && val.UserID == obj.UserID)": employees_context}, + employees, + ) + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + + @logger + + def highriskemployee_add_risk_tags_command(client, args): + username = args["username"] + tags = args["risktags"] + try: + user_id = client.add_user_risk_tags(username, tags) + rt_context = {"UserID": user_id, "Username": username, "RiskTags": tags} + readable_outputs = tableToMarkdown("Code42 Risk Tags Added", rt_context) + return readable_outputs, {"Code42.HighRiskEmployee": rt_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) + + + @logger + + def highriskemployee_remove_risk_tags_command(client, args): + username = args["username"] + tags = args["risktags"] + try: + user_id = client.remove_user_risk_tags(username, tags) + rt_context = {"UserID": user_id, "Username": username, "RiskTags": tags} + readable_outputs = tableToMarkdown("Code42 Risk Tags Removed", rt_context) + return readable_outputs, {"Code42.HighRiskEmployee": rt_context}, user_id + except Exception as e: + return_error(create_command_error_message(demisto.command(), e)) def _create_incident_from_alert_details(details): @@ -667,7 +818,7 @@ script: 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) + return self._client.search_file_events(security_data_query) def fetch_incidents( @@ -699,11 +850,11 @@ script: file_context = [] # If JSON payload is passed as an argument, ignore all other args and search by JSON payload if _json is not None: - file_events = client.search_json(_json) + file_events = client.search_file_events(_json) else: # Build payload payload = build_query_payload(args) - file_events = client.search_json(payload) + file_events = client.search_file_events(payload) if file_events: for file_event in file_events: code42_context_event = map_to_code42_event_context(file_event) @@ -711,7 +862,7 @@ script: file_context_event = map_to_file_context(file_event) file_context.append(file_context_event) readable_outputs = tableToMarkdown( - f"Code42 Security Data Results", + "Code42 Security Data Results", code42_security_data_context, headers=SECURITY_EVENT_HEADERS, ) @@ -755,6 +906,12 @@ script: "code42-securitydata-search": securitydata_search_command, "code42-departingemployee-add": departingemployee_add_command, "code42-departingemployee-remove": departingemployee_remove_command, + "code42-departingemployee-get-all": departingemployee_get_all_command, + "code42-highriskemployee-add": highriskemployee_add_command, + "code42-highriskemployee-remove": highriskemployee_remove_command, + "code42-highriskemployee-get-all": highriskemployee_get_all_command, + "code42-highriskemployee-add-risk-tags": highriskemployee_add_risk_tags_command, + "code42-highriskemployee-remove-risk-tags": highriskemployee_remove_risk_tags_command, } command = demisto.command() if command == "test-module": @@ -972,6 +1129,16 @@ script: description: The severity of the alert. type: string description: Retrieve alert details by alert ID + - name: code42-alert-resolve + arguments: + - name: id + required: true + description: The alert ID to resolve. Alert IDs are associated with alerts that are fetched via fetch-incidents. + outputs: + - contextPath: Code42.SecurityAlert.ID + description: The alert ID of the resolved alert. + type: string + description: Resolves a Code42 Security alert. - name: code42-departingemployee-add arguments: - name: username @@ -997,23 +1164,108 @@ script: - name: code42-departingemployee-remove arguments: - name: username + required: true description: The username to remove from the Departing Employee List. outputs: - 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. + type: string description: Removes a user from the Departing Employee List. - - name: code42-alert-resolve + - name: code42-departingemployee-get-all + outputs: + - 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. + type: string + - contextPath: Code42.DepartingEmployee.Note + description: Note associated with the Departing Employee. + type: string + - contextPath: Code42.DepartingEmployee.DepartureDate + description: The departure date for the Departing Employee. + description: Get all employees on the Departing Employee List. + - name: code42-highriskemployee-add arguments: - - name: id + - name: username required: true - description: The alert ID to resolve. Alert IDs are associated with alerts that are fetched via fetch-incidents. + description: The username to add to the High Risk Employee List. + - name: note + description: Note to attach to the High Risk Employee. outputs: - - contextPath: Code42.SecurityAlert.ID - description: The alert ID of the resolved alert. + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the High Risk Employee. type: string - description: Resolves a Code42 Security alert. + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Note + description: Note associated with the High Risk Employee. + type: string + description: Removes a user from the High Risk Employee List. + - name: code42-highriskemployee-remove + arguments: + - name: username + required: true + description: The username to remove from the High Risk Employee List. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the High Risk Employee. + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + description: Removes a user from the High Risk Employee List. + - name: code42-highriskemployee-get-all + arguments: + - name: risktags + description: To filter results by employees who have these risk tags. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Note + description: Note associated with the High Risk Employee. + type: string + description: Get all employees on the High Risk Employee List. + - name: code42-highriskemployee-add-risk-tags + arguments: + - name: username + required: true + description: The username of the High Risk Employee. + - name: risktags + required: true + description: Risk tags to associate with the High Risk Employee. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the Departing Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.RiskTags + description: Risk tags to associate with the High Risk Employee. + - name: code42-highriskemployee-remove-risk-tags + arguments: + - name: username + required: true + description: The username of the High Risk Employee. + - name: risktags + required: true + description: Risk tags to disassociate from the High Risk Employee. + outputs: + - contextPath: Code42.HighRiskEmployee.UserID + description: Internal Code42 User ID for the Departing Employee. + type: string + - contextPath: Code42.HighRiskEmployee.Username + description: The username of the High Risk Employee. + type: string + - contextPath: Code42.HighRiskEmployee.RiskTags + description: Risk tags to disassociate from the High Risk Employee. dockerimage: demisto/py42:1.0.0.9242 isfetch: true runonce: false diff --git a/Packs/Code42/Playbooks/playbook-Code42_Exfiltration_Playbook_README.md b/Packs/Code42/Playbooks/playbook-Code42_Exfiltration_Playbook_README.md new file mode 100644 index 000000000000..d99283b652ad --- /dev/null +++ b/Packs/Code42/Playbooks/playbook-Code42_Exfiltration_Playbook_README.md @@ -0,0 +1,41 @@ +The Code42 Exfiltration playbook acts on Code42 Security Alerts, retrieves file event data, and allows security teams to remediate file exfiltration events by revoking access rights to cloud files or containing endpoints. + +## Dependencies +This playbook uses the following sub-playbooks, integrations, and scripts. + +### Sub-playbooks +* Active Directory - Get User Manager Details + +### Integrations +* jira-v2 +* CrowdstrikeFalcon + +### Scripts +This playbook does not use any scripts. + +### Commands +* send-mail +* closeInvestigation +* jira-create-issue +* cs-falcon-search-device +* code42-alert-resolve +* cs-falcon-contain-host + +## Playbook Inputs +--- + +| **Name** | **Description** | **Default Value** | **Required** | +| --- | --- | --- | --- | +| JiraProject | Jira Project for created incident ticket | Security | Optional | +| JiraType | Type of Jira ticket to create | Investigation | Optional | +| JiraSummary | Summary to use with Jira ticket creation | Code42 Security Alert for Demisto Incident ${incident.id} | Optional | +| ContainHostsMax | Maximum number of network hosts to contain. | 2 | Optional | +| DemistoInstanceURL | URL of Demisto instance for emails. | https://example.com/ | Optional | + +## Playbook Outputs +--- +There are no outputs for this playbook. + +## Playbook Image +--- +![Code42 Exfiltration Playbook](Insert the link to your image here) \ No newline at end of file diff --git a/Packs/Code42/Playbooks/playbook-Code42_File_Search.yml b/Packs/Code42/Playbooks/playbook-Code42_File_Search.yml index c2985a981f7a..50f5c72dfe40 100644 --- a/Packs/Code42/Playbooks/playbook-Code42_File_Search.yml +++ b/Packs/Code42/Playbooks/playbook-Code42_File_Search.yml @@ -296,7 +296,7 @@ outputs: - contextPath: Code42.SecurityData.EmailFrom description: Sender email address for email exfiltration events - contextPath: Code42.SecurityData.EmailTo - description: Recipient emial address for email exfiltration events + description: Recipient email address for email exfiltration events - contextPath: Code42.SecurityData.EmailSubject description: Email subject line for email exfiltration events - contextPath: Code42.SecurityData.EventID @@ -324,7 +324,7 @@ outputs: - contextPath: Code42.SecurityData.DevicePublicIPAddress description: Public IP address of device where event was captured - contextPath: Code42.SecurityData.RemovableMediaType - description: Type of removate media + description: Type of removable media - contextPath: Code42.SecurityData.RemovableMediaCapacity description: Total capacity of removable media in bytes - contextPath: Code42.SecurityData.RemovableMediaMediaName diff --git a/Packs/Code42/Playbooks/playbook-Code42_File_Search_README.md b/Packs/Code42/Playbooks/playbook-Code42_File_Search_README.md new file mode 100644 index 000000000000..fdc7ee39b468 --- /dev/null +++ b/Packs/Code42/Playbooks/playbook-Code42_File_Search_README.md @@ -0,0 +1,78 @@ +This playbook searches for files via Code42 security events by either MD5 or SHA256 hash. The data is output to the Code42.SecurityData context for use. + +## Dependencies +This playbook uses the following sub-playbooks, integrations, and scripts. + +### Sub-playbooks +This playbook does not use any sub-playbooks. + +### Integrations +This playbook does not use any integrations. + +### Scripts +This playbook does not use any scripts. + +### Commands +* code42-securitydata-search + +## Playbook Inputs +--- + +| **Name** | **Description** | **Default Value** | **Required** | +| --- | --- | --- | --- | +| MD5 | MD5 hash to search for | File.MD5 | Optional | +| SHA256 | SHA256 hash to search for | File.SHA256 | Optional | + +## Playbook Outputs +--- + +| **Path** | **Description** | **Type** | +| --- | --- | --- | +| Code42.SecurityData | Returned File Results | unknown | +| Code42.SecurityData.EventTimestamp | Timestamp for event | unknown | +| Code42.SecurityData.FileCreated | File creation date | unknown | +| Code42.SecurityData.EndpointID | Code42 device ID | unknown | +| Code42.SecurityData.DeviceUsername | Username that device is associated with in Code42 | unknown | +| Code42.SecurityData.EmailFrom | Sender email address for email exfiltration events | unknown | +| Code42.SecurityData.EmailTo | Recipient email address for email exfiltration events | unknown | +| Code42.SecurityData.EmailSubject | Email subject line for email exfiltration events | unknown | +| Code42.SecurityData.EventID | Security Data event ID | unknown | +| Code42.SecurityData.EventType | Type of Security Data event | unknown | +| Code42.SecurityData.FileCategory | Type of file as determined by Code42 engine | unknown | +| Code42.SecurityData.FileOwner | Owner of file | unknown | +| Code42.SecurityData.FileName | File name | unknown | +| Code42.SecurityData.FilePath | Path to file | unknown | +| Code42.SecurityData.FileSize | Size of file in bytes | unknown | +| Code42.SecurityData.FileModified | File modification date | unknown | +| Code42.SecurityData.FileMD5 | MD5 hash of file | unknown | +| Code42.SecurityData.FileHostname | Hostname where file event was captured | unknown | +| Code42.SecurityData.DevicePrivateIPAddress | Private IP addresses of device where event was captured | unknown | +| Code42.SecurityData.DevicePublicIPAddress | Public IP address of device where event was captured | unknown | +| Code42.SecurityData.RemovableMediaType | Type of removable media | unknown | +| Code42.SecurityData.RemovableMediaCapacity | Total capacity of removable media in bytes | unknown | +| Code42.SecurityData.RemovableMediaMediaName | Full name of removable media | unknown | +| Code42.SecurityData.RemovableMediaName | Name of removable media | unknown | +| Code42.SecurityData.RemovableMediaSerialNumber | Serial number for removable medial device | unknown | +| Code42.SecurityData.RemovableMediaVendor | Vendor name for removable device | unknown | +| Code42.SecurityData.FileSHA256 | SHA256 hash of file | unknown | +| Code42.SecurityData.FileShared | Whether file is shared using cloud file service | unknown | +| Code42.SecurityData.FileSharedWith | Accounts that file is shared with on cloud file service | unknown | +| Code42.SecurityData.Source | Source of file event, Cloud or Endpoint | unknown | +| Code42.SecurityData.ApplicationTabURL | URL associated with application read event | unknown | +| Code42.SecurityData.ProcessName | Process name for application read event | unknown | +| Code42.SecurityData.ProcessOwner | Process owner for application read event | unknown | +| Code42.SecurityData.WindowTitle | Process name for application read event | unknown | +| Code42.SecurityData.FileURL | URL of file on cloud file service | unknown | +| Code42.SecurityData.Exposure | Exposure type for event | unknown | +| Code42.SecurityData.SharingTypeAdded | Type of sharing added to file | unknown | +| File | The file object. | unknown | +| File.Name | File name | unknown | +| File.Path | File path | unknown | +| File.Size | File size in bytes | unknown | +| File.MD5 | MD5 hash of file | unknown | +| File.SHA256 | FSHA256 hash of file | unknown | +| File.Hostname | Hostname where file event was captured | unknown | + +## Playbook Image +--- +![Code42 File Search](Insert the link to your image here) \ No newline at end of file