diff --git a/Packs/Code42/IncidentFields/incidentfield-Code42_SecurityAlert_Timestamp.json b/Packs/Code42/IncidentFields/incidentfield-Code42_SecurityAlert_Timestamp.json index b0e5558f2162..cf39367d357c 100644 --- a/Packs/Code42/IncidentFields/incidentfield-Code42_SecurityAlert_Timestamp.json +++ b/Packs/Code42/IncidentFields/incidentfield-Code42_SecurityAlert_Timestamp.json @@ -31,7 +31,7 @@ "system": false, "systemAssociatedTypes": null, "threshold": 72, - "type": "shortText", + "type": "date", "unmapped": false, "unsearchable": false, "useAsKpi": false, diff --git a/Packs/Code42/IncidentFields/incidentfield-Code42_Username.json b/Packs/Code42/IncidentFields/incidentfield-Code42_Username.json index 16292af7b787..e9eb4acb4b47 100644 --- a/Packs/Code42/IncidentFields/incidentfield-Code42_Username.json +++ b/Packs/Code42/IncidentFields/incidentfield-Code42_Username.json @@ -1,5 +1,5 @@ { - "associatedToAll": true, + "associatedToAll": false, "associatedTypes": null, "breachScript": "", "caseInsensitive": true, diff --git a/Packs/Code42/Integrations/Code42/Code42.py b/Packs/Code42/Integrations/Code42/Code42.py index 4cd5a077125a..910a47fce21a 100644 --- a/Packs/Code42/Integrations/Code42/Code42.py +++ b/Packs/Code42/Integrations/Code42/Code42.py @@ -136,8 +136,7 @@ def _create_alert_query(event_severity_filter, start_time): def _get_all_high_risk_employees_from_page(page, risk_tags): res = [] - # Note: page is a `Py42Response` and has no `get()` method. - employees = page["items"] + employees = page.get("items") or [] for employee in employees: if not risk_tags: res.append(employee) @@ -198,8 +197,8 @@ def get_all_departing_employees(self, results): results = int(results) if results else None pages = self._get_sdk().detectionlists.departing_employee.get_all() for page in pages: - # Note: page is a `Py42Response` and has no `get()` method. - employees = page["items"] + page_json = json.loads(page.text) + employees = page_json.get("items") or [] for employee in employees: res.append(employee) if results and len(res) == results: @@ -236,7 +235,8 @@ def get_all_high_risk_employees(self, risk_tags, results): res = [] pages = self._get_sdk().detectionlists.high_risk_employee.get_all() for page in pages: - employees = _get_all_high_risk_employees_from_page(page, risk_tags) + page_json = json.loads(page.text) + employees = _get_all_high_risk_employees_from_page(page_json, risk_tags) for employee in employees: res.append(employee) if results and len(res) == results: @@ -246,10 +246,11 @@ def get_all_high_risk_employees(self, risk_tags, results): def fetch_alerts(self, start_time, event_severity_filter): query = _create_alert_query(event_severity_filter, start_time) res = self._get_sdk().alerts.search(query) - return res["alerts"] + return json.loads(res.text).get("alerts") def get_alert_details(self, alert_id): - res = self._get_sdk().alerts.get_details(alert_id)["alerts"] + py42_res = self._get_sdk().alerts.get_details(alert_id) + res = json.loads(py42_res.text).get("alerts") if not res: raise Code42AlertNotFoundError(alert_id) return res[0] @@ -263,7 +264,8 @@ def get_current_user(self): return res def get_user(self, username): - res = self._get_sdk().users.get_by_username(username)["users"] + py42_res = self._get_sdk().users.get_by_username(username) + res = json.loads(py42_res.text).get("users") if not res: raise Code42UserNotFoundError(username) return res[0] @@ -320,15 +322,16 @@ def remove_user_from_legal_hold_matter(self, username, matter_name): def get_org(self, org_name): org_pages = self._get_sdk().orgs.get_all() for org_page in org_pages: - orgs = org_page["orgs"] + page_json = json.loads(org_page.text) + orgs = page_json.get("orgs") for org in orgs: if org.get("orgName") == org_name: return org raise Code42OrgNotFoundError(org_name) def search_file_events(self, payload): - res = self._get_sdk().securitydata.search_file_events(payload) - return res["fileEvents"] + py42_res = self._get_sdk().securitydata.search_file_events(payload) + return json.loads(py42_res.text).get("fileEvents") def download_file(self, hash_arg): security_module = self._get_sdk().securitydata @@ -337,7 +340,7 @@ def download_file(self, hash_arg): elif _hash_is_sha256(hash_arg): return security_module.stream_file_by_sha256(hash_arg) else: - raise Exception("Unsupported hash. Must be SHA256 or MD5.") + raise Code42UnsupportedHashError() def _get_user_id(self, username): user_id = self.get_user(username).get("userUid") @@ -391,6 +394,20 @@ def __init__(self, org_name): ) +class Code42UnsupportedHashError(Exception): + def __init__(self): + super(Code42UnsupportedHashError, self).__init__( + "Unsupported hash. Must be SHA256 or MD5." + ) + + +class Code42MissingSearchArgumentsError(Exception): + def __init__(self): + super(Code42MissingSearchArgumentsError, self).__init__( + "No query args provided for searching Code42 security events." + ) + + class Code42LegalHoldMatterNotFoundError(Exception): def __init__(self, matter_name): super(Code42LegalHoldMatterNotFoundError, self).__init__( @@ -401,8 +418,9 @@ def __init__(self, matter_name): class Code42InvalidLegalHoldMembershipError(Exception): def __init__(self, username, matter_name): super(Code42InvalidLegalHoldMembershipError, self).__init__( - "User '{0}' is not an active member of legal hold matter '{1}'".format(username, - matter_name) + "User '{0}' is not an active member of legal hold matter '{1}'".format( + username, matter_name + ) ) @@ -468,6 +486,9 @@ def build_query_payload(args): username = args.get("username") exposure = args.get("exposure") + if not _hash and not hostname and not username and not exposure: + raise Code42MissingSearchArgumentsError() + search_args = FileEventQueryFilters(pg_size) search_args.append_result(_hash, _create_hash_filter) search_args.append_result(hostname, OSHostname.eq) @@ -1143,7 +1164,7 @@ def _fetch_alerts(self, start_query_time): return self._client.fetch_alerts(start_query_time, self._event_severity_filter) def _create_incident_from_alert(self, alert): - details = self._client.get_alert_details(alert["id"]) + details = self._client.get_alert_details(alert.get("id")) incident = _create_incident_from_alert_details(details) if self._include_files: details = self._relate_files_to_alert(details) @@ -1161,7 +1182,7 @@ def _relate_files_to_alert(self, alert_details): return 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"]) + security_data_query = map_observation_to_security_query(observation, alert_details.get("actor")) return self._client.search_file_events(security_data_query) diff --git a/Packs/Code42/Integrations/Code42/Code42_test.py b/Packs/Code42/Integrations/Code42/Code42_test.py index 807c28c74df6..8bfa6b9289ed 100644 --- a/Packs/Code42/Integrations/Code42/Code42_test.py +++ b/Packs/Code42/Integrations/Code42/Code42_test.py @@ -5,8 +5,6 @@ from py42.response import Py42Response from Code42 import ( Code42Client, - Code42AlertNotFoundError, - Code42UserNotFoundError, Code42LegalHoldMatterNotFoundError, Code42InvalidLegalHoldMembershipError, build_query_payload, @@ -34,6 +32,11 @@ legal_hold_remove_user_command, download_file_command, fetch_incidents, + Code42AlertNotFoundError, + Code42UserNotFoundError, + Code42OrgNotFoundError, + Code42UnsupportedHashError, + Code42MissingSearchArgumentsError, ) import time @@ -1293,8 +1296,13 @@ def assert_detection_list_outputs_match_response_items(outputs_list, response_it """TESTS""" -def test_client_lazily_inits_sdk(mocker): - mocker.patch("py42.sdk.from_local_account") +def test_client_lazily_inits_sdk(mocker, code42_sdk_mock): + sdk_factory_mock = mocker.patch("py42.sdk.from_local_account") + response_json_mock = """{"total": 1, "users": [{"username": "Test"}]}""" + code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response( + mocker, response_json_mock + ) + sdk_factory_mock.return_value = code42_sdk_mock # test that sdk does not init during ctor client = Code42Client(sdk=None, base_url=MOCK_URL, auth=MOCK_AUTH, verify=False, proxy=False) @@ -1305,23 +1313,28 @@ def test_client_lazily_inits_sdk(mocker): assert client._sdk is not None -def test_client_when_no_alert_found_raises_alert_not_found(code42_sdk_mock): - code42_sdk_mock.alerts.get_details.return_value = ( - json.loads('{"type$": "ALERT_DETAILS_RESPONSE", "alerts": []}') +def test_client_when_no_alert_found_raises_alert_not_found(mocker, code42_sdk_mock): + response_json = """{"alerts": []}""" + code42_sdk_mock.alerts.get_details.return_value = create_mock_code42_sdk_response( + mocker, response_json ) client = create_client(code42_sdk_mock) with pytest.raises(Code42AlertNotFoundError): client.get_alert_details("mock-id") -def test_client_when_no_user_found_raises_user_not_found(code42_sdk_mock): - code42_sdk_mock.users.get_by_username.return_value = json.loads('{"totalCount":0, "users":[]}') +def test_client_when_no_user_found_raises_user_not_found(mocker, code42_sdk_mock): + response_json = """{"totalCount": 0, "users": []}""" + code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response( + mocker, response_json + ) client = create_client(code42_sdk_mock) with pytest.raises(Code42UserNotFoundError): client.get_user("test@example.com") def test_client_add_to_matter_when_no_legal_hold_matter_found_raises_matter_not_found(code42_sdk_mock, mocker): + code42_sdk_mock.legalhold.get_all_matters.return_value = ( get_empty_legalhold_matters_response(mocker, MOCK_GET_ALL_MATTERS_RESPONSE) ) @@ -1331,8 +1344,9 @@ def test_client_add_to_matter_when_no_legal_hold_matter_found_raises_matter_not_ client.add_user_to_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME") -def test_client_add_to_matter_when_no_user_found_raises_user_not_found(code42_sdk_mock): - code42_sdk_mock.users.get_by_username.return_value = json.loads('{"totalCount":0, "users":[]}') +def test_client_add_to_matter_when_no_user_found_raises_user_not_found(mocker, code42_sdk_mock): + response_json = '{"totalCount":0, "users":[]}' + code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response(mocker, response_json) client = create_client(code42_sdk_mock) with pytest.raises(Code42UserNotFoundError): client.add_user_to_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME") @@ -1348,8 +1362,9 @@ def test_client_remove_from_matter_when_no_legal_hold_matter_found_raises_except client.remove_user_from_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME") -def test_client_remove_from_matter_when_no_user_found_raises_user_not_found(code42_sdk_mock): - code42_sdk_mock.users.get_by_username.return_value = json.loads('{"totalCount":0, "users":[]}') +def test_client_remove_from_matter_when_no_user_found_raises_user_not_found(mocker, code42_sdk_mock): + response_json = '{"totalCount":0, "users":[]}' + code42_sdk_mock.users.get_by_username.return_value = create_mock_code42_sdk_response(mocker, response_json) client = create_client(code42_sdk_mock) with pytest.raises(Code42UserNotFoundError): client.remove_user_from_legal_hold_matter("TESTUSERNAME", "TESTMATTERNAME") @@ -1725,6 +1740,23 @@ def test_user_create_command(code42_users_mock): assert cmd_res.outputs["Email"] == "new.user@example.com" +def test_user_create_command_when_org_not_found_raises_org_not_found(mocker, code42_users_mock): + response_json = """{"total": 0, "orgs": []}""" + code42_users_mock.orgs.get_all.return_value = create_mock_code42_sdk_response_generator( + mocker, [response_json] + ) + client = create_client(code42_users_mock) + with pytest.raises(Code42OrgNotFoundError): + user_create_command( + client, + { + "orgname": _TEST_ORG_NAME, + "username": "new.user@example.com", + "email": "new.user@example.com", + } + ) + + def test_user_block_command(code42_users_mock): client = create_client(code42_users_mock) cmd_res = user_block_command(client, {"username": "new.user@example.com"}) @@ -1798,6 +1830,12 @@ def test_security_data_search_command(code42_file_events_mock): assert output_item == mapped_event +def test_securitydata_search_command_when_not_given_any_queryable_args_raises_error(code42_file_events_mock): + client = create_client(code42_file_events_mock) + with pytest.raises(Code42MissingSearchArgumentsError): + securitydata_search_command(client, {}) + + def test_download_file_command_when_given_md5(code42_sdk_mock, mocker): fr = mocker.patch("Code42.fileResult") client = create_client(code42_sdk_mock) @@ -1817,6 +1855,15 @@ def test_download_file_command_when_given_sha256(code42_sdk_mock, mocker): assert fr.call_count == 1 +def test_download_file_when_given_other_hash_raises_unsupported_hash(code42_sdk_mock, mocker): + mocker.patch("Code42.fileResult") + _hash = "41966f10cc59ab466444add08974fde4cd37f88d79321d42da8e4c79b51c214941966f10cc59ab466444add08974fde4cd37" \ + "f88d79321d42da8e4c79b51c2149" + client = create_client(code42_sdk_mock) + with pytest.raises(Code42UnsupportedHashError): + _ = download_file_command(client, {"hash": _hash}) + + def test_fetch_when_no_significant_file_categories_ignores_filter( code42_fetch_incidents_mock, mocker ): @@ -1840,8 +1887,8 @@ def test_fetch_when_no_significant_file_categories_ignores_filter( assert "IMAGE" not in actual_query -def test_fetch_incidents_handles_single_severity(code42_sdk_mock): - client = create_client(code42_sdk_mock) +def test_fetch_incidents_handles_single_severity(code42_fetch_incidents_mock): + client = create_client(code42_fetch_incidents_mock) fetch_incidents( client=client, last_run={"last_fetch": None}, @@ -1851,7 +1898,7 @@ def test_fetch_incidents_handles_single_severity(code42_sdk_mock): include_files=True, integration_context=None, ) - assert "HIGH" 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]) def test_fetch_incidents_handles_multi_severity(code42_fetch_incidents_mock): diff --git a/Packs/Code42/Integrations/Code42/README.md b/Packs/Code42/Integrations/Code42/README.md index 9b455af17a16..6205cacc3456 100644 --- a/Packs/Code42/Integrations/Code42/README.md +++ b/Packs/Code42/Integrations/Code42/README.md @@ -1,5 +1,4 @@ Use the Code42 integration to identify potential data exfiltration from insider threats while speeding investigation and response by providing fast access to file events and metadata across physical and cloud environments. -This integration was integrated and tested with version xx of Code42 ## Configure Code42 on Cortex XSOAR 1. Navigate to **Settings** > **Integrations** > **Servers & Services**. diff --git a/Packs/Code42/Integrations/Code42/command_examples.txt b/Packs/Code42/Integrations/Code42/command_examples.txt index 39203c676b08..6771dd7d7158 100644 --- a/Packs/Code42/Integrations/Code42/command_examples.txt +++ b/Packs/Code42/Integrations/Code42/command_examples.txt @@ -3,8 +3,8 @@ !code42-departingemployee-add username="partner.demisto@example.com" departuredate="2020-010-28" note="Leaving for competitor" !code42-departingemployee-remove username="partner.demisto@example.com" !code42-departingemployee-get-all -!code42-highriskemployee-add username="partner.demisto@example.com" note="Risky activity" -!code42-highriskemployee-remove username="partner.demisto@example.com" note="Risky activity" +!code42-highriskemployee-add username="partner.demisto@example.com" note="Approved activity" +!code42-highriskemployee-remove username="partner.demisto@example.com" note="Approved activity" !code42-highriskemployee-get-all !code42-highriskemployee-add-risk-tags username="partner.demisto@example.com" note="PERFORMANCE_CONCERN" !code42-highriskemployee-remove-risk-tags username="partner.demisto@example.com" risktags="PERFORMANCE_CONCERNS" diff --git a/Packs/Code42/ReleaseNotes/2_0_0.md b/Packs/Code42/ReleaseNotes/2_0_0.md index 8b2b76d3b58b..b8100e9092bd 100644 --- a/Packs/Code42/ReleaseNotes/2_0_0.md +++ b/Packs/Code42/ReleaseNotes/2_0_0.md @@ -1,6 +1,7 @@ #### Integrations ##### Code42 +## [Unreleased] - Internal code improvements. - Added new commands: - **code42-departingemployee-get-all** @@ -14,6 +15,8 @@ - **code42-user-block** - **code42-user-unblock** - **code42-user-create** + - **code42-legalhold-add-user** + - **code42-legalhold-remove-user** - **code42-file-download** - Improve error messages for all Commands to include exception detail. - **Code42 Exfiltration Playbook** now downloads the file in replace of a manual step for retrieving file contents.