diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 6f15b22..131c639 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,7 +1,7 @@ name: Linting on: [push, pull_request] jobs: - lint: + lint: # Run per push for internal contributers. This isn't possible for forked pull requests, # so we'll need to run on PR events for external contributers. # String comparison below is case insensitive. diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 23d31c5..712cc1b 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,5 +1,5 @@ name: Semgrep -on: +on: pull_request_target: branches: - next @@ -21,8 +21,8 @@ jobs: echo "REPOSITORY=${{ github.event.pull_request.head.repo.full_name }}" >> $GITHUB_ENV echo "REF=${{ github.event.pull_request.head.ref }}" >> $GITHUB_ENV - uses: 'phantomcyber/dev-cicd-tools/github-actions/semgrep@main' - with: + with: SEMGREP_DEPLOYMENT_ID: ${{ secrets.SEMGREP_DEPLOYMENT_ID }} SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - REPOSITORY: ${{ github.repository }} + REPOSITORY: ${{ github.repository }} REF: ${{ github.ref }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c97c8e5..13ffd89 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v1.9 + rev: v1.13 hooks: - id: org-hook - id: package-app-dependencies - repo: https://github.com/Yelp/detect-secrets - rev: v1.1.0 + rev: v1.3.0 hooks: - id: detect-secrets args: ['--no-verify', '--exclude-files', '^cybereason.json$'] diff --git a/LICENSE b/LICENSE index f003b93..56c8192 100644 --- a/LICENSE +++ b/LICENSE @@ -198,4 +198,4 @@ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/cybereason.json b/cybereason.json index 5a55b7b..24b8509 100644 --- a/cybereason.json +++ b/cybereason.json @@ -11,12 +11,13 @@ "product_version_regex": ".*", "publisher": "Cybereason", "license": "Copyright (c) Cybereason, 2018-2021", - "app_version": "2.1.4", + "app_version": "2.2.0", "utctime_updated": "2022-01-07T20:19:08.000000Z", "package_name": "phantom_cybereason", "main_module": "cybereason_connector.py", - "min_phantom_version": "5.0.0", + "min_phantom_version": "5.3.0", "app_wizard_version": "1.0.0", + "fips_compliant": false, "configuration": { "base_url": { "description": "The URL of the Cybereason server to connect to. This should be of the form 'https://:'", @@ -1817,7 +1818,7 @@ { "data_path": "action_result.data.*.dns_query", "data_type": "string", - "column_name": "DNS Query", + "column_name": "Dns Query", "column_order": 10 }, { @@ -2017,32 +2018,32 @@ "wheel": [ { "module": "beautifulsoup4", - "input_file": "wheels/beautifulsoup4-4.9.1-py3-none-any.whl" + "input_file": "wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl" }, { "module": "certifi", - "input_file": "wheels/certifi-2021.10.8-py2.py3-none-any.whl" + "input_file": "wheels/py3/certifi-2022.6.15-py3-none-any.whl" }, { "module": "chardet", - "input_file": "wheels/chardet-3.0.4-py2.py3-none-any.whl" + "input_file": "wheels/shared/chardet-3.0.4-py2.py3-none-any.whl" }, { "module": "idna", - "input_file": "wheels/idna-2.10-py2.py3-none-any.whl" + "input_file": "wheels/shared/idna-2.10-py2.py3-none-any.whl" }, { "module": "requests", - "input_file": "wheels/requests-2.25.0-py2.py3-none-any.whl" + "input_file": "wheels/shared/requests-2.25.0-py2.py3-none-any.whl" }, { "module": "soupsieve", - "input_file": "wheels/soupsieve-2.3-py3-none-any.whl" + "input_file": "wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl" }, { "module": "urllib3", - "input_file": "wheels/urllib3-1.26.7-py2.py3-none-any.whl" + "input_file": "wheels/shared/urllib3-1.26.11-py2.py3-none-any.whl" } ] } -} \ No newline at end of file +} diff --git a/cybereason_connector.py b/cybereason_connector.py index 9ffdbed..1097136 100644 --- a/cybereason_connector.py +++ b/cybereason_connector.py @@ -129,6 +129,7 @@ def _validate_integer(self, action_result, parameter, key): return phantom.APP_SUCCESS, parameter def _handle_test_connectivity(self, param): + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) # Add an action result object to self (BaseConnector) to represent the action for this param action_result = self.add_action_result(ActionResult(dict(param))) @@ -143,6 +144,7 @@ def _handle_test_connectivity(self, param): 'Successfully connected to the Cybereason console and verified session cookie' ) else: + self.debug_print('Failure to verify session cookie.') return action_result.set_status( phantom.APP_ERROR, 'Connectivity failed. Unable to get session cookie from Cybereason console' @@ -235,6 +237,7 @@ def _handle_delete_registry_key(self, param): }) except Exception as e: err = self._get_error_message_from_exception(e) + self.debug_print("Error occurred: {}".format(err)) return action_result.set_status(phantom.APP_ERROR, "Error occurred. {}".format(err)) return action_result.set_status(phantom.APP_SUCCESS) @@ -350,8 +353,8 @@ def _handle_update_malop_status(self, param): malop_id = self._get_string_param(param['malop_id']) - phantom_status = param['status'] - cybereason_status = PHANTOM_TO_CYBEREASON_STATUS.get(phantom_status) + soar_status = param['status'] + cybereason_status = SOAR_TO_CYBEREASON_STATUS.get(soar_status) if not cybereason_status: self.save_progress("Invalid status selected") return action_result.set_status( @@ -945,6 +948,54 @@ def _get_machine_name_by_machine_ip(self, machine_ip, action_result): return RetVal(action_result.set_status(phantom.APP_SUCCESS), machine_names) + def on_poll(self, param): + self.save_progress("Entered the on_poll function") + self.save_progress("processing") + poller = CybereasonPoller() + return poller.do_poll(self, param) + + def _handle_query_processes(self, param): + self.save_progress("Entered the _handle_query_processes function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_processes(self, param) + + def _handle_query_machine(self, param): + self.save_progress("Entered the _handle_query_machine function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_machine(self, param) + + def _handle_query_machine_ip(self, param): + self.save_progress("Entered the _handle_query_machine_ip function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_machine_ip(self, param) + + def _handle_query_users(self, param): + self.save_progress("Entered the _handle_query_users function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_users(self, param) + + def _handle_query_files(self, param): + self.save_progress("Entered the _handle_query_files function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_files(self, param) + + def _handle_query_domain(self, param): + self.save_progress("Entered the _handle_query_domain function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_domain(self, param) + + def _handle_query_connections(self, param): + self.save_progress("Entered the _handle_query_connections function") + self.save_progress("processing") + query_action = CybereasonQueryActions() + return query_action._handle_query_connections(self, param) + def handle_action(self, param): ret_val = phantom.APP_SUCCESS @@ -963,8 +1014,7 @@ def handle_action(self, param): ret_val = self._handle_get_sensor_status(param) elif action_id == 'on_poll': - poller = CybereasonPoller() - ret_val = poller.do_poll(self, param) + ret_val = self.on_poll(param) elif action_id == 'add_malop_comment': ret_val = self._handle_add_malop_comment(param) @@ -1000,32 +1050,25 @@ def handle_action(self, param): ret_val = self._handle_restart_sensor(param) elif action_id == 'query_processes': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_processes(self, param) + ret_val = self._handle_query_processes(param) elif action_id == 'query_machine': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_machine(self, param) + ret_val = self._handle_query_machine(param) elif action_id == 'query_machine_ip': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_machine_ip(self, param) + ret_val = self._handle_query_machine_ip(param) elif action_id == 'query_users': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_users(self, param) + ret_val = self._handle_query_users(param) elif action_id == 'query_files': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_files(self, param) + ret_val = self._handle_query_files(param) elif action_id == 'query_domain': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_domain(self, param) + ret_val = self._handle_query_domain(param) elif action_id == 'query_connections': - query_action = CybereasonQueryActions() - ret_val = query_action._handle_query_connections(self, param) + ret_val = self._handle_query_connections(param) return ret_val @@ -1055,6 +1098,7 @@ def finalize(self): def main(): import argparse + import sys import pudb @@ -1083,7 +1127,7 @@ def main(): login_url = CybereasonConnector._get_phantom_base_url() + '/login' print("Accessing the Login page") - r = requests.get(login_url, verify=False) + r = requests.get(login_url, timeout=DEFAULT_REQUEST_TIMEOUT) csrftoken = r.cookies['csrftoken'] data = dict() @@ -1096,11 +1140,11 @@ def main(): headers['Referer'] = login_url print("Logging into Platform to get the session id") - r2 = requests.post(login_url, verify=False, data=data, headers=headers) + r2 = requests.post(login_url, data=data, headers=headers, timeout=DEFAULT_REQUEST_TIMEOUT) session_id = r2.cookies['sessionid'] except Exception as e: print("Unable to get session id from the platform. Error: " + str(e)) - exit(1) + sys.exit(1) with open(args.input_test_json) as f: in_json = f.read() @@ -1117,7 +1161,7 @@ def main(): ret_val = connector._handle_action(json.dumps(in_json), None) print(json.dumps(json.loads(ret_val), indent=4)) - exit(0) + sys.exit(0) if __name__ == '__main__': diff --git a/cybereason_consts.py b/cybereason_consts.py index 1d9b1fa..fcb827c 100644 --- a/cybereason_consts.py +++ b/cybereason_consts.py @@ -11,7 +11,7 @@ # either express or implied. See the License for the specific language governing permissions # and limitations under the License. -PHANTOM_TO_CYBEREASON_STATUS = { +SOAR_TO_CYBEREASON_STATUS = { 'Unread': "UNREAD", 'To Review': "TODO", 'Not Relevant': "FP", @@ -31,3 +31,5 @@ MALOP_HISTORICAL_DAYS_KEY = "malop_historical_days asset configuration parameter" MALWARE_HISTORICAL_DAYS_KEY = "malware_historical_days asset configuration parameter" + +DEFAULT_REQUEST_TIMEOUT = 60 # in seconds diff --git a/cybereason_poller.py b/cybereason_poller.py old mode 100644 new mode 100755 index 77752ce..aee243a --- a/cybereason_poller.py +++ b/cybereason_poller.py @@ -368,14 +368,15 @@ def _ingest_malop(self, connector, config, malop_id, malop_data): return phantom.APP_SUCCESS if success else phantom.APP_ERROR - def _does_container_exist_for_malop_malware(self, connector, malop_id): + def _does_container_exist_for_malop_malware(self, connector, source_data_identifier): url = '{0}rest/container?_filter_source_data_identifier="{1}"&_filter_asset={2}'.format( connector.get_phantom_base_url(), - malop_id, connector.get_asset_id() + source_data_identifier, connector.get_asset_id() ) + existing_container_id = False try: - r = requests.get(url, verify=False) + r = requests.get(url, verify=False, timeout=DEFAULT_REQUEST_TIMEOUT) # nosemgrep resp_json = r.json() except Exception as e: err = connector._get_error_message_from_exception(e) @@ -402,7 +403,7 @@ def _update_container_for_malop_malware(self, connector, config, existing_contai update_json = container.copy() del update_json["artifacts"] url = '{0}rest/container/{1}'.format(connector.get_phantom_base_url(), existing_container_id) - r = requests.post(url, json=update_json, verify=False) + r = requests.post(url, json=update_json, verify=False, timeout=DEFAULT_REQUEST_TIMEOUT) # nosemgrep resp_json = r.json() for artifact in container["artifacts"]: @@ -435,7 +436,7 @@ def _get_artifact(self, connector, config, source_data_identifier, container_id) url = '{0}rest/artifact?_filter_source_data_identifier="{1}"&_filter_container_id={2}&sort=id&order=desc'.format( connector.get_phantom_base_url(), source_data_identifier, container_id) try: - r = requests.get(url, verify=False) + r = requests.get(url, verify=False, timeout=DEFAULT_REQUEST_TIMEOUT) # nosemgrep resp_json = r.json() except Exception as e: err = connector._get_error_message_from_exception(e) @@ -762,7 +763,7 @@ def _get_malware_with_offset(self, connector, malware_millisec_since_last_poll, def _ingest_malware(self, connector, config, malware): success = phantom.APP_ERROR container = self._get_container_dict_for_malware(connector, config, malware) - existing_container_id = self._does_container_exist_for_malop_malware(connector, malware["guid"]) + existing_container_id = self._does_container_exist_for_malop_malware(connector, "{}_{}".format(malware["guid"], malware["timestamp"])) if not existing_container_id: # Container does not exist. Go ahead and save it connector.debug_print("Saving container for Malware with id {}".format(malware["guid"])) @@ -782,7 +783,7 @@ def _get_container_dict_for_malware(self, connector, config, malware): ) container_json["data"] = malware container_json["description"] = malware["name"] - container_json["source_data_identifier"] = malware["guid"] + container_json["source_data_identifier"] = "{}_{}".format(malware["guid"], malware["timestamp"]) container_json["label"] = config.get("ingest", {}).get("container_label") status_map = self._get_status_map_malware() container_json["status"] = status_map.get(malware["status"], "New") @@ -889,7 +890,7 @@ def _get_cef_type_map(self): } # Converts timestamps from Cybereason API - # (e.g. string "1585270873770") to Phantom/ISO 8601 format (e.g. 2020-03-27T01:01:13.770Z) + # (e.g. string "1585270873770") to SOAR/ISO 8601 format (e.g. 2020-03-27T01:01:13.770Z) def _phtimestamp_from_crtimestamp(self, cybereason_timestamp): timestamp = datetime.datetime.fromtimestamp(int(cybereason_timestamp) / 1000.0) # Timestamp is in epoch-milliseconds return timestamp.isoformat()[:-3] + "Z" # Remove the microsecond accuracy, add "Z" for UTC timezone diff --git a/cybereason_query_actions.py b/cybereason_query_actions.py index b7906ea..f752442 100644 --- a/cybereason_query_actions.py +++ b/cybereason_query_actions.py @@ -14,7 +14,6 @@ import traceback -# Phantom App imports import phantom.app as phantom from phantom.action_result import ActionResult diff --git a/cybereason_session.py b/cybereason_session.py index 3bc8dfd..19596e8 100644 --- a/cybereason_session.py +++ b/cybereason_session.py @@ -14,6 +14,8 @@ import requests +from cybereason_consts import DEFAULT_REQUEST_TIMEOUT + class CybereasonSession: @@ -26,7 +28,7 @@ def __init__(self, connector): } try: url = "{0}/login.html".format(connector._base_url) - res = self.session.post(url, data=post_body, verify=connector._verify_server_cert) + res = self.session.post(url, data=post_body, verify=connector._verify_server_cert, timeout=DEFAULT_REQUEST_TIMEOUT) if self.session.cookies.get_dict().get("JSESSIONID") is None: connector.save_progress("Error when logging in to the the Cybereason console: No session cookie returned") connector.save_progress("Status code: {}".format(res.status_code)) diff --git a/logo_cybereason.svg b/logo_cybereason.svg index 02faca4..c129528 100644 --- a/logo_cybereason.svg +++ b/logo_cybereason.svg @@ -1 +1 @@ -Asset 1 \ No newline at end of file +Asset 1 diff --git a/logo_cybereason_dark.svg b/logo_cybereason_dark.svg index 157dab3..54d98ac 100644 --- a/logo_cybereason_dark.svg +++ b/logo_cybereason_dark.svg @@ -1 +1 @@ -Asset 2 \ No newline at end of file +Asset 2 diff --git a/readme.html b/readme.html index 55728ad..7a1921e 100644 --- a/readme.html +++ b/readme.html @@ -3,7 +3,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index 82a7eec..65d882e 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1 +1,2 @@ -**Unreleased** \ No newline at end of file +**Unreleased** +* Minor fixes and enhancements \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c4644ad --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[flake8] +max-line-length = 145 +max-complexity = 28 +extend-ignore = F403,E128,E126,E111,E121,E127,E731,E201,E202,F405,E722,D,W292 + +[isort] +line_length = 145 diff --git a/wheels/certifi-2021.10.8-py2.py3-none-any.whl b/wheels/certifi-2021.10.8-py2.py3-none-any.whl deleted file mode 100644 index fbcb86b..0000000 Binary files a/wheels/certifi-2021.10.8-py2.py3-none-any.whl and /dev/null differ diff --git a/wheels/beautifulsoup4-4.9.1-py3-none-any.whl b/wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl similarity index 100% rename from wheels/beautifulsoup4-4.9.1-py3-none-any.whl rename to wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl diff --git a/wheels/py3/certifi-2022.6.15-py3-none-any.whl b/wheels/py3/certifi-2022.6.15-py3-none-any.whl new file mode 100644 index 0000000..6e70631 Binary files /dev/null and b/wheels/py3/certifi-2022.6.15-py3-none-any.whl differ diff --git a/wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl b/wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl new file mode 100644 index 0000000..b363a9b Binary files /dev/null and b/wheels/py3/soupsieve-2.3.2.post1-py3-none-any.whl differ diff --git a/wheels/chardet-3.0.4-py2.py3-none-any.whl b/wheels/shared/chardet-3.0.4-py2.py3-none-any.whl similarity index 100% rename from wheels/chardet-3.0.4-py2.py3-none-any.whl rename to wheels/shared/chardet-3.0.4-py2.py3-none-any.whl diff --git a/wheels/idna-2.10-py2.py3-none-any.whl b/wheels/shared/idna-2.10-py2.py3-none-any.whl similarity index 100% rename from wheels/idna-2.10-py2.py3-none-any.whl rename to wheels/shared/idna-2.10-py2.py3-none-any.whl diff --git a/wheels/requests-2.25.0-py2.py3-none-any.whl b/wheels/shared/requests-2.25.0-py2.py3-none-any.whl similarity index 100% rename from wheels/requests-2.25.0-py2.py3-none-any.whl rename to wheels/shared/requests-2.25.0-py2.py3-none-any.whl diff --git a/wheels/shared/urllib3-1.26.11-py2.py3-none-any.whl b/wheels/shared/urllib3-1.26.11-py2.py3-none-any.whl new file mode 100644 index 0000000..7c66bd9 Binary files /dev/null and b/wheels/shared/urllib3-1.26.11-py2.py3-none-any.whl differ diff --git a/wheels/soupsieve-2.3-py3-none-any.whl b/wheels/soupsieve-2.3-py3-none-any.whl deleted file mode 100644 index 939d6ce..0000000 Binary files a/wheels/soupsieve-2.3-py3-none-any.whl and /dev/null differ diff --git a/wheels/urllib3-1.26.7-py2.py3-none-any.whl b/wheels/urllib3-1.26.7-py2.py3-none-any.whl deleted file mode 100644 index 62189e6..0000000 Binary files a/wheels/urllib3-1.26.7-py2.py3-none-any.whl and /dev/null differ