diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c420ced..784ff17 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,12 @@ repos: - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v1.16 + rev: v1.24 hooks: - id: org-hook - id: package-app-dependencies + - id: readme-check - repo: https://github.com/Yelp/detect-secrets - rev: v1.4.0 + rev: v1.5.0 hooks: - id: detect-secrets - args: ['--no-verify', '--exclude-files', '^awscloudtrail.json$'] + args: ['--no-verify', '--exclude-files', '^(awscloudtrail.json|README.md)$'] diff --git a/LICENSE b/LICENSE index 94b040f..271de55 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2019-2023 Splunk Inc. + Copyright (c) 2019-2024 Splunk Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 9d9c1cd..12ce3fa 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ # AWS CloudTrail Publisher: Splunk -Connector Version: 2.2.7 +Connector Version: 2.2.8 Product Vendor: AWS Product Name: CloudTrail Product Version Supported (regex): ".\*" -Minimum Product Version: 4.9.39220 +Minimum Product Version: 6.3.0 This app integrates with AWS CloudTrail to perform various investigative actions @@ -38,8 +38,8 @@ actions within a playbook. For more information, please see the [AWS Identity an documentation](https://docs.aws.amazon.com/iam/index.html) . -### Configuration Variables -The below configuration variables are required for this Connector to operate. These variables are specified when configuring a CloudTrail asset in SOAR. +### Configuration variables +This table lists the configuration variables required to operate AWS CloudTrail. These variables are specified when configuring a CloudTrail asset in Splunk SOAR. VARIABLE | REQUIRED | TYPE | DESCRIPTION -------- | -------- | ---- | ----------- diff --git a/__init__.py b/__init__.py index 44bae34..a21cc51 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ # File: __init__.py # -# Copyright (c) 2019-2023 Splunk Inc. +# Copyright (c) 2019-2024 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/awscloudtrail.json b/awscloudtrail.json index 4c2f7df..053b64a 100644 --- a/awscloudtrail.json +++ b/awscloudtrail.json @@ -10,12 +10,12 @@ "product_version_regex": ".*", "python_version": "3", "publisher": "Splunk", - "license": "Copyright (c) 2019-2023 Splunk Inc.", - "app_version": "2.2.7", - "utctime_updated": "2022-01-07T20:23:12.000000Z", + "license": "Copyright (c) 2019-2024 Splunk Inc.", + "app_version": "2.2.8", + "utctime_updated": "2024-12-17T14:45:31.000000Z", "package_name": "phantom_awscloudtrail", "main_module": "awscloudtrail_connector.py", - "min_phantom_version": "4.9.39220", + "min_phantom_version": "6.3.0", "fips_compliant": true, "app_wizard_version": "1.0.0", "latest_tested_versions": [ @@ -91,21 +91,9 @@ "module": "jmespath", "input_file": "wheels/shared/jmespath-0.10.0-py2.py3-none-any.whl" }, - { - "module": "python_dateutil", - "input_file": "wheels/shared/python_dateutil-2.8.2-py2.py3-none-any.whl" - }, { "module": "s3transfer", "input_file": "wheels/shared/s3transfer-0.3.7-py2.py3-none-any.whl" - }, - { - "module": "six", - "input_file": "wheels/shared/six-1.15.0-py2.py3-none-any.whl" - }, - { - "module": "urllib3", - "input_file": "wheels/shared/urllib3-1.26.18-py2.py3-none-any.whl" } ] }, @@ -1364,4 +1352,4 @@ "versions": "EQ(*)" } ] -} +} \ No newline at end of file diff --git a/awscloudtrail_connector.py b/awscloudtrail_connector.py index 6e13f2a..ccb24b8 100644 --- a/awscloudtrail_connector.py +++ b/awscloudtrail_connector.py @@ -1,6 +1,6 @@ # File: awscloudtrail_connector.py # -# Copyright (c) 2019-2023 Splunk Inc. +# Copyright (c) 2019-2024 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -60,12 +60,12 @@ def _create_client(self, action_result, param=None): # Try getting and using temporary assume role credentials from parameters temp_credentials = dict() - if param and 'credentials' in param: + if param and "credentials" in param: try: - temp_credentials = ast.literal_eval(param['credentials']) - self._access_key = temp_credentials.get('AccessKeyId', '') - self._secret_key = temp_credentials.get('SecretAccessKey', '') - self._session_token = temp_credentials.get('SessionToken', '') + temp_credentials = ast.literal_eval(param["credentials"]) + self._access_key = temp_credentials.get("AccessKeyId", "") + self._secret_key = temp_credentials.get("SecretAccessKey", "") + self._session_token = temp_credentials.get("SessionToken", "") self.save_progress("Using temporary assume role ceredentials for action") except Exception as e: @@ -75,18 +75,16 @@ def _create_client(self, action_result, param=None): if self._access_key and self._secret_key: self.debug_print("Creating boto3 client with API keys") self._client = client( - 'cloudtrail', + "cloudtrail", region_name=self._region, aws_access_key_id=self._access_key, aws_secret_access_key=self._secret_key, aws_session_token=self._session_token, - config=boto_config) + config=boto_config, + ) else: self.debug_print("Creating boto3 client without API keys") - self._client = client( - 'cloudtrail', - region_name=self._region, - config=boto_config) + self._client = client("cloudtrail", region_name=self._region, config=boto_config) except Exception as e: return action_result.set_status(phantom.APP_ERROR, "Could not create boto3 client: {0}".format(e)) @@ -112,9 +110,9 @@ def _make_boto_call(self, action_result, method, **kwargs): resp_json = boto_func(**kwargs) if can_paginate: for e in resp_json.get(set_name): - new_obj = self._extract_cloudtrail_event_object(e['CloudTrailEvent']) - e['ExtractedCloudTrailEvent'] = new_obj - del e['CloudTrailEvent'] + new_obj = self._extract_cloudtrail_event_object(e["CloudTrailEvent"]) + e["ExtractedCloudTrailEvent"] = new_obj + del e["CloudTrailEvent"] updated_list.append(e) else: resp_json = boto_func(**kwargs) @@ -122,19 +120,14 @@ def _make_boto_call(self, action_result, method, **kwargs): updated_list.append(i) except Exception as e: exception_message = e.args[0].strip() - return RetVal( - action_result.set_status(phantom.APP_ERROR, "boto3 call to CloudTrail failed.", exception_message), - None) + return RetVal(action_result.set_status(phantom.APP_ERROR, "boto3 call to CloudTrail failed.", exception_message), None) if can_paginate: next_token = None - if resp_json and resp_json.get('NextToken'): - next_token = resp_json.get('NextToken') + if resp_json and resp_json.get("NextToken"): + next_token = resp_json.get("NextToken") - res_dict = { - "response_list": self._sanitize_data(updated_list), - "next_token": next_token - } + res_dict = {"response_list": self._sanitize_data(updated_list), "next_token": next_token} return phantom.APP_SUCCESS, res_dict else: return phantom.APP_SUCCESS, self._sanitize_data(updated_list) @@ -145,17 +138,14 @@ def _process_json_response(self, r, action_result): try: resp_json = r.json() except Exception as e: - return RetVal( - action_result.set_status(phantom.APP_ERROR, "Unable to parse JSON response. Error: {0}".format(str(e))), - None) + return RetVal(action_result.set_status(phantom.APP_ERROR, "Unable to parse JSON response. Error: {0}".format(str(e))), None) # Please specify the status codes here if 200 <= r.status_code < 399: return RetVal(phantom.APP_SUCCESS, resp_json) # You should process the error returned in the json - message = "Error from server. Status Code: {0} Data from server: {1}".format( - r.status_code, r.text.replace('{', '{{').replace('}', '}}')) + message = "Error from server. Status Code: {0} Data from server: {1}".format(r.status_code, r.text.replace("{", "{{").replace("}", "}}")) return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) @@ -172,7 +162,7 @@ def _paginator(self, method_name, limit, action_result, **kwargs): while True: if next_token: - kwargs['NextToken'] = next_token + kwargs["NextToken"] = next_token ret_val, response = self._make_boto_call(action_result, method_name, **kwargs) @@ -180,10 +170,10 @@ def _paginator(self, method_name, limit, action_result, **kwargs): return None if response is not None: - if response.get('next_token'): + if response.get("next_token"): next_token = response.get("next_token") - if response.get('response_list'): - list_items.extend(response.get('response_list')) + if response.get("response_list"): + list_items.extend(response.get("response_list")) if limit and len(list_items) >= limit: return list_items[:limit] @@ -216,7 +206,7 @@ def _handle_describe_trails(self, param): self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) action_result = self.add_action_result(ActionResult(dict(param))) - include_shadow_trails = param.get('include_shadow_trails', False) + include_shadow_trails = param.get("include_shadow_trails", False) # workaround for default-value weirdness with boolean types if include_shadow_trails == "true": @@ -235,7 +225,7 @@ def _handle_describe_trails(self, param): # Add a dictionary that is made up of the most important values from data into the summary summary = action_result.update_summary({}) - summary['message'] = "Received {} trails".format(action_result.get_data_size()) + summary["message"] = "Received {} trails".format(action_result.get_data_size()) return action_result.set_status(phantom.APP_SUCCESS) @@ -244,11 +234,11 @@ def _handle_run_query(self, param): self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) action_result = self.add_action_result(ActionResult(dict(param))) - attribute_key = param.get('attribute_key', '') - attribute_value = param.get('attribute_value', '') - start_date = param.get('start_date', '') - end_date = param.get('end_date', '') - limit = param.get('max_results', 50) + attribute_key = param.get("attribute_key", "") + attribute_value = param.get("attribute_value", "") + start_date = param.get("start_date", "") + end_date = param.get("end_date", "") + limit = param.get("max_results", 50) if (limit and not str(limit).isdigit()) or limit == 0: return action_result.set_status(phantom.APP_ERROR, AWSCLOUDTRAIL_INVALID_LIMIT) @@ -257,24 +247,21 @@ def _handle_run_query(self, param): if not self._create_client(action_result, param): return action_result.get_status() - kwargs = {} # define the kwargs to send to _make_boto_call - if attribute_key != '' and attribute_value != '': - kwargs['LookupAttributes'] = [{ - "AttributeKey": attribute_key, - "AttributeValue": attribute_value - }] + kwargs = {} # define the kwargs to send to _make_boto_call + if attribute_key != "" and attribute_value != "": + kwargs["LookupAttributes"] = [{"AttributeKey": attribute_key, "AttributeValue": attribute_value}] # it is possible the user did not respect the format. Let try to do what we do and # inform them of the mistake if necessary. try: - if start_date != '': - kwargs['StartTime'] = datetime.strptime(start_date, "%Y-%m-%d") - if end_date != '': - kwargs['EndTime'] = datetime.strptime(end_date, "%Y-%m-%d") + if start_date != "": + kwargs["StartTime"] = datetime.strptime(start_date, "%Y-%m-%d") + if end_date != "": + kwargs["EndTime"] = datetime.strptime(end_date, "%Y-%m-%d") except ValueError: - return RetVal(action_result.set_status( - phantom.APP_ERROR, - "Invalid date format. Remember to left-pad in the format of YYYY-MM-DD"), None) + return RetVal( + action_result.set_status(phantom.APP_ERROR, "Invalid date format. Remember to left-pad in the format of YYYY-MM-DD"), None + ) lookup_events = self._paginator("lookup_events", limit, action_result, **kwargs) @@ -299,11 +286,11 @@ def handle_action(self, param): self.debug_print("action_id", self.get_action_identifier()) - if action_id == 'test_connectivity': + if action_id == "test_connectivity": ret_val = self._handle_test_connectivity(param) - elif action_id == 'describe_trails': + elif action_id == "describe_trails": ret_val = self._handle_describe_trails(param) - elif action_id == 'run_query': + elif action_id == "run_query": ret_val = self._handle_run_query(param) return ret_val @@ -342,7 +329,7 @@ def _sanitize_data(self, cur_obj): new_dict.update(page) return new_dict except Exception as e: - return {'error': e} + return {"error": e} return cur_obj @@ -356,17 +343,17 @@ def initialize(self): config = self.get_config() # Load required configs - self._region = AWS_CLOUDTRAIL_REGIONS.get(config['Region']) + self._region = AWS_CLOUDTRAIL_REGIONS.get(config["Region"]) # handle proxies self._proxy = {} - env_vars = config.get('_reserved_environment_variables', {}) - if 'HTTP_PROXY' in env_vars: - self._proxy['http'] = env_vars['HTTP_PROXY']['value'] - if 'HTTPS_PROXY' in env_vars: - self._proxy['https'] = env_vars['HTTPS_PROXY']['value'] + env_vars = config.get("_reserved_environment_variables", {}) + if "HTTP_PROXY" in env_vars: + self._proxy["http"] = env_vars["HTTP_PROXY"]["value"] + if "HTTPS_PROXY" in env_vars: + self._proxy["https"] = env_vars["HTTPS_PROXY"]["value"] - if config.get('use_role'): + if config.get("use_role"): credentials = self._handle_get_ec2_role() if not credentials: return self.set_status(phantom.APP_ERROR, "Failed to get EC2 role credentials") @@ -376,8 +363,8 @@ def initialize(self): return phantom.APP_SUCCESS - self._access_key = config.get('Access Key') - self._secret_key = config.get('Secret Key') + self._access_key = config.get("Access Key") + self._secret_key = config.get("Secret Key") if not (self._access_key and self._secret_key): return self.set_status(phantom.APP_ERROR, AWSCLOUDTRAIL_BAD_ASSET_CONFIG_MSG) @@ -391,7 +378,7 @@ def finalize(self): return phantom.APP_SUCCESS -if __name__ == '__main__': +if __name__ == "__main__": import argparse @@ -401,9 +388,9 @@ def finalize(self): argparser = argparse.ArgumentParser() - argparser.add_argument('input_test_json', help='Input Test JSON file') - argparser.add_argument('-u', '--username', help='username', required=False) - argparser.add_argument('-p', '--password', help='password', required=False) + argparser.add_argument("input_test_json", help="Input Test JSON file") + argparser.add_argument("-u", "--username", help="username", required=False) + argparser.add_argument("-p", "--password", help="password", required=False) args = argparser.parse_args() session_id = None @@ -411,32 +398,32 @@ def finalize(self): username = args.username password = args.password - if (username is not None and password is None): + if username is not None and password is None: # User specified a username but not a password, so ask import getpass password = getpass.getpass("Password: ") - if (username and password): + if username and password: try: - login_url = AwsCloudtrailConnector._get_phantom_base_url() + '/login' + login_url = AwsCloudtrailConnector._get_phantom_base_url() + "/login" print("Accessing the Login page") r = requests.get(login_url, verify=False) - csrftoken = r.cookies['csrftoken'] + csrftoken = r.cookies["csrftoken"] data = dict() - data['username'] = username - data['password'] = password - data['csrfmiddlewaretoken'] = csrftoken + data["username"] = username + data["password"] = password + data["csrfmiddlewaretoken"] = csrftoken headers = dict() - headers['Cookie'] = 'csrftoken=' + csrftoken - headers['Referer'] = login_url + headers["Cookie"] = "csrftoken=" + csrftoken + headers["Referer"] = login_url print("Logging into Platform to get the session id") r2 = requests.post(login_url, verify=False, data=data, headers=headers) - session_id = r2.cookies['sessionid'] + session_id = r2.cookies["sessionid"] except Exception as e: print("Unable to get session id from the platform. Error: " + str(e)) exit(1) @@ -449,9 +436,9 @@ def finalize(self): connector = AwsCloudtrailConnector() connector.print_progress_message = True - if (session_id is not None): - in_json['user_session_token'] = session_id - connector._set_csrf_info(csrftoken, headers['Referer']) + if session_id is not None: + in_json["user_session_token"] = session_id + connector._set_csrf_info(csrftoken, headers["Referer"]) ret_val = connector._handle_action(json.dumps(in_json), None) print(json.dumps(json.loads(ret_val), indent=4)) diff --git a/awscloudtrail_consts.py b/awscloudtrail_consts.py index 633dd63..c0a9cfc 100644 --- a/awscloudtrail_consts.py +++ b/awscloudtrail_consts.py @@ -1,6 +1,6 @@ # File: awscloudtrail_consts.py # -# Copyright (c) 2019-2023 Splunk Inc. +# Copyright (c) 2019-2024 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -35,11 +35,8 @@ "Middle East (Bahrain)": "me-south-1", "AWS GovCloud (US-East)": "us-gov-east-1", "AWS GovCloud (US-West)": "us-gov-west-1", - "South America (São Paulo)": "sa-east-1" + "South America (São Paulo)": "sa-east-1", } -AWSCLOUDTRAIL_DICT_MAP = { - 'describe_trails': 'trailList', - 'lookup_events': 'Events' - } -AWSCLOUDTRAIL_INVALID_LIMIT = 'Please provide a non-zero positive integer in the max_results' -AWSCLOUDTRAIL_BAD_ASSET_CONFIG_MSG = 'Please provide access keys or select assume role check box in asset configuration' +AWSCLOUDTRAIL_DICT_MAP = {"describe_trails": "trailList", "lookup_events": "Events"} +AWSCLOUDTRAIL_INVALID_LIMIT = "Please provide a non-zero positive integer in the max_results" +AWSCLOUDTRAIL_BAD_ASSET_CONFIG_MSG = "Please provide access keys or select assume role check box in asset configuration" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4c594fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 145 +target-version = ['py39'] +verbose = true + +[tool.isort] +line_length = 145 +profile = "black" diff --git a/release_notes/2.2.8.md b/release_notes/2.2.8.md new file mode 100644 index 0000000..beaeba7 --- /dev/null +++ b/release_notes/2.2.8.md @@ -0,0 +1 @@ +* Updated dependencies to resolve security vulnerabilities [PSAAS-20421] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c4ba192..8951e25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ boto3==1.17.6 botocore==1.20.30 docutils==0.18 -six==1.15.0 diff --git a/tox.ini b/tox.ini index c4644ad..720a141 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,4 @@ [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 +extend-ignore = F403,E128,E126,E121,E127,E731,E201,E202,E203,E701,F405,E722,D,W503 diff --git a/wheels/shared/python_dateutil-2.8.2-py2.py3-none-any.whl b/wheels/shared/python_dateutil-2.8.2-py2.py3-none-any.whl deleted file mode 100644 index 8ffb923..0000000 Binary files a/wheels/shared/python_dateutil-2.8.2-py2.py3-none-any.whl and /dev/null differ diff --git a/wheels/shared/six-1.15.0-py2.py3-none-any.whl b/wheels/shared/six-1.15.0-py2.py3-none-any.whl deleted file mode 100644 index 89edace..0000000 Binary files a/wheels/shared/six-1.15.0-py2.py3-none-any.whl and /dev/null differ diff --git a/wheels/shared/urllib3-1.26.18-py2.py3-none-any.whl b/wheels/shared/urllib3-1.26.18-py2.py3-none-any.whl deleted file mode 100644 index c7337c7..0000000 Binary files a/wheels/shared/urllib3-1.26.18-py2.py3-none-any.whl and /dev/null differ