diff --git a/Packs/Code42/Integrations/Code42/CHANGELOG.md b/Packs/Code42/Integrations/Code42/CHANGELOG.md index d30648e0701..64cdf65c7f1 100644 --- a/Packs/Code42/Integrations/Code42/CHANGELOG.md +++ b/Packs/Code42/Integrations/Code42/CHANGELOG.md @@ -1,18 +1,22 @@ ## [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. - - **code42-user-deactivate** that deactivates a user in Code42. - - **code42-user-reactivate** that reactivates a user in Code42. - - **code42-user-block** that blocks a user in Code42. - - **code42-user-unblock** that unblocks a user in Code42. - - **code42-user-create** that creates a user in Code42. -Improve error messages for all Commands to include exception detail. +- Internal code improvements. +- Added new commands: + - **code42-departingemployee-get-all** + - **code42-highriskemployee-add** + - **code42-highriskemployee-remove** + - **code42-highriskemployee-get-all** + - **code42-highriskemployee-add-risk-tags** + - **code42-highriskemployee-remove-risk-tags** + - **code42-user-deactivate** + - **code42-user-reactivate** + - **code42-user-block** + - **code42-user-unblock** + - **code42-user-create** + - **code42-file-download** +- Improve error messages for all Commands to include exception detail. +- Fixed bug in Fetch where errors occurred when `FileCategory` was set to include only one category. +- Fixed bug in Fetch to handle new Code42 exposure type **Outside trusted domains**. +- Improved Fetch to handle unsupported exposure types better. ## [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 23dfcbe25f1..7a38ccdd163 100644 --- a/Packs/Code42/Integrations/Code42/Code42.py +++ b/Packs/Code42/Integrations/Code42/Code42.py @@ -306,6 +306,15 @@ def search_file_events(self, payload): res = self._get_sdk().securitydata.search_file_events(payload) return res["fileEvents"] + def download_file(self, hash_arg): + security_module = self._get_sdk().securitydata + if _hash_is_md5(hash_arg): + return security_module.stream_file_by_md5(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.") + def _get_user_id(self, username): user_id = self.get_user(username).get("userUid") if user_id: @@ -419,12 +428,18 @@ def build_query_payload(args): return query +def _hash_is_sha256(hash_arg): + return hash_arg and len(hash_arg) == 64 + + +def _hash_is_md5(hash_arg): + return hash_arg and len(hash_arg) == 32 + + def _create_hash_filter(hash_arg): - if not hash_arg: - return None - elif len(hash_arg) == 32: + if _hash_is_md5(hash_arg): return MD5.eq(hash_arg) - elif len(hash_arg) == 64: + elif _hash_is_sha256(hash_arg): return SHA256.eq(hash_arg) @@ -455,7 +470,7 @@ class ObservationToSecurityQueryMapper(object): exposure_type_map = { "PublicSearchableShare": ExposureType.IS_PUBLIC, "PublicLinkShare": ExposureType.SHARED_VIA_LINK, - "SharedOutsideTrustedDomain": "OutsideTrustedDomains", + "SharedOutsideTrustedDomain": ExposureType.OUTSIDE_TRUSTED_DOMAINS, } def __init__(self, observation, actor): @@ -935,6 +950,13 @@ def user_reactivate_command(client, args): ) +def download_file_command(client, args): + file_hash = args.get("hash") + response = client.download_file(file_hash) + file_chunks = [c for c in response.iter_content(chunk_size=128) if c] + return fileResult(file_hash, data=b"".join(file_chunks)) + + """Fetching""" @@ -974,7 +996,7 @@ def __init__( self._first_fetch_time = first_fetch_time self._event_severity_filter = event_severity_filter self._fetch_limit = fetch_limit - self._include_files = (include_files,) + self._include_files = include_files self._integration_context = integration_context @logger @@ -996,7 +1018,7 @@ def _fetch_remaining_incidents_from_last_run(self): if remaining_incidents: return ( self._last_run, - remaining_incidents[: self._fetch_limit], + remaining_incidents[:self._fetch_limit], remaining_incidents[self._fetch_limit:], ) @@ -1021,7 +1043,8 @@ def _fetch_alerts(self, start_query_time): def _create_incident_from_alert(self, alert): details = self._client.get_alert_details(alert["id"]) incident = _create_incident_from_alert_details(details) - details = self._relate_files_to_alert(details) + if self._include_files: + details = self._relate_files_to_alert(details) incident["rawJSON"] = json.dumps(details) return incident @@ -1095,6 +1118,7 @@ def get_command_map(): "code42-user-unblock": user_unblock_command, "code42-user-deactivate": user_deactivate_command, "code42_user-reactivate": user_reactivate_command, + "code42-download-file": download_file_command, } @@ -1123,10 +1147,10 @@ def handle_fetch_command(client): demisto.setIntegrationContext(integration_context) -def try_run_command(command): +def run_command(command): try: results = command() - if not isinstance(results, tuple) and not isinstance(results, list): + if not isinstance(results, (tuple, list)): results = [results] for result in results: return_results(result) @@ -1134,28 +1158,32 @@ def try_run_command(command): return_error(create_command_error_message(demisto.command(), e)) -def run_code42_integration(): +def create_client(): username = demisto.params().get("credentials").get("identifier") password = demisto.params().get("credentials").get("password") base_url = demisto.params().get("console_url") verify_certificate = not demisto.params().get("insecure", False) proxy = demisto.params().get("proxy", False) - LOG("Command being called is {0}.".format(demisto.command())) - client = Code42Client( + return Code42Client( base_url=base_url, sdk=None, auth=(username, password), verify=verify_certificate, proxy=proxy, ) + + +def run_code42_integration(): + client = create_client() commands = get_command_map() - command = demisto.command() - if command == "test-module": + command_key = demisto.command() + LOG("Command being called is {0}.".format(command_key)) + if command_key == "test-module": handle_test_command(client) - elif command == "fetch-incidents": + elif command_key == "fetch-incidents": handle_fetch_command(client) - elif command in commands: - try_run_command(lambda: commands[command](client, demisto.args())) + elif command_key in commands: + run_command(lambda: commands[command_key](client, demisto.args())) def main(): diff --git a/Packs/Code42/Integrations/Code42/Code42.yml b/Packs/Code42/Integrations/Code42/Code42.yml index 1315f1bff0f..b16f1aa9334 100644 --- a/Packs/Code42/Integrations/Code42/Code42.yml +++ b/Packs/Code42/Integrations/Code42/Code42.yml @@ -79,7 +79,7 @@ script: - auto: PREDEFINED default: false description: Exposure types to search for. Can be "RemovableMedia", "ApplicationRead", - "CloudStorage", "IsPublic", "SharedViaLink", or "SharedViaDomain". + "CloudStorage", "IsPublic", "SharedViaLink", "SharedViaDomain", or "OutsideTrustedDomains". isArray: true name: exposure predefined: @@ -89,6 +89,7 @@ script: - IsPublic - SharedViaLink - SharedViaDomain + - OutsideTrustedDomains required: false secret: false - default: false @@ -331,7 +332,7 @@ script: description: The username to remove from the Departing Employee List. isArray: false name: username - required: true + required: false secret: false deprecated: false description: Removes a user from the Departing Employee List. @@ -596,7 +597,18 @@ script: - contextPath: Code42.User.UserID description: The ID of a Code42 User. type: String - dockerimage: demisto/py42:1.0.0.9323 + - arguments: + - default: false + description: Either the SHA256 or MD5 hash of the file. + isArray: false + name: hash + required: true + secret: false + deprecated: false + description: Downloads a file from Code42 servers. + execution: false + name: code42-download-file + dockerimage: demisto/py42:1.0.0.9653 feed: false isfetch: true longRunning: false diff --git a/Packs/Code42/Integrations/Code42/Code42_test.py b/Packs/Code42/Integrations/Code42/Code42_test.py index a3698087000..43ea9043687 100644 --- a/Packs/Code42/Integrations/Code42/Code42_test.py +++ b/Packs/Code42/Integrations/Code42/Code42_test.py @@ -26,6 +26,7 @@ user_unblock_command, user_deactivate_command, user_reactivate_command, + download_file_command, fetch_incidents, ) import time @@ -1379,16 +1380,7 @@ def test_departingemployee_get_all_command_when_no_employees( no_employees_response ) client = create_client(code42_departing_employee_mock) - cmd_res = departingemployee_get_all_command( - client, - { - "risktags": [ - "PERFORMANCE_CONCERNS", - "SUSPICIOUS_SYSTEM_ACTIVITY", - "POOR_SECURITY_PRACTICES", - ] - }, - ) + cmd_res = departingemployee_get_all_command(client,{}) assert cmd_res.outputs_prefix == "Code42.DepartingEmployee" assert cmd_res.outputs_key_field == "UserID" assert cmd_res.raw_response == {} @@ -1635,6 +1627,25 @@ def test_security_data_search_command(code42_file_events_mock): assert output_item == mapped_event +def test_download_file_command_when_given_md5(code42_sdk_mock, mocker): + fr = mocker.patch("Code42.fileResult") + client = create_client(code42_sdk_mock) + _ = download_file_command(client, {"hash": "b6312dbe4aa4212da94523ccb28c5c16"}) + code42_sdk_mock.securitydata.stream_file_by_md5.assert_called_once_with( + "b6312dbe4aa4212da94523ccb28c5c16" + ) + assert fr.call_count == 1 + + +def test_download_file_command_when_given_sha256(code42_sdk_mock, mocker): + fr = mocker.patch("Code42.fileResult") + _hash = "41966f10cc59ab466444add08974fde4cd37f88d79321d42da8e4c79b51c2149" + client = create_client(code42_sdk_mock) + _ = download_file_command(client, {"hash": _hash}) + code42_sdk_mock.securitydata.stream_file_by_sha256.assert_called_once_with(_hash) + assert fr.call_count == 1 + + def test_fetch_when_no_significant_file_categories_ignores_filter( code42_fetch_incidents_mock, mocker ): @@ -1683,8 +1694,41 @@ def test_fetch_incidents_handles_multi_severity(code42_fetch_incidents_mock): include_files=True, integration_context=None, ) - 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]) + call_args = str(code42_fetch_incidents_mock.alerts.search.call_args[0][0]) + assert "HIGH" in call_args + assert "LOW" in call_args + + +def test_fetch_when_include_files_includes_files(code42_fetch_incidents_mock): + client = create_client(code42_fetch_incidents_mock) + _, incidents, _ = fetch_incidents( + client=client, + last_run={"last_fetch": None}, + first_fetch_time=MOCK_FETCH_TIME, + event_severity_filter=["High", "Low"], + fetch_limit=10, + include_files=True, + integration_context=None, + ) + for i in incidents: + _json = json.loads(i["rawJSON"]) + assert len(_json["fileevents"]) + + +def test_fetch_when_not_include_files_excludes_files(code42_fetch_incidents_mock): + client = create_client(code42_fetch_incidents_mock) + _, incidents, _ = fetch_incidents( + client=client, + last_run={"last_fetch": None}, + first_fetch_time=MOCK_FETCH_TIME, + event_severity_filter=["High", "Low"], + fetch_limit=10, + include_files=False, + integration_context=None, + ) + for i in incidents: + _json = json.loads(i["rawJSON"]) + assert not _json.get("fileevents") def test_fetch_incidents_first_run(code42_fetch_incidents_mock): diff --git a/Packs/Code42/Integrations/Code42/README.md b/Packs/Code42/Integrations/Code42/README.md index 5ad1a1e2e4c..02c75bdbd7d 100644 --- a/Packs/Code42/Integrations/Code42/README.md +++ b/Packs/Code42/Integrations/Code42/README.md @@ -717,3 +717,27 @@ Reactivates the user with the given username. | UserID | | ------ | | 123456790 | + + +### code42-download-file +*** +Downloads a file from Code42 servers. + +#### Base Command + +`code42-download-file` +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| hash | Either the SHA256 or MD5 hash of the file. | Required | + + +#### Command Example +```!code42-download-file hash="bf6b326107d4d85eb485eed84b28133a"``` + +#### Human Readable Output +### Code42 User Deactivated +| Type | Size | Info | MD5 | SHA1 | SHA256 | SHA512 | SSDeep | +| ------ | ---- | ---- | --- | ---- | ------ | ------ | ------ | +| application/vnd.ms-excel | 41,472 bytes | Composite Document File V2 Document, Little Endian, Os: MacOS, Version 14.10, Code page: 10000, Last Saved By: John Doe, Name of Creating Application: Microsoft Macintosh Excel, Create Time/Date: Fri Feb 21 17:35:19 2020, Last Saved Time/Date: Mon Apr 13 11:54:08 2020, Security: 0 | 2e45562437ec4f41387f2e14c3850dd6 | 59e552e637bfe5254b163bb4e426a2322d10f50d | d3f8566d04df5dc34bf2607ac803a585ac81e06f28afe81f35cc2e5fe63d2ab5 | 776bd9626761cd567a4b498bafe4f5f896c3f4bc9f3c60513ccacd14251a2568fa3ba44060000affa8b57fb768c417cf271500086e4e49272f26b26a90627abb | 768:pudkQzl3ZpWh+QO3uMdS9dSttRJwyE/KtxA1almvy6mhk+GlESOwWoqSY7bTKCUv:siQzl3ZpWh+QO3uMdS9dSttRJwyE/KtF | \ No newline at end of file diff --git a/Packs/Code42/Integrations/Code42/integration-Code42.yml b/Packs/Code42/Integrations/Code42/integration-Code42.yml deleted file mode 100644 index 80c39d1cad6..00000000000 --- a/Packs/Code42/Integrations/Code42/integration-Code42.yml +++ /dev/null @@ -1,1822 +0,0 @@ -category: Endpoint -commonfields: - id: Code42 - version: -1 -configuration: -- defaultvalue: console.us.code42.com - display: Code42 Console URL for the pod your Code42 instance is running in - name: console_url - required: true - type: 0 -- display: Username - name: credentials - required: true - type: 9 -- display: Fetch incidents - name: isFetch - required: false - type: 8 -- display: Incident type - name: incidentType - required: false - type: 13 -- display: Alert severities to fetch when fetching incidents - name: alert_severity - options: - - High - - Medium - - Low - required: false - type: 16 -- defaultvalue: 24 hours - display: First fetch time range (