From d1980611b4ce9ff682af7bf17905ea708c63a43f Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 11:31:32 -0500 Subject: [PATCH 01/30] refactor: updated HttpSession class to be much more flexible --- laceworksdk/config.py | 1 + laceworksdk/exceptions.py | 15 ++- laceworksdk/http_session.py | 236 ++++++++++++++++++++++++------------ 3 files changed, 172 insertions(+), 80 deletions(-) diff --git a/laceworksdk/config.py b/laceworksdk/config.py index b3b36f0..29b7d3d 100644 --- a/laceworksdk/config.py +++ b/laceworksdk/config.py @@ -7,6 +7,7 @@ DEFAULT_BASE_DOMAIN = "lacework.net" DEFAULT_ACCESS_TOKEN_EXPIRATION = 3600 DEFAULT_SUCCESS_RESPONSE_CODES = [200, 201, 204] +RATE_LIMIT_RESPONSE_CODE = 429 # Environment Variable Definitions LACEWORK_ACCOUNT_ENVIRONMENT_VARIABLE = "LW_ACCOUNT" diff --git a/laceworksdk/exceptions.py b/laceworksdk/exceptions.py index 8bb44e4..08590f1 100644 --- a/laceworksdk/exceptions.py +++ b/laceworksdk/exceptions.py @@ -57,7 +57,7 @@ def __init__(self, response): self.message = None """The error message from the parsed API response.""" - super(ApiError, self).__init__( + super().__init__( "[{status_code}]{status} - {message}".format( status_code=self.status_code, status=" " + self.status if self.status else "", @@ -70,3 +70,16 @@ def __repr__(self): exception_name=self.__class__.__name__, status_code=self.status_code, ) + + +class MalformedResponse(laceworksdkException): + """Raised when a malformed response is received from Lacework.""" + pass + + +class RateLimitError(ApiError): + """LAcework Rate-Limit exceeded Error. + + Raised when a rate-limit exceeded message is received and the request **will not** be retried. + """ + pass diff --git a/laceworksdk/http_session.py b/laceworksdk/http_session.py index 89ad002..45e0d57 100644 --- a/laceworksdk/http_session.py +++ b/laceworksdk/http_session.py @@ -15,14 +15,15 @@ from laceworksdk.config import ( DEFAULT_BASE_DOMAIN, DEFAULT_ACCESS_TOKEN_EXPIRATION, - DEFAULT_SUCCESS_RESPONSE_CODES + DEFAULT_SUCCESS_RESPONSE_CODES, + RATE_LIMIT_RESPONSE_CODE ) -from laceworksdk.exceptions import ApiError +from laceworksdk.exceptions import ApiError, MalformedResponse, RateLimitError logger = logging.getLogger(__name__) -class HttpSession(object): +class HttpSession: """ Package HttpSession class. """ @@ -43,7 +44,7 @@ def __init__(self, account, subaccount, api_key, api_secret, base_domain): :return HttpSession object. """ - super(HttpSession, self).__init__() + super().__init__() # Create a requests session self._session = self._retry_session() @@ -54,6 +55,7 @@ def __init__(self, account, subaccount, api_key, api_secret, base_domain): self._base_domain = base_domain or DEFAULT_BASE_DOMAIN self._base_url = f"https://{account}.{self._base_domain}" self._subaccount = subaccount + self._org_level_access = False # Get an access token self._check_access_token() @@ -61,7 +63,8 @@ def __init__(self, account, subaccount, api_key, api_secret, base_domain): def _retry_session(self, retries=3, backoff_factor=0.3, - status_forcelist=(500, 502, 504)): + status_forcelist=(500, 502, 503, 504), + allowed_methods=None): """ A method to set up automatic retries on HTTP requests that fail. """ @@ -70,17 +73,15 @@ def _retry_session(self, session = requests.Session() # Establish the retry criteria - retry = Retry( + retry_strategy = Retry( total=retries, - read=retries, - connect=retries, - status=retries, backoff_factor=backoff_factor, status_forcelist=status_forcelist, + allowed_methods=allowed_methods ) # Build the adapter with the retry criteria - adapter = HTTPAdapter(max_retries=retry) + adapter = HTTPAdapter(max_retries=retry_strategy) # Bind the adapter to HTTP/HTTPS calls session.mount("http://", adapter) @@ -110,6 +111,8 @@ def _check_response_code(self, response, expected_response_codes): """ if response.status_code in expected_response_codes: pass + elif response.status_code == RATE_LIMIT_RESPONSE_CODE: + raise RateLimitError(response) else: raise ApiError(response) @@ -118,6 +121,8 @@ def _print_debug_response(self, response): Print the debug logging, based on the returned content type. """ + logger.debug(response.headers) + # If it's supposed to be a JSON response, parse and log, otherwise, log the raw text if "application/json" in response.headers.get("Content-Type", "").lower(): try: @@ -184,7 +189,7 @@ def _get_request_headers(self, org_access=False): headers = self._session.headers headers["Authorization"] = f"Bearer {self._access_token}" - headers["Org-Access"] = "true" if org_access else "false" + headers["Org-Access"] = "true" if self._org_level_access or org_access else "false" headers["User-Agent"] = f"laceworksdk-python-client/{version}" if self._subaccount: @@ -194,141 +199,214 @@ def _get_request_headers(self, org_access=False): return headers - def get(self, uri, org=False): + def _request(self, method, uri, **kwargs): """ - :param uri: uri to send the HTTP GET request to - :param org: boolean representing whether the request should be performed at the Organization level + A method to abstract building requests to Lacework. + + :param method: string representing the HTTP request method ("GET", "POST", ...) + :param uri: string representing the URI of the API endpoint + :param kwargs: passed on to the requests package :return: response json - :raises: ApiError if unable to get a connection + :raises: ApiError if anything but expected response code is returned """ self._check_access_token() + # Strip the protocol/host if provided + domain_begin = uri.find(self._base_domain) + if domain_begin >= 0: + domain_end = domain_begin + len(self._base_domain) + uri = uri[domain_end:] + uri = f"{self._base_url}{uri}" - logger.info(f"GET request to URI: {uri}") + logger.info(f"{method} request to URI: {uri}") - # Perform a GET request - response = self._session.get(uri, headers=self._get_request_headers(org_access=org)) + # Check for 'org' - if True, make an organization-level API call + # TODO: Remove this on v1.0 release - this is done for back compat + org = kwargs.pop("org", None) + headers = self._get_request_headers(org_access=org) + + # Check for 'data' or 'json' + data = kwargs.get("data", "") + json = kwargs.get("json", "") + if data or json: + logger.debug(f"{method} request data:\n{data}{json}") + + # TODO: Remove this on v1.0 release - this is done for back compat + if data and not json: + kwargs["json"] = data + kwargs.pop("data") + + # Make the HTTP request to the API endpoint + response = self._session.request(method, uri, headers=headers, **kwargs) # Validate the response self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES) self._print_debug_response(response) + # Fix for when Lacework returns a 204 with no data on searches + if method != "DELETE" and response.status_code == 204: + try: + response.json() + except Exception: + response._content = b'{"data": []}' + return response - def patch(self, uri, org=False, data=None, param=None): + def get(self, uri, params=None, **kwargs): """ - :param uri: uri to send the HTTP POST request to - :param org: boolean representing whether the request should be performed at the Organization level - :param data: json object containing the data - :param param: python object containing the parameters + A method to build a GET request to interact with Lacework. + + :param uri: uri to send the HTTP GET request to + :param params: dict of parameters for the HTTP request + :param kwargs: passed on to the requests package :return: response json - :raises: ApiError if unable to get a connection + :raises: ApiError if anything but expected response code is returned """ - self._check_access_token() + # Perform a GET request + response = self._request("GET", uri, params=params, **kwargs) - uri = f"{self._base_url}{uri}" + return response + + def get_pages(self, uri, params=None, **kwargs): + """ + A method to build a GET request that yields pages of data returned by Lacework. - logger.info(f"PATCH request to URI: {uri}") - logger.debug(f"PATCH request data:\n{data}") + :param uri: uri to send the initial HTTP GET request to + :param params: dict of parameters for the HTTP request + :param kwargs: passed on to the requests package - # Perform a PATCH request - response = self._session.patch(uri, params=param, json=data, headers=self._get_request_headers(org_access=org)) + :return: a generator that yields pages of data - # Validate the response - self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES) + :raises: ApiError if anything but expected response code is returned + """ - self._print_debug_response(response) + response = self.get(uri, params=params, **kwargs) - return response + while True: + yield response - def post(self, uri, org=False, data=None, param=None): - """ - :param uri: uri to send the HTTP POST request to - :param org: boolean representing whether the request should be performed at the Organization level - :param data: json object containing the data - :param param: python object containing the parameters + try: + response_json = response.json() + next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage") + except Exception: + next_page = None - :return: response json + if next_page: + response = self.get(next_page, params=params, **kwargs) + else: + break - :raises: ApiError if unable to get a connection + def get_data_items(self, uri, params=None, **kwargs): """ + A method to build a GET request that yields individual objects as returned by Lacework. - self._check_access_token() + :param uri: uri to send the initial HTTP GET request to + :param params: dict of parameters for the HTTP request + :param kwargs: passed on to the requests package - uri = f"{self._base_url}{uri}" + :return: a generator that yields individual objects from pages of data - logger.info(f"POST request to URI: {uri}") - logger.debug(f"POST request data:\n{data}") + :raises: ApiError if anything but expected response code is returned + :raises: MalformedResponse if the returned response does not contain a + top-level dictionary with an "data" key. + """ - # Perform a POST request - response = self._session.post(uri, params=param, json=data, headers=self._get_request_headers(org_access=org)) + # Get generator for pages of JSON data + pages = self.get_pages(uri, params=params, **kwargs) - # Validate the response - self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES) + for page in pages: + page = page.json() + assert isinstance(page, dict) - self._print_debug_response(response) + items = page.get("data") - return response + if items is None: + error_message = f"'data' key not found in JSON data:\n{page}" + raise MalformedResponse(error_message) + + for item in items: + yield item - def put(self, uri, org=False, data=None, param=None): + def patch(self, uri, data=None, json=None, **kwargs): """ + A method to build a PATCH request to interact with Lacework. + :param uri: uri to send the HTTP POST request to - :param org: boolean representing whether the request should be performed at the Organization level - :param data: json object containing the data - :param param: python object containing the parameters + :param data: data to be sent in the body of the request + :param json: data to be sent in JSON format in the body of the request + :param kwargs: passed on to the requests package :return: response json - :raises: ApiError if unable to get a connection + :raises: ApiError if anything but expected response code is returned """ - self._check_access_token() + # Perform a PATCH request + response = self._request("PATCH", uri, data=data, json=json, **kwargs) - uri = f"{self._base_url}{uri}" + return response - logger.info(f"PUT request to URI: {uri}") - logger.debug(f"PUT request data:\n{data}") + def post(self, uri, data=None, json=None, **kwargs): + """ + A method to build a POST request to interact with Lacework. - # Perform a PUT request - response = self._session.put(uri, params=param, json=data, headers=self._get_request_headers(org_access=org)) + :param uri: uri to send the HTTP POST request to + :param data: data to be sent in the body of the request + :param json: data to be sent in JSON format in the body of the request + :param kwargs: passed on to the requests package - # Validate the response - self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES) + :return: response json - self._print_debug_response(response) + :raises: ApiError if anything but expected response code is returned + """ + + # Perform a POST request + response = self._request("POST", uri, data=data, json=json, **kwargs) return response - def delete(self, uri, data=None, org=False): + def put(self, uri, data=None, json=None, **kwargs): """ - :param uri: uri to send the http DELETE request to - :param org: boolean representing whether the request should be performed at the Organization level + A method to build a PUT request to interact with Lacework. - :response: reponse json + :param uri: uri to send the HTTP POST request to + :param data: data to be sent in the body of the request + :param json: data to be sent in JSON format in the body of the request + :param kwargs: passed on to the requests package - :raises: ApiError if unable to get a connection + :return: response json + + :raises: ApiError if anything but expected response code is returned """ - self._check_access_token() + # Perform a PUT request + response = self._request("PUT", uri, data=data, json=json, **kwargs) - uri = f"{self._base_url}{uri}" + return response + + def delete(self, uri, data=None, json=None, **kwargs): + """ + A method to build a DELETE request to interact with Lacework. - logger.info(f"DELETE request to URI: {uri}") + :param uri: uri to send the http DELETE request to + :param data: data to be sent in the body of the request + :param json: data to be sent in JSON format in the body of the request + :param kwargs: passed on to the requests package - # Perform a DELETE request - response = self._session.delete(uri, json=data, headers=self._get_request_headers(org_access=org)) + :response: reponse json - # Validate the response - self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES) + :raises: ApiError if anything but expected response code is returned + """ - self._print_debug_response(response) + # Perform a DELETE request + response = self._request("DELETE", uri, data=data, json=json, **kwargs) return response From 8386f54e470c9738e7911ccc8d25890e0dcf9259 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 11:33:43 -0500 Subject: [PATCH 02/30] feat: allow org-level access and sub-account to be modified dynamically --- laceworksdk/api/__init__.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/laceworksdk/api/__init__.py b/laceworksdk/api/__init__.py index 9af4591..50d8ac6 100644 --- a/laceworksdk/api/__init__.py +++ b/laceworksdk/api/__init__.py @@ -149,3 +149,19 @@ def __init__(self, self.tokens = TokenAPI(self._session) self.user_profile = UserProfileAPI(self._session) self.vulnerabilities = VulnerabilityAPI(self._session) + def set_org_level_access(self, org_level_access): + """ + A method to set whether the client should use organization-level API calls. + """ + + if org_level_access is True: + self._session._org_level_access = True + else: + self._session._org_level_access = False + + def set_subaccount(self, subaccount): + """ + A method to update the subaccount the client should use for API calls. + """ + + self._session._subaccount = subaccount From b585fce2db2a734f99badae444e679a6ab2e2841 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 11:36:39 -0500 Subject: [PATCH 03/30] refactor: created three base class types that endpoints should inherit --- laceworksdk/api/base_endpoint.py | 109 ++++++++++++++++++++++++++ laceworksdk/api/crud_endpoint.py | 118 +++++++++++++++++++++++++++++ laceworksdk/api/search_endpoint.py | 44 +++++++++++ 3 files changed, 271 insertions(+) create mode 100644 laceworksdk/api/base_endpoint.py create mode 100644 laceworksdk/api/crud_endpoint.py create mode 100644 laceworksdk/api/search_endpoint.py diff --git a/laceworksdk/api/base_endpoint.py b/laceworksdk/api/base_endpoint.py new file mode 100644 index 0000000..688362e --- /dev/null +++ b/laceworksdk/api/base_endpoint.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +class BaseEndpoint: + """ + Lacework BaseEndpoint Class. + """ + + def __init__(self, + session, + object_type, + endpoint_root="/api/v2"): + """ + :param session: An instance of the HttpSession class. + :param object_type: The Lacework object type to use. + :param endpoint_root: The URL endpoint root to use. + """ + + super().__init__() + self._session = session + self._object_type = object_type + self._endpoint_root = endpoint_root + + def build_dict_from_items(self, *dicts, **items): + """ + A method to build a dictionary based on inputs, pruning items that are None + """ + + dict_list = list(dicts) + dict_list.append(items) + result = {} + + for d in dict_list: + for key, value in d.items(): + camel_key = self._convert_lower_camel_case(key) + if value is not None: + if camel_key not in result.keys(): + result[camel_key] = value + else: + raise KeyError(f"Attempted to insert duplicate key '{camel_key}'") + + return result + + def build_url(self, id=None, resource=None, action=None): + """ + Builds the URL to use based on the endpoint path, resource, type, and ID. + """ + + result = f"{self._endpoint_root}/{self._object_type}" + + if resource: + result += f"/{resource}" + if action: + result += f"/{action}" + if id: + result += f"/{id}" + + return result + + @staticmethod + def _convert_lower_camel_case(param_name): + """ + Convert a Pythonic variable name to lowerCamelCase. + """ + + words = param_name.split("_") + first_word = words[0] + + if len(words) == 1: + return first_word + + word_string = "".join([x.capitalize() or "_" for x in words[1:]]) + + return f"{first_word}{word_string}" + + def _get_schema(self, subtype=None): + """ + Get the schema for the current object type. + """ + if subtype: + url = f"/api/v2/schemas/{self._object_type}/{subtype}" + else: + url = f"/api/v2/schemas/{self._object_type}" + + response = self._session.get(url) + + return response.json() + + @property + def session(self): + """ + Get the :class:`HttpSession` instance the object is using. + """ + + return self._session + + def validate_json(self, json, subtype=None): + """ + TODO: A method to validate the provided JSON based on the schema of the current object. + """ + + schema = self._get_schema(subtype) + + # TODO: perform validation here + + return schema + + def __repr__(self): + if hasattr(self, "id"): + return "<%s %s>" % (self.__class__.__name__, self.id) diff --git a/laceworksdk/api/crud_endpoint.py b/laceworksdk/api/crud_endpoint.py new file mode 100644 index 0000000..fa2e67d --- /dev/null +++ b/laceworksdk/api/crud_endpoint.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class CrudEndpoint(BaseEndpoint): + + def __init__(self, + session, + object_type, + endpoint_root="/api/v2"): + """ + :param session: An instance of the HttpSession class. + :param object_type: The Lacework object type to use. + :param endpoint_root: The URL endpoint root to use. + """ + + super().__init__(session, object_type, endpoint_root) + + def create(self, **request_params): + """ + A method to create a new object. + + :param request_params: Request parameters. + + :return response json + """ + + json = self.build_dict_from_items( + request_params + ) + + response = self._session.post(self.build_url(), json=json) + + return response.json() + + def get(self, id=None, resource=None, **request_params): + """ + A method to get objects. + + :param guid: A string representing the object ID. + :param type: A string representing the object resource type. + :param request_params: A dictionary of parameters to add to the request. + + :return response json + """ + + params = self.build_dict_from_items( + request_params + ) + + response = self._session.get(self.build_url(id=id, resource=resource), params=params) + + return response.json() + + def search(self, json=None, **kwargs): + """ + A method to search objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + # TODO: Remove this on v1.0 release - provided for back compat + if kwargs["query_data"]: + json = kwargs["query_data"] + + response = self._session.post(self.build_url(action="search"), json=json) + + return response.json() + + def update(self, id=None, **request_params): + """ + A method to update an object. + + :param guid: A string representing the object ID. + :param request_params: Request parameters. + + :return response json + """ + + json = self.build_dict_from_items( + request_params + ) + + response = self._session.patch(self.build_url(id=id), json=json) + + return response.json() + + def delete(self, id): + """ + A method to delete an object. + + :param guid: A string representing the alert channel GUID. + + :return response json + """ + + response = self._session.delete(self.build_url(id=id)) + + return response + + def _format_filters(self, + filters): + """ + A method to properly format the filters object. + + :param filters: A dict of filters to be properly formatted (boolean conversion). + + :return json + """ + + if "enabled" in filters.keys(): + filters["enabled"] = int(bool(filters["enabled"])) + + return filters diff --git a/laceworksdk/api/search_endpoint.py b/laceworksdk/api/search_endpoint.py new file mode 100644 index 0000000..82ae846 --- /dev/null +++ b/laceworksdk/api/search_endpoint.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class SearchEndpoint(BaseEndpoint): + + def __init__(self, + session, + object_type, + endpoint_root="/api/v2"): + """ + :param session: An instance of the HttpSession class. + :param object_type: The Lacework object type to use. + :param endpoint_root: The URL endpoint root to use. + """ + + super().__init__(session, object_type, endpoint_root) + + def search(self, json=None, resource=None, **kwargs): + """ + A method to search objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return a generator which yields a page of objects at a time as returned by the Lacework API. + """ + + response = self._session.post(self.build_url(resource=resource, action="search"), json=json) + + while True: + response_json = response.json() + yield response_json + + try: + next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage") + except Exception: + next_page = None + + if next_page: + response = self._session.get(next_page, **kwargs) + else: + break From 1c6f698033e95c4c2486b7a3a1414cf90cdb67a5 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 11:44:16 -0500 Subject: [PATCH 04/30] refactor: reorganized existing code and refactored to use new base classes --- laceworksdk/api/__init__.py | 69 ++--- laceworksdk/api/agent_access_tokens.py | 152 ----------- laceworksdk/api/alert_channels.py | 238 ----------------- laceworksdk/api/alert_rules.py | 234 ----------------- laceworksdk/api/audit_logs.py | 78 ------ laceworksdk/api/cloud_accounts.py | 210 --------------- laceworksdk/api/cloud_activities.py | 77 ------ laceworksdk/api/container_registries.py | 210 --------------- laceworksdk/api/contract_info.py | 54 ---- laceworksdk/api/datasources.py | 108 -------- laceworksdk/api/policies.py | 225 ---------------- laceworksdk/api/queries.py | 248 ------------------ laceworksdk/api/report_rules.py | 246 ----------------- laceworksdk/api/resource_groups.py | 189 ------------- laceworksdk/api/team_members.py | 225 ---------------- laceworksdk/api/user_profile.py | 45 ---- laceworksdk/api/v1/__init__.py | 0 laceworksdk/api/{ => v1}/account.py | 4 +- laceworksdk/api/{ => v1}/compliance.py | 4 +- .../api/{ => v1}/custom_compliance_config.py | 4 +- laceworksdk/api/{ => v1}/download_file.py | 4 +- laceworksdk/api/{ => v1}/events.py | 4 +- laceworksdk/api/{ => v1}/integrations.py | 4 +- laceworksdk/api/{ => v1}/recommendations.py | 4 +- laceworksdk/api/{ => v1}/run_reports.py | 4 +- laceworksdk/api/{ => v1}/suppressions.py | 4 +- laceworksdk/api/{ => v1}/token.py | 4 +- laceworksdk/api/{ => v1}/vulnerability.py | 4 +- laceworksdk/api/v2/__init__.py | 0 laceworksdk/api/v2/agent_access_tokens.py | 110 ++++++++ laceworksdk/api/v2/alert_channels.py | 160 +++++++++++ laceworksdk/api/v2/alert_rules.py | 143 ++++++++++ laceworksdk/api/v2/audit_logs.py | 65 +++++ laceworksdk/api/v2/cloud_accounts.py | 143 ++++++++++ laceworksdk/api/v2/cloud_activities.py | 99 +++++++ laceworksdk/api/v2/container_registries.py | 143 ++++++++++ laceworksdk/api/v2/contract_info.py | 45 ++++ laceworksdk/api/v2/datasources.py | 82 ++++++ laceworksdk/api/v2/policies.py | 162 ++++++++++++ laceworksdk/api/v2/queries.py | 192 ++++++++++++++ laceworksdk/api/v2/report_rules.py | 145 ++++++++++ laceworksdk/api/v2/resource_groups.py | 129 +++++++++ laceworksdk/api/{ => v2}/schemas.py | 27 +- laceworksdk/api/v2/team_members.py | 163 ++++++++++++ laceworksdk/api/v2/user_profile.py | 31 +++ tests/api/v1/test_account.py | 2 +- tests/api/v1/test_compliance.py | 2 +- tests/api/v1/test_custom_compliance_config.py | 2 +- tests/api/v1/test_download_file.py | 2 +- tests/api/v1/test_events.py | 2 +- tests/api/v1/test_integrations.py | 2 +- tests/api/v1/test_recommendations.py | 2 +- tests/api/v1/test_run_reports.py | 2 +- tests/api/v1/test_suppressions.py | 2 +- tests/api/v1/test_token.py | 2 +- tests/api/v1/test_vulnerability.py | 2 +- tests/api/v2/test_agent_access_tokens.py | 2 +- tests/api/v2/test_alert_channels.py | 29 +- tests/api/v2/test_alert_rules.py | 2 +- tests/api/v2/test_audit_logs.py | 2 +- tests/api/v2/test_cloud_accounts.py | 2 +- tests/api/v2/test_cloud_activities.py | 41 ++- tests/api/v2/test_container_registries.py | 2 +- tests/api/v2/test_contract_info.py | 2 +- tests/api/v2/test_datasources.py | 30 +++ tests/api/v2/test_policies.py | 2 +- tests/api/v2/test_queries.py | 2 +- tests/api/v2/test_report_rules.py | 2 +- tests/api/v2/test_resource_groups.py | 2 +- tests/api/v2/test_schemas.py | 2 +- tests/api/v2/test_team_members.py | 8 +- tests/api/v2/test_user_profile.py | 4 +- 72 files changed, 2000 insertions(+), 2647 deletions(-) delete mode 100644 laceworksdk/api/agent_access_tokens.py delete mode 100644 laceworksdk/api/alert_channels.py delete mode 100644 laceworksdk/api/alert_rules.py delete mode 100644 laceworksdk/api/audit_logs.py delete mode 100644 laceworksdk/api/cloud_accounts.py delete mode 100644 laceworksdk/api/cloud_activities.py delete mode 100644 laceworksdk/api/container_registries.py delete mode 100644 laceworksdk/api/contract_info.py delete mode 100644 laceworksdk/api/datasources.py delete mode 100644 laceworksdk/api/policies.py delete mode 100644 laceworksdk/api/queries.py delete mode 100644 laceworksdk/api/report_rules.py delete mode 100644 laceworksdk/api/resource_groups.py delete mode 100644 laceworksdk/api/team_members.py delete mode 100644 laceworksdk/api/user_profile.py create mode 100644 laceworksdk/api/v1/__init__.py rename laceworksdk/api/{ => v1}/account.py (91%) rename laceworksdk/api/{ => v1}/compliance.py (98%) rename laceworksdk/api/{ => v1}/custom_compliance_config.py (93%) rename laceworksdk/api/{ => v1}/download_file.py (92%) rename laceworksdk/api/{ => v1}/events.py (97%) rename laceworksdk/api/{ => v1}/integrations.py (98%) rename laceworksdk/api/{ => v1}/recommendations.py (95%) rename laceworksdk/api/{ => v1}/run_reports.py (96%) rename laceworksdk/api/{ => v1}/suppressions.py (97%) rename laceworksdk/api/{ => v1}/token.py (98%) rename laceworksdk/api/{ => v1}/vulnerability.py (99%) create mode 100644 laceworksdk/api/v2/__init__.py create mode 100644 laceworksdk/api/v2/agent_access_tokens.py create mode 100644 laceworksdk/api/v2/alert_channels.py create mode 100644 laceworksdk/api/v2/alert_rules.py create mode 100644 laceworksdk/api/v2/audit_logs.py create mode 100644 laceworksdk/api/v2/cloud_accounts.py create mode 100644 laceworksdk/api/v2/cloud_activities.py create mode 100644 laceworksdk/api/v2/container_registries.py create mode 100644 laceworksdk/api/v2/contract_info.py create mode 100644 laceworksdk/api/v2/datasources.py create mode 100644 laceworksdk/api/v2/policies.py create mode 100644 laceworksdk/api/v2/queries.py create mode 100644 laceworksdk/api/v2/report_rules.py create mode 100644 laceworksdk/api/v2/resource_groups.py rename laceworksdk/api/{ => v2}/schemas.py (55%) create mode 100644 laceworksdk/api/v2/team_members.py create mode 100644 laceworksdk/api/v2/user_profile.py create mode 100644 tests/api/v2/test_datasources.py diff --git a/laceworksdk/api/__init__.py b/laceworksdk/api/__init__.py index 50d8ac6..875e5d0 100644 --- a/laceworksdk/api/__init__.py +++ b/laceworksdk/api/__init__.py @@ -9,33 +9,34 @@ import configparser from laceworksdk.http_session import HttpSession -from .account import AccountAPI -from .agent_access_tokens import AgentAccessTokensAPI -from .alert_channels import AlertChannelsAPI -from .alert_rules import AlertRulesAPI -from .audit_logs import AuditLogsAPI -from .cloud_accounts import CloudAccountsAPI -from .cloud_activities import CloudActivitiesAPI -from .compliance import ComplianceAPI -from .container_registries import ContainerRegistriesAPI -from .contract_info import ContractInfoAPI -from .custom_compliance_config import CustomComplianceConfigAPI -from .datasources import DatasourcesAPI -from .download_file import DownloadFileAPI -from .events import EventsAPI -from .integrations import IntegrationsAPI -from .policies import PoliciesAPI -from .queries import QueriesAPI -from .recommendations import RecommendationsAPI -from .report_rules import ReportRulesAPI -from .resource_groups import ResourceGroupsAPI -from .run_reports import RunReportsAPI -from .schemas import SchemasAPI -from .suppressions import SuppressionsAPI -from .team_members import TeamMembersAPI -from .token import TokenAPI -from .user_profile import UserProfileAPI -from .vulnerability import VulnerabilityAPI + +from .v1.account import AccountAPI +from .v1.compliance import ComplianceAPI +from .v1.custom_compliance_config import CustomComplianceConfigAPI +from .v1.download_file import DownloadFileAPI +from .v1.events import EventsAPI +from .v1.integrations import IntegrationsAPI +from .v1.recommendations import RecommendationsAPI +from .v1.run_reports import RunReportsAPI +from .v1.suppressions import SuppressionsAPI +from .v1.token import TokenAPI + +from .v2.agent_access_tokens import AgentAccessTokensAPI +from .v2.alert_channels import AlertChannelsAPI +from .v2.alert_rules import AlertRulesAPI +from .v2.audit_logs import AuditLogsAPI +from .v2.cloud_accounts import CloudAccountsAPI +from .v2.cloud_activities import CloudActivitiesAPI +from .v2.container_registries import ContainerRegistriesAPI +from .v2.contract_info import ContractInfoAPI +from .v2.datasources import DatasourcesAPI +from .v2.policies import PoliciesAPI +from .v2.queries import QueriesAPI +from .v2.report_rules import ReportRulesAPI +from .v2.resource_groups import ResourceGroupsAPI +from .v2.schemas import SchemasAPI +from .v2.team_members import TeamMembersAPI +from .v2.user_profile import UserProfileAPI from laceworksdk.config import ( LACEWORK_ACCOUNT_ENVIRONMENT_VARIABLE, @@ -50,7 +51,7 @@ load_dotenv() -class LaceworkClient(object): +class LaceworkClient: """ Lacework API wrapper for Python. """ @@ -87,28 +88,28 @@ def __init__(self, LACEWORK_API_BASE_DOMAIN_ENVIRONMENT_VARIABLE) config_file_path = os.path.join( - os.path.expanduser('~'), LACEWORK_CLI_CONFIG_RELATIVE_PATH) + os.path.expanduser("~"), LACEWORK_CLI_CONFIG_RELATIVE_PATH) if os.path.isfile(config_file_path): profile = profile or os.getenv( - LACEWORK_API_CONFIG_SECTION_ENVIRONMENT_VARIABLE, 'default') + LACEWORK_API_CONFIG_SECTION_ENVIRONMENT_VARIABLE, "default") config_obj = configparser.ConfigParser() config_obj.read([config_file_path]) if config_obj.has_section(profile): config_section = config_obj[profile] - api_key = config_section.get('api_key', '').strip('""') + api_key = config_section.get("api_key", "").strip('""') if not self._api_key and api_key: self._api_key = api_key - api_secret = config_section.get('api_secret', '').strip('""') + api_secret = config_section.get("api_secret", "").strip('""') if not self._api_secret and api_secret: self._api_secret = api_secret - account = config_section.get('account', '').strip('""') + account = config_section.get("account", "").strip('""') if not self._account and account: self._account = account - subaccount = config_section.get('subaccount', '').strip('""') + subaccount = config_section.get("subaccount", "").strip('""') if not self._subaccount and subaccount: self._subaccount = subaccount diff --git a/laceworksdk/api/agent_access_tokens.py b/laceworksdk/api/agent_access_tokens.py deleted file mode 100644 index a708b58..0000000 --- a/laceworksdk/api/agent_access_tokens.py +++ /dev/null @@ -1,152 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Agent Access Tokens API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class AgentAccessTokensAPI(object): - """ - Lacework Agent Access Tokens API. - """ - - def __init__(self, session): - """ - Initializes the AgentAccessTokensAPI object. - - :param session: An instance of the HttpSession class - - :return AgentAccessTokensAPI object. - """ - - super(AgentAccessTokensAPI, self).__init__() - - self._session = session - - def create(self, - alias, - enabled=True, - org=False): - """ - A method to create a new agent access token. - - :param alias: A string representing the alias of the agent access token. - :param enabled: A boolean/integer representing whether the agent access token is enabled. - (0 or 1) - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating agent access token in Lacework...") - - # Build the Agent Access Tokens request URI - api_uri = "/api/v2/AgentAccessTokens" - - data = { - "tokenAlias": alias, - "tokenEnabled": int(bool(enabled)) - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - id=None, - org=False): - """ - A method to get agent access tokens. - - :param id: A string representing the agent access token ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting agent access token info from Lacework...") - - # Build the Agent Access Tokens request URI - if id: - api_uri = f"/api/v2/AgentAccessTokens/{id}" - else: - api_uri = "/api/v2/AgentAccessTokens" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_id(self, - id, - org=False): - """ - A method to get an agent access token by ID. - - :param id: A string representing the agent access token ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(id=id, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search agent access tokens. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching agent access tokens from Lacework...") - - # Build the Agent Access Tokens request URI - api_uri = "/api/v2/AgentAccessTokens/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - return response.json() - - def update(self, - id, - alias=None, - enabled=None, - org=False): - """ - A method to update an agent access token. - - :param id: A string representing the agent access token ID. - :param alias: A string representing the alias of the agent access token. - :param enabled: A boolean/integer representing whether the agent access token is enabled. - (0 or 1) - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating agent access token in Lacework...") - - # Build the Agent Access Tokens request URI - api_uri = f"/api/v2/AgentAccessTokens/{id}" - - tmp_data = {} - - if alias: - tmp_data["tokenAlias"] = alias - if enabled is not None: - tmp_data["tokenEnabled"] = int(bool(enabled)) - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() diff --git a/laceworksdk/api/alert_channels.py b/laceworksdk/api/alert_channels.py deleted file mode 100644 index 96f01db..0000000 --- a/laceworksdk/api/alert_channels.py +++ /dev/null @@ -1,238 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Alert Channels API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class AlertChannelsAPI(object): - """ - Lacework Alert Channels API. - """ - - def __init__(self, session): - """ - Initializes the AlertChannelsAPI object. - - :param session: An instance of the HttpSession class - - :return AlertChannelsAPI object. - """ - - super(AlertChannelsAPI, self).__init__() - - self._session = session - - def create(self, - name, - type, - enabled, - data, - org=False): - """ - A method to create a new alert channel. - - :param name: A string representing the alert channel name. - :param type: A string representing the alert channel type. - :param enabled: A boolean/integer representing whether the alert channel is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating alert channel in Lacework...") - - # Build the Alert Channels request URI - api_uri = "/api/v2/AlertChannels" - - data = { - "name": name, - "type": type, - "enabled": int(bool(enabled)), - "data": data - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - guid=None, - type=None, - org=False): - """ - A method to get all alert channels. - - :param guid: A string representing the alert channel GUID. - :param type: A string representing the alert channel type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting alert channel info from Lacework...") - - # Build the Alert Channels request URI - if guid: - api_uri = f"/api/v2/AlertChannels/{guid}" - elif type: - api_uri = f"/api/v2/AlertChannels/{type}" - else: - api_uri = "/api/v2/AlertChannels" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_type(self, - type, - org=False): - """ - A method to get all alert channels by type. - - :param type: A string representing the alert channel type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(type=type, org=org) - - def get_by_guid(self, - guid, - org=False): - """ - A method to get all alert channels. - - :param guid: A string representing the alert channel GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search alert channels. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching alert channels from Lacework...") - - # Build the Alert Channels request URI - api_uri = "/api/v2/AlertChannels/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - if response.status_code == 204: - return {"data": []} - else: - return response.json() - - def update(self, - guid, - name=None, - type=None, - enabled=None, - data=None, - org=False): - """ - A method to update an alert channel. - - :param guid: A string representing the alert channel GUID. - :param name: A string representing the alert channel name. - :param type: A string representing the alert channel type. - :param enabled: A boolean/integer representing whether the alert channel is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating alert channel in Lacework...") - - # Build the Alert Channels request URI - api_uri = f"/api/v2/AlertChannels/{guid}" - - tmp_data = {} - - if name: - tmp_data["name"] = name - if type: - tmp_data["type"] = type - if enabled is not None: - tmp_data["enabled"] = int(bool(enabled)) - if data: - tmp_data["data"] = data - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, - guid, - org=False): - """ - A method to delete an alert channel. - - :param guid: A string representing the alert channel GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting alert channel in Lacework...") - - # Build the Alert Channels request URI - api_uri = f"/api/v2/AlertChannels/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() - - def test(self, - guid, - org=False): - """ - A method to test an alert channel. - - :param guid: A string representing the alert channel GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Testing alert channel in Lacework...") - - # Build the Alert Channels request URI - api_uri = f"/api/v2/AlertChannels/{guid}/test" - - response = self._session.post(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() diff --git a/laceworksdk/api/alert_rules.py b/laceworksdk/api/alert_rules.py deleted file mode 100644 index 9edda62..0000000 --- a/laceworksdk/api/alert_rules.py +++ /dev/null @@ -1,234 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Alert Rules API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class AlertRulesAPI(object): - """ - Lacework Alert Rules API. - """ - - def __init__(self, session): - """ - Initializes the AlertRulesAPI object. - - :param session: An instance of the HttpSession class - - :return AlertRulesAPI object. - """ - - super(AlertRulesAPI, self).__init__() - - self._session = session - - def create(self, - type, - filters, - intg_guid_list, - org=False): - """ - A method to create a new alert rule. - - :param type: A string representing the type of the alert rule. - ('Event') - :param filters: A filter object for the alert rule configuration. - obj: - :param name: A string representing the alert rule name. - :param description: A string representing the alert rule description. - :param enabled: A boolean/integer representing whether the alert rule is enabled. - (0 or 1) - :param resourceGroups: A list of resource groups to define for the alert rule. - :param eventCategory: A list of event categories to define for the alert rule. - ("Compliance", "App", "Cloud", "Aws", "AzureActivityLog", "GcpAuditTrail", - "File", "Machine", "User") - :param severity: A list of alert severities to define for the alert rule. - (1, 2, 3, 4, 5) - :param intg_guid_list: A list of integration GUIDs representing the alert channels to use. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating alert rule in Lacework...") - - # Build the Alert Rules request URI - api_uri = "/api/v2/AlertRules" - - data = { - "type": type, - "filters": self._build_filters(filters), - "intgGuidList": intg_guid_list - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - guid=None, - org=False): - """ - A method to get alert rules. - - :param guid: A string representing the alert rule GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting alert rule info from Lacework...") - - # Build the Alert Rules request URI - if guid: - api_uri = f"/api/v2/AlertRules/{guid}" - else: - api_uri = "/api/v2/AlertRules" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_guid(self, - guid, - org=False): - """ - A method to get an alert rule by GUID. - - :param guid: A string representing the alert rule GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search alert rules. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching alert rules from Lacework...") - - # Build the Alert Rules request URI - api_uri = "/api/v2/AlertRules/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - if response.status_code == 204: - return {"data": []} - else: - return response.json() - - def update(self, - guid, - type=None, - filters=None, - intg_guid_list=None, - org=False): - """ - A method to update an alert rule. - - :param guid: A string representing the alert rule GUID. - :param type: A string representing the type of the alert rule. - ('Event') - :param filters: A filter object for the alert rule configuration. - obj: - :param name: A string representing the alert rule name. - :param description: A string representing the alert rule description. - :param enabled: A boolean/integer representing whether the alert rule is enabled. - (0 or 1) - :param resourceGroups: A list of resource groups to define for the alert rule. - :param eventCategory: A list of event categories to define for the alert rule. - ("Compliance", "App", "Cloud", "Aws", "AzureActivityLog", "GcpAuditTrail", - "File", "Machine", "User") - :param severity: A list of alert severities to define for the alert rule. - (1, 2, 3, 4, 5) - :param intg_guid_list: A list of integration GUIDs representing the alert channels to use. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating alert rule in Lacework...") - - # Build the Alert Rules request URI - api_uri = f"/api/v2/AlertRules/{guid}" - - tmp_data = {} - - if type: - tmp_data["type"] = type - if filters: - tmp_data["filters"] = self._build_filters(filters) - if intg_guid_list: - tmp_data["intgGuidList"] = intg_guid_list - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, - guid, - org=False): - """ - A method to delete an alert rule. - - :param guid: A string representing the alert rule GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting alert rule in Lacework...") - - # Build the AlertRules request URI - api_uri = f"/api/v2/AlertRules/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() - - def _build_filters(self, - filters): - """ - A method to properly structure the filters object. - """ - - keys = filters.keys() - - response = {} - - if "name" in keys: - response["name"] = filters["name"] - if "description" in keys: - response["description"] = filters["description"] - if "enabled" in keys: - response["enabled"] = int(bool(filters["enabled"])) - if "resourceGroups" in keys: - response["resourceGroups"] = filters["resourceGroups"] - if "eventCategory" in keys: - response["eventCategory"] = filters["eventCategory"] - if "severity" in keys: - response["severity"] = filters["severity"] - - return response diff --git a/laceworksdk/api/audit_logs.py b/laceworksdk/api/audit_logs.py deleted file mode 100644 index 1d5bfce..0000000 --- a/laceworksdk/api/audit_logs.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Audit Logs API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class AuditLogsAPI(object): - """ - Lacework Audit Logs API. - """ - - def __init__(self, session): - """ - Initializes the AuditLogsAPI object. - - :param session: An instance of the HttpSession class - - :return AuditLogsAPI object. - """ - - super(AuditLogsAPI, self).__init__() - - self._session = session - - def get(self, - start_time=None, - end_time=None, - org=False): - """ - A method to get audit logs. - - :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. - :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting audit logs from Lacework...") - - # Build the Audit Logs request URI - api_uri = "/api/v2/AuditLogs" - - if start_time and end_time: - api_uri += f"?startTime={start_time}&endTime={end_time}" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def search(self, - query_data=None, - org=False): - """ - A method to search audit logs. - - :param query_data: A dictionary containing the necessary search parameters - (timeFilter, filters, returns) - - :return response json - """ - - logger.info("Searching audit logs from Lacework...") - - # Build the Audit Logs request URI - api_uri = "/api/v2/AuditLogs/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - if response.status_code == 204: - return {"data": []} - else: - return response.json() diff --git a/laceworksdk/api/cloud_accounts.py b/laceworksdk/api/cloud_accounts.py deleted file mode 100644 index 5ffb90a..0000000 --- a/laceworksdk/api/cloud_accounts.py +++ /dev/null @@ -1,210 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Cloud Accounts API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class CloudAccountsAPI(object): - """ - Lacework Cloud Accounts API. - """ - - def __init__(self, session): - """ - Initializes the CloudAccountsAPI object. - - :param session: An instance of the HttpSession class - - :return CloudAccountsAPI object. - """ - - super(CloudAccountsAPI, self).__init__() - - self._session = session - - def create(self, - name, - type, - enabled, - data, - org=False): - """ - A method to create a new cloud account. - - :param name: A string representing the cloud account name. - :param type: A string representing the cloud account type. - :param enabled: A boolean/integer representing whether the cloud account is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating cloud account in Lacework...") - - # Build the Cloud Accounts request URI - api_uri = "/api/v2/CloudAccounts" - - data = { - "name": name, - "type": type, - "enabled": int(bool(enabled)), - "data": data - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - guid=None, - type=None, - org=False): - """ - A method to get all cloud accounts. - - :param guid: A string representing the cloud account GUID. - :param type: A string representing the cloud account type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting cloud account info from Lacework...") - - # Build the Cloud Accounts request URI - if guid: - api_uri = f"/api/v2/CloudAccounts/{guid}" - elif type: - api_uri = f"/api/v2/CloudAccounts/{type}" - else: - api_uri = "/api/v2/CloudAccounts" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_type(self, - type, - org=False): - """ - A method to get all cloud accounts by type. - - :param type: A string representing the cloud account type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(type=type, org=org) - - def get_by_guid(self, - guid, - org=False): - """ - A method to get all cloud accounts. - - :param guid: A string representing the cloud account GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search cloud accounts. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching cloud accounts from Lacework...") - - # Build the Cloud Accounts request URI - api_uri = "/api/v2/CloudAccounts/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - return response.json() - - def update(self, - guid, - name=None, - type=None, - enabled=None, - data=None, - org=False): - """ - A method to update an cloud account. - - :param guid: A string representing the cloud account GUID. - :param name: A string representing the cloud account name. - :param type: A string representing the cloud account type. - :param enabled: A boolean/integer representing whether the cloud account is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating cloud account in Lacework...") - - # Build the Cloud Accounts request URI - api_uri = f"/api/v2/CloudAccounts/{guid}" - - tmp_data = {} - - if name: - tmp_data["name"] = name - if type: - tmp_data["type"] = type - if enabled is not None: - tmp_data["enabled"] = int(bool(enabled)) - if data: - tmp_data["data"] = data - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, - guid, - org=False): - """ - A method to delete an cloud account. - - :param guid: A string representing the cloud account GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting cloud account in Lacework...") - - # Build the Cloud Accounts request URI - api_uri = f"/api/v2/CloudAccounts/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() diff --git a/laceworksdk/api/cloud_activities.py b/laceworksdk/api/cloud_activities.py deleted file mode 100644 index f884c10..0000000 --- a/laceworksdk/api/cloud_activities.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework CloudActivities API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class CloudActivitiesAPI(object): - """ - Lacework CloudActivities API. - """ - - def __init__(self, session): - """ - Initializes the CloudActivitiesAPI object. - - :param session: An instance of the HttpSession class - - :return CloudActivitiesAPI object. - """ - - super(CloudActivitiesAPI, self).__init__() - - self._session = session - - def get(self, - start_time=None, - end_time=None, - org=False): - """ - A method to get CloudActivities details. - - :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. - :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting CloudActivities details from Lacework...") - - # Build the CloudActivities request URI - api_uri = "/api/v2/CloudActivities" - - if start_time and end_time: - api_uri += f"?startTime={start_time}&endTime={end_time}" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def search(self, - query_data=None, - org=False): - """ - A method to search CloudActivities details. - - :param query_data: A dictionary containing the necessary search parameters - (timeFilter, filters, returns) - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Searching CloudActivities details from Lacework...") - - # Build the CloudActivities request URI - api_uri = "/api/v2/CloudActivities/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - return response.json() diff --git a/laceworksdk/api/container_registries.py b/laceworksdk/api/container_registries.py deleted file mode 100644 index 76ded3c..0000000 --- a/laceworksdk/api/container_registries.py +++ /dev/null @@ -1,210 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Container Registries API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class ContainerRegistriesAPI(object): - """ - Lacework Container Registries API. - """ - - def __init__(self, session): - """ - Initializes the ContainerRegistriesAPI object. - - :param session: An instance of the HttpSession class - - :return ContainerRegistriesAPI object. - """ - - super(ContainerRegistriesAPI, self).__init__() - - self._session = session - - def create(self, - name, - type, - enabled, - data, - org=False): - """ - A method to create a new container registry. - - :param name: A string representing the container registry name. - :param type: A string representing the container registry type. - :param enabled: A boolean/integer representing whether the container registry is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating container registry in Lacework...") - - # Build the Container Registries request URI - api_uri = "/api/v2/ContainerRegistries" - - data = { - "name": name, - "type": type, - "enabled": int(bool(enabled)), - "data": data - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - guid=None, - type=None, - org=False): - """ - A method to get all container registries. - - :param guid: A string representing the container registry GUID. - :param type: A string representing the container registry type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting container registry info from Lacework...") - - # Build the Container Registries request URI - if guid: - api_uri = f"/api/v2/ContainerRegistries/{guid}" - elif type: - api_uri = f"/api/v2/ContainerRegistries/{type}" - else: - api_uri = "/api/v2/ContainerRegistries" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_type(self, - type, - org=False): - """ - A method to get all container registries by type. - - :param type: A string representing the container registry type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(type=type, org=org) - - def get_by_guid(self, - guid, - org=False): - """ - A method to get all container registries. - - :param guid: A string representing the container registry GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search container registries. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching container registries from Lacework...") - - # Build the Container Registries request URI - api_uri = "/api/v2/ContainerRegistries/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - return response.json() - - def update(self, - guid, - name=None, - type=None, - enabled=None, - data=None, - org=False): - """ - A method to update an container registry. - - :param guid: A string representing the container registry GUID. - :param name: A string representing the container registry name. - :param type: A string representing the container registry type. - :param enabled: A boolean/integer representing whether the container registry is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating container registry in Lacework...") - - # Build the Container Registries request URI - api_uri = f"/api/v2/ContainerRegistries/{guid}" - - tmp_data = {} - - if name: - tmp_data["name"] = name - if type: - tmp_data["type"] = type - if enabled is not None: - tmp_data["enabled"] = int(bool(enabled)) - if data: - tmp_data["data"] = data - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, - guid, - org=False): - """ - A method to delete an container registry. - - :param guid: A string representing the container registry GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting container registry in Lacework...") - - # Build the Container Registries request URI - api_uri = f"/api/v2/ContainerRegistries/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() diff --git a/laceworksdk/api/contract_info.py b/laceworksdk/api/contract_info.py deleted file mode 100644 index 7d197b7..0000000 --- a/laceworksdk/api/contract_info.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Contract Info API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class ContractInfoAPI(object): - """ - Lacework Contract Info API. - """ - - def __init__(self, session): - """ - Initializes the ContractInfoAPI object. - - :param session: An instance of the HttpSession class - - :return ContractInfoAPI object. - """ - - super(ContractInfoAPI, self).__init__() - - self._session = session - - def get(self, - start_time=None, - end_time=None, - org=False): - """ - A method to get contract info. - - :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. - :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting contract info from Lacework...") - - # Build the Contract Info request URI - api_uri = "/api/v2/ContractInfo" - - if start_time and end_time: - api_uri += f"?startTime={start_time}&endTime={end_time}" - - response = self._session.get(api_uri, org=org) - - return response.json() diff --git a/laceworksdk/api/datasources.py b/laceworksdk/api/datasources.py deleted file mode 100644 index c6c10e5..0000000 --- a/laceworksdk/api/datasources.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Datasources API wrapper. -""" - -import logging - -import bleach - -logger = logging.getLogger(__name__) - - -class DatasourcesAPI: - """ - Lacework Datasources API. - """ - - _DEFAULT_DESCRIPTION = "No description available." - - def __init__(self, session): - """ - Initializes the Datasources object. - - :param session: An instance of the HttpSession class - - :return DatasourcesAPI object. - """ - - super(DatasourcesAPI, self).__init__() - - self._session = session - - def get(self, - type=None, - org=False): - """ - A method to get datasources. - - :param type: A string representing the type of Datasource. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - # Build the Datasources request URI - if type: - type_clean = bleach.clean(type) - logger.info(f"Getting datasource info for '{type_clean}' from Lacework...") - api_uri = f"/api/v2/Datasources/{type_clean}" - else: - logger.info("Getting datasource info from Lacework...") - api_uri = "/api/v2/Datasources" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_type(self, - type, - org=False): - """ - A method to get a datasource by type. - - :param type: A string representing the type of Datasource. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(type=type, org=org) - - def get_datasource_schema(self, - data_source): - """ - A method to get the schema for a particular datasource. - - :param data_source: A string representing the datasource to check for. - - :return response json - """ - - return self.get(type=data_source) - - def list_data_sources(self): - """ - A method to list the datasources that are available. - - :return A list of tuples with two entries, source name and description. - """ - - logger.info("Getting list of data sources Lacework...") - - response_json = self.get() - - return_sources = [] - data_sources = response_json.get("data", []) - for data_source in data_sources: - description = data_source.get( - "description", self._DEFAULT_DESCRIPTION) - if description == 'None': - description = self._DEFAULT_DESCRIPTION - - return_sources.append( - (data_source.get("name", "No name"), description)) - - return return_sources diff --git a/laceworksdk/api/policies.py b/laceworksdk/api/policies.py deleted file mode 100644 index 2186cf3..0000000 --- a/laceworksdk/api/policies.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Policies API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class PoliciesAPI(object): - - def __init__(self, session): - """ - Initializes the PoliciesAPI object. - - :param session: An instance of the HttpSession class - - :return PoliciesAPI object. - """ - - super(PoliciesAPI, self).__init__() - - self._session = session - - def create(self, - policy_type, - query_id, - enabled, - title, - description, - remediation, - severity, - alert_enabled, - alert_profile, - evaluator_id=None, - limit=None, - eval_frequency=None, - org=False): - """ - A method to create a new Lacework Query Language (LQL) policy. - - :param policy_type: A string representing the policy type. - :param query_id: A string representing the LQL query ID. - :param enabled: A boolean representing whether the policy is enabled. - :param title: A string representing the policy title. - :param description: A string representing the policy description. - :param remediation: A string representing the remediation strategy for the policy. - :param severity: A string representing the policy severity. - ("info", "low", "medium", "high", "critical") - :param alert_enabled: A boolean representing whether alerting is enabled. - :param alert_profile: A string representing the alert profile. - :param evaluator_id: A string representing the evaluator in which the policy is to be run. - :param limit: An integer representing the number of results to return. - :param eval_frequency: A string representing the frequency in which to evaluate the policy. - ("Hourly", "Daily") - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating LQL policy in Lacework...") - - # Build the Policies request URI - api_uri = "/api/v2/Policies" - - data = { - "policyType": policy_type, - "queryId": query_id, - "title": title, - "enabled": int(bool(enabled)), - "description": description, - "remediation": remediation, - "severity": severity, - "alertEnabled": int(bool(alert_enabled)), - "alertProfile": alert_profile, - } - if evaluator_id: - data["evaluatorId"] = evaluator_id - - if isinstance(limit, int) and limit >= 0: - data["limit"] = limit - if eval_frequency: - data["evalFrequency"] = eval_frequency - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - policy_id=None, - org=False): - """ - A method to get LQL policies. - - :param policy_id: A string representing the LQL policy ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting LQL policy info from Lacework...") - - # Build the Policies request URI - if policy_id: - api_uri = f"/api/v2/Policies/{policy_id}" - else: - api_uri = "/api/v2/Policies" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_id(self, - policy_id, - org=False): - """ - A method to get an LQL policy by policy ID. - - :param policy_id: A string representing the LQL policy ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(policy_id=policy_id, org=org) - - def update(self, # noqa: C901 - policy_id, - policy_type=None, - query_id=None, - enabled=None, - title=None, - description=None, - remediation=None, - severity=None, - alert_enabled=None, - alert_profile=None, - limit=None, - eval_frequency=None, - org=False): - """ - A method to update a Lacework Query Language (LQL) policy. - - :param policy_id: A string representing the policy ID. - :param policy_type: A string representing the policy type. - :param query_id: A string representing the LQL query ID. - :param enabled: A boolean representing whether the policy is enabled. - :param title: A string representing the policy title. - :param description: A string representing the policy description. - :param remediation: A string representing the remediation strategy for the policy. - :param severity: A string representing the policy severity. - ("info", "low", "medium", "high", "critical") - :param alert_enabled: A boolean representing whether alerting is enabled. - :param alert_profile: A string representing the alert profile. - :param limit: An integer representing the number of results to return. - :param eval_frequency: A string representing the frequency in which to evaluate the policy. - ("Hourly", "Daily") - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating LQL policy in Lacework...") - - # Build the Policies request URI - api_uri = f"/api/v2/Policies/{policy_id}" - - data = {} - - if policy_type: - data["policyType"] = policy_type - if query_id: - data["queryId"] = query_id - if enabled is not None: - data["enabled"] = bool(enabled) - if title: - data["title"] = title - if description: - data["description"] = description - if remediation: - data["remediation"] = remediation - if severity: - data["severity"] = severity - if alert_enabled is not None: - data["alertEnabled"] = bool(alert_enabled) - if alert_profile: - data["alertProfile"] = alert_profile - if isinstance(limit, int) and limit >= 0: - data["limit"] = limit - if eval_frequency: - data["evalFrequency"] = eval_frequency - - response = self._session.patch(api_uri, org=org, data=data) - - return response.json() - - def delete(self, - policy_id, - org=False): - """ - A method to delete a Lacework Query Language (LQL) policy. - - :param policy_id: A string representing the LQL policy ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting LQL policy in Lacework...") - - # Build the Policies request URI - api_uri = f"/api/v2/Policies/{policy_id}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() diff --git a/laceworksdk/api/queries.py b/laceworksdk/api/queries.py deleted file mode 100644 index 89e6b89..0000000 --- a/laceworksdk/api/queries.py +++ /dev/null @@ -1,248 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Queries API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class QueriesAPI(object): - - def __init__(self, session): - """ - Initializes the QueriesAPI object. - - :param session: An instance of the HttpSession class - - :return QueriesAPI object. - """ - - super(QueriesAPI, self).__init__() - - self._session = session - - def create(self, - query_id, - query_text, - evaluator_id="", - org=False): - """ - A method to create a new Lacework Query Language (LQL) query. - - :param query_id: A string representing the LQL query ID. - :param query_text: A string representing the LQL query text. - :param evaluator_id: A string representing the evaluator in which the - policy is to be run. This is an optional parameter, with the - default behaviour of omitting the value while sending the API call. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating LQL query in Lacework...") - - # Build the Queries request URI - api_uri = "/api/v2/Queries" - - data = { - "queryId": query_id, - "queryText": query_text - } - if evaluator_id: - data["evaluatorId"] = evaluator_id - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - query_id=None, - org=False): - """ - A method to get LQL queries. - - :param query_id: A string representing the LQL query ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting LQL query info from Lacework...") - - # Build the Queries request URI - if query_id: - api_uri = f"/api/v2/Queries/{query_id}" - else: - api_uri = "/api/v2/Queries" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_id(self, - query_id, - org=False): - """ - A method to get an LQL query by query ID. - - :param query_id: A string representing the LQL query ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(query_id=query_id, org=org) - - def execute(self, - evaluator_id=None, - query_id=None, - query_text=None, - arguments={}, - org=False): - """ - A method to execute a Lacework Query Language (LQL) query. - - :param evaluator_id: A string representing the evaluator in which the policy is to be run. - :param query_id: A string representing the LQL query ID. - :param query_text: A string representing the LQL query text. - :param arguments: A dictionary of key/value pairs to be used as arguments in the LQL query. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Executing LQL query in Lacework...") - - data = { - "arguments": [] - } - - # Build the Queries request URI - if query_id: - api_uri = f"/api/v2/Queries/{query_id}/execute" - else: - api_uri = "/api/v2/Queries/execute" - - data["query"] = { - "queryText": query_text - } - if evaluator_id: - data["query"]["evaluatorId"] = evaluator_id - - for key, value in arguments.items(): - data["arguments"].append({ - "name": key, - "value": value - }) - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def execute_by_id(self, - query_id, - arguments={}, - org=False): - """ - A method to execute a Lacework Query Language (LQL) query. - - :param query_id: A string representing the LQL query ID. - :param arguments: A dictionary of key/value pairs to be used as arguments in the LQL query. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.execute(query_id=query_id, arguments=arguments, org=org) - - def validate(self, - query_text, - evaluator_id="", - org=False): - """ - A method to validate a Lacework Query Language (LQL) query. - - :param query_text: A string representing the LQL query text. - :param evaluator_id: A string representing the evaluator in which the - policy is to be run. Optional parameter, defaults to omitting - the evaluator from the validation request. - :param org: A boolean representing whether the request should be - performed at the Organization level - - :return response json - """ - - logger.info("Validating LQL query in Lacework...") - - # Build the Queries request URI - api_uri = "/api/v2/Queries/validate" - - data = { - "queryText": query_text - } - if evaluator_id: - data["evaluatorId"] = evaluator_id - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def update(self, - query_id, - query_text, - org=False): - """ - A method to update a Lacework Query Language (LQL) query. - - :param query_id: A string representing the LQL query ID. - :param query_text: A string representing the LQL query text. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating LQL query in Lacework...") - - # Build the Queries request URI - api_uri = f"/api/v2/Queries/{query_id}" - - data = { - "queryText": query_text - } - - response = self._session.patch(api_uri, org=org, data=data) - - return response.json() - - def delete(self, - query_id, - org=False): - """ - A method to delete a Lacework Query Language (LQL) query. - - :param query_id: A string representing the LQL query ID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting LQL query in Lacework...") - - # Build the Queries request URI - api_uri = f"/api/v2/Queries/{query_id}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() diff --git a/laceworksdk/api/report_rules.py b/laceworksdk/api/report_rules.py deleted file mode 100644 index be79b04..0000000 --- a/laceworksdk/api/report_rules.py +++ /dev/null @@ -1,246 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Report Rules API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class ReportRulesAPI(object): - """ - Lacework Report Rules API. - """ - - def __init__(self, session): - """ - Initializes the ReportRulesAPI object. - - :param session: An instance of the HttpSession class - - :return ReportRulesAPI object. - """ - - super(ReportRulesAPI, self).__init__() - - self._session = session - - def create(self, - type, - filters, - intg_guid_list, - report_notification_types, - org=False): - """ - A method to create a new report rule. - - :param type: A string representing the type of the report rule. - ('Report') - :param filters: A filter object for the report rule configuration. - obj: - :param name: A string representing the report rule name. - :param description: A string representing the report rule description. - :param enabled: A boolean/integer representing whether the report rule is enabled. - (0 or 1) - :param resourceGroups: A list of resource groups to define for the report rule. - :param severity: A list of alert severities to define for the report rule. - (1, 2, 3, 4, 5) - :param intg_guid_list: A list of integration GUIDs representing the report channels to use. - :param report_notification_types: An object of booleans for the types of reports that should be sent. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating report rule in Lacework...") - - # Build the Report Rules request URI - api_uri = "/api/v2/ReportRules" - - data = { - "type": type, - "filters": self._build_filters(filters), - "intgGuidList": intg_guid_list, - "reportNotificationTypes": self._build_report_notification_types(report_notification_types) - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - guid=None, - org=False): - """ - A method to get report rules. - - :param guid: A string representing the report rule GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting report rule info from Lacework...") - - # Build the Report Rules request URI - if guid: - api_uri = f"/api/v2/ReportRules/{guid}" - else: - api_uri = "/api/v2/ReportRules" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_guid(self, - guid, - org=False): - """ - A method to get an report rule by GUID. - - :param guid: A string representing the report rule GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search report rules. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching report rules from Lacework...") - - # Build the Report Rules request URI - api_uri = "/api/v2/ReportRules/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - if response.status_code == 204: - return {"data": []} - else: - return response.json() - - def update(self, - guid, - type=None, - filters=None, - intg_guid_list=None, - report_notification_types=None, - org=False): - """ - A method to update an report rule. - - :param guid: A string representing the report rule GUID. - :param type: A string representing the type of the report rule. - ('Report') - :param filters: A filter object for the report rule configuration. - obj: - :param name: A string representing the report rule name. - :param description: A string representing the report rule description. - :param enabled: A boolean/integer representing whether the report rule is enabled. - (0 or 1) - :param resourceGroups: A list of resource groups to define for the report rule. - :param severity: A list of alert severities to define for the report rule. - (1, 2, 3, 4, 5) - :param intg_guid_list: A list of integration GUIDs representing the report channels to use. - :param report_notification_types: An object of booleans for the types of reports that should be sent. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating report rule in Lacework...") - - # Build the Report Rules request URI - api_uri = f"/api/v2/ReportRules/{guid}" - - tmp_data = {} - - if type: - tmp_data["type"] = type - if filters: - tmp_data["filters"] = self._build_filters(filters) - if intg_guid_list: - tmp_data["intgGuidList"] = intg_guid_list - if report_notification_types: - tmp_data["reportNotificationTypes"] = self._build_report_notification_types(report_notification_types) - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, - guid, - org=False): - """ - A method to delete an report rule. - - :param guid: A string representing the report rule GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting report rule in Lacework...") - - # Build the ReportRules request URI - api_uri = f"/api/v2/ReportRules/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() - - def _build_filters(self, - filters): - """ - A method to properly structure the filters object. - """ - - keys = filters.keys() - - response = {} - - if "name" in keys: - response["name"] = filters["name"] - if "description" in keys: - response["description"] = filters["description"] - if "enabled" in keys: - response["enabled"] = int(bool(filters["enabled"])) - if "resourceGroups" in keys: - response["resourceGroups"] = filters["resourceGroups"] - if "severity" in keys: - response["severity"] = filters["severity"] - - return response - - def _build_report_notification_types(self, - report_notification_types): - """ - A method to properly structure the report notification types object. - """ - - response = {} - - for report_notification_type in report_notification_types.keys(): - response[report_notification_type] = report_notification_types[report_notification_type] - - return response diff --git a/laceworksdk/api/resource_groups.py b/laceworksdk/api/resource_groups.py deleted file mode 100644 index 7b732ad..0000000 --- a/laceworksdk/api/resource_groups.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Resource Groups API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class ResourceGroupsAPI(object): - """ - Lacework Resource Groups API. - """ - - def __init__(self, session): - """ - Initializes the ResourceGroupsAPI object. - - :param session: An instance of the HttpSession class - - :return ResourceGroupsAPI object. - """ - - super(ResourceGroupsAPI, self).__init__() - - self._session = session - - def create(self, - name, - type, - enabled, - props, - org=False): - """ - A method to create a new resource group. - - :param name: A string representing the resource group name. - :param type: A string representing the resource group type. - :param enabled: A boolean/integer representing whether the resource group is enabled. - (0 or 1) - :param props: A JSON object matching the schema for the specified resource group type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating resource group in Lacework...") - - # Build the Resource Groups request URI - api_uri = "/api/v2/ResourceGroups" - - data = { - "resourceName": name, - "resourceType": type, - "enabled": int(bool(enabled)), - "props": props - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, - guid=None, - org=False): - """ - A method to get all resource groups. - - :param guid: A string representing the resource group GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting resource group info from Lacework...") - - # Build the Resource Groups request URI - if guid: - api_uri = f"/api/v2/ResourceGroups/{guid}" - else: - api_uri = "/api/v2/ResourceGroups" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_guid(self, - guid, - org=False): - """ - A method to get all resource groups. - - :param guid: A string representing the resource group GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, - query_data=None, - org=False): - """ - A method to search resource groups. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching resource groups from Lacework...") - - # Build the Resource Groups request URI - api_uri = "/api/v2/ResourceGroups/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - return response.json() - - def update(self, - guid, - name=None, - type=None, - enabled=None, - props=None, - org=False): - """ - A method to update an resource group. - - :param guid: A string representing the resource group GUID. - :param name: A string representing the resource group name. - :param type: A string representing the resource group type. - :param enabled: A boolean/integer representing whether the resource group is enabled. - (0 or 1) - :param data: A JSON object matching the schema for the specified type. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating resource group in Lacework...") - - # Build the Resource Groups request URI - api_uri = f"/api/v2/ResourceGroups/{guid}" - - tmp_data = {} - - if name: - tmp_data["resourceName"] = name - if type: - tmp_data["resourceType"] = type - if enabled is not None: - tmp_data["enabled"] = int(bool(enabled)) - if props: - tmp_data["props"] = props - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, guid, org=False): - """ - A method to delete a resource group. - - :param guid: A string representing the resource group GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting resource group in Lacework...") - - # Build the Resource Groups request URI - api_uri = f"/api/v2/ResourceGroups/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() diff --git a/laceworksdk/api/team_members.py b/laceworksdk/api/team_members.py deleted file mode 100644 index a9d629e..0000000 --- a/laceworksdk/api/team_members.py +++ /dev/null @@ -1,225 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework Team Members API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class TeamMembersAPI(object): - """ - Lacework Team Members API. - """ - - def __init__(self, session): - """ - Initializes the TeamMembersAPI object. - - :param session: An instance of the HttpSession class - - :return TeamMembersAPI object. - """ - - super(TeamMembersAPI, self).__init__() - - self._session = session - - def create(self, - username, - props, - enabled, - org=False): - """ - A method to create a new team member. - - :param username: A string representing the email address of the user. - :param props: An object containing team member configuration - obj: - :param firstName: The first name of the team member. - :param lastName: The last name of the team member. - :param company: The company of the team member. - :param accountAdmin: A boolean representing if the team member is an account admin. - :param orgAdmin: A boolean representing if the team member is an organization admin. - :param orgUser: A boolean representing if the team member is an organization user. - :param adminRoleAccounts: A list of strings representing accounts where the team member is an admin. - :param userRoleAccounts: A list of strings representing accounts where the team member is a user. - :param enabled: A boolean/integer representing whether the team member is enabled. - (0 or 1) - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Creating team member in Lacework...") - - # Build the Team Members request URI - api_uri = "/api/v2/TeamMembers" - - data = { - "userName": username, - "props": self._build_props(props), - "userEnabled": int(bool(enabled)) - } - - response = self._session.post(api_uri, org=org, data=data) - - return response.json() - - def get(self, guid=None, org=False): - """ - A method to get team members. - - :param guid: A string representing the team member GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Getting team member info from Lacework...") - - # Build the Team Members request URI - if guid: - api_uri = f"/api/v2/TeamMembers/{guid}" - else: - api_uri = "/api/v2/TeamMembers" - - response = self._session.get(api_uri, org=org) - - return response.json() - - def get_by_guid(self, guid, org=False): - """ - A method to get an team member by GUID. - - :param guid: A string representing the team member GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - return self.get(guid=guid, org=org) - - def search(self, query_data=None, org=False): - """ - A method to search team members. - - :param query_data: A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - logger.info("Searching team members from Lacework...") - - # Build the Team Members request URI - api_uri = "/api/v2/TeamMembers/search" - - response = self._session.post(api_uri, data=query_data, org=org) - - return response.json() - - def update(self, - guid, - username=None, - props=None, - enabled=None, - org=False): - """ - A method to update a team member. - - :param guid: A string representing the team member GUID. - :param username: A string representing the email address of the user. - :param props: An object containing team member configuration - obj: - :param firstName: The first name of the team member. - :param lastName: The last name of the team member. - :param company: The company of the team member. - :param accountAdmin: A boolean representing if the team member is an account admin. - :param orgAdmin: A boolean representing if the team member is an organization admin. - :param orgUser: A boolean representing if the team member is an organization user. - :param adminRoleAccounts: A list of strings representing accounts where the team member is an admin. - :param userRoleAccounts: A list of strings representing accounts where the team member is a user. - :param enabled: A boolean/integer representing whether the team member is enabled. - (0 or 1) - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Updating team member in Lacework...") - - # Build the Team Members request URI - api_uri = f"/api/v2/TeamMembers/{guid}" - - tmp_data = {} - - if username: - tmp_data["userName"] = username - if props: - tmp_data["props"] = self._build_props(props) - if enabled is not None: - tmp_data["userEnabled"] = int(bool(enabled)) - - response = self._session.patch(api_uri, org=org, data=tmp_data) - - return response.json() - - def delete(self, - guid, - org=False): - """ - A method to delete an team member. - - :param guid: A string representing the team member GUID. - :param org: A boolean representing whether the request should be performed - at the Organization level - - :return response json - """ - - logger.info("Deleting team member in Lacework...") - - # Build the Team Members request URI - api_uri = f"/api/v2/TeamMembers/{guid}" - - response = self._session.delete(api_uri, org=org) - - if response.status_code == 204: - return response - else: - return response.json() - - def _build_props(self, - props): - """ - A method to properly structure the props object. - """ - - keys = props.keys() - - response = {} - - if "firstName" in keys: - response["firstName"] = props["firstName"] - if "lastName" in keys: - response["lastName"] = props["lastName"] - if "company" in keys: - response["company"] = props["company"] - if "accountAdmin" in keys: - response["accountAdmin"] = int(bool(props["accountAdmin"])) - if "orgAdmin" in keys: - response["orgAdmin"] = int(bool(props["orgAdmin"])) - if "orgUser" in keys: - response["orgUser"] = int(bool(props["orgUser"])) - if "adminRoleAccounts" in keys: - response["adminRoleAccounts"] = props["adminRoleAccounts"] - if "userRoleAccounts" in keys: - response["userRoleAccounts"] = props["userRoleAccounts"] - - return response diff --git a/laceworksdk/api/user_profile.py b/laceworksdk/api/user_profile.py deleted file mode 100644 index e2def6f..0000000 --- a/laceworksdk/api/user_profile.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Lacework User Profile API wrapper. -""" - -import logging - -logger = logging.getLogger(__name__) - - -class UserProfileAPI(object): - """ - Lacework User Profile API. - """ - - def __init__(self, session): - """ - Initializes the UserProfileAPI object. - - :param session: An instance of the HttpSession class - - :return UserProfileAPI object. - """ - - super(UserProfileAPI, self).__init__() - - self._session = session - - def get(self, - type=None, - subtype=None): - """ - A method to list all info in the user profile - - :return response json - """ - - logger.info("Fetching user profile info from Lacework...") - - # Build the User Profile request URI - api_uri = "/api/v2/UserProfile" - - response = self._session.get(api_uri) - - return response.json() diff --git a/laceworksdk/api/v1/__init__.py b/laceworksdk/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laceworksdk/api/account.py b/laceworksdk/api/v1/account.py similarity index 91% rename from laceworksdk/api/account.py rename to laceworksdk/api/v1/account.py index c51c918..b766e21 100644 --- a/laceworksdk/api/account.py +++ b/laceworksdk/api/v1/account.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class AccountAPI(object): +class AccountAPI: """ Lacework Account API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return AccountAPI object """ - super(AccountAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/compliance.py b/laceworksdk/api/v1/compliance.py similarity index 98% rename from laceworksdk/api/compliance.py rename to laceworksdk/api/v1/compliance.py index 2e42285..b97f679 100644 --- a/laceworksdk/api/compliance.py +++ b/laceworksdk/api/v1/compliance.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class ComplianceAPI(object): +class ComplianceAPI: """ Lacework Compliance API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return ComplianceAPI object. """ - super(ComplianceAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/custom_compliance_config.py b/laceworksdk/api/v1/custom_compliance_config.py similarity index 93% rename from laceworksdk/api/custom_compliance_config.py rename to laceworksdk/api/v1/custom_compliance_config.py index 60c39e5..a2cdeed 100644 --- a/laceworksdk/api/custom_compliance_config.py +++ b/laceworksdk/api/v1/custom_compliance_config.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class CustomComplianceConfigAPI(object): +class CustomComplianceConfigAPI: """ Lacework Custom Compliance Config API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return CustomComplianceConfigAPI object """ - super(CustomComplianceConfigAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/download_file.py b/laceworksdk/api/v1/download_file.py similarity index 92% rename from laceworksdk/api/download_file.py rename to laceworksdk/api/v1/download_file.py index 6bac8cf..1348a1c 100644 --- a/laceworksdk/api/download_file.py +++ b/laceworksdk/api/v1/download_file.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class DownloadFileAPI(object): +class DownloadFileAPI: """ Lacework Download File API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return DownloadFileAPI object. """ - super(DownloadFileAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/events.py b/laceworksdk/api/v1/events.py similarity index 97% rename from laceworksdk/api/events.py rename to laceworksdk/api/v1/events.py index 81e1386..3aacb10 100644 --- a/laceworksdk/api/events.py +++ b/laceworksdk/api/v1/events.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class EventsAPI(object): +class EventsAPI: """ Lacework Events API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return EventsAPI object """ - super(EventsAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/integrations.py b/laceworksdk/api/v1/integrations.py similarity index 98% rename from laceworksdk/api/integrations.py rename to laceworksdk/api/v1/integrations.py index 71afe93..8c7ef82 100644 --- a/laceworksdk/api/integrations.py +++ b/laceworksdk/api/v1/integrations.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class IntegrationsAPI(object): +class IntegrationsAPI: """ Lacework Integrations API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return IntegrationsAPI object. """ - super(IntegrationsAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/recommendations.py b/laceworksdk/api/v1/recommendations.py similarity index 95% rename from laceworksdk/api/recommendations.py rename to laceworksdk/api/v1/recommendations.py index b692145..a503c5a 100644 --- a/laceworksdk/api/recommendations.py +++ b/laceworksdk/api/v1/recommendations.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -class RecommendationsAPI(object): +class RecommendationsAPI: """ Lacework Recommendations API. """ @@ -21,7 +21,7 @@ def __init__(self, session): :return RecommendationsAPI object. """ - super(RecommendationsAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/run_reports.py b/laceworksdk/api/v1/run_reports.py similarity index 96% rename from laceworksdk/api/run_reports.py rename to laceworksdk/api/v1/run_reports.py index 68272da..bdcee5a 100644 --- a/laceworksdk/api/run_reports.py +++ b/laceworksdk/api/v1/run_reports.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -class RunReportsAPI(object): +class RunReportsAPI: """ Lacework RunReports API. """ @@ -21,7 +21,7 @@ def __init__(self, session): :return RunReportsAPI object. """ - super(RunReportsAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/suppressions.py b/laceworksdk/api/v1/suppressions.py similarity index 97% rename from laceworksdk/api/suppressions.py rename to laceworksdk/api/v1/suppressions.py index 134adf1..b8189ac 100644 --- a/laceworksdk/api/suppressions.py +++ b/laceworksdk/api/v1/suppressions.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -class SuppressionsAPI(object): +class SuppressionsAPI: """ Lacework Suppressions API. """ @@ -21,7 +21,7 @@ def __init__(self, session): :return SuppressionsAPI object. """ - super(SuppressionsAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/token.py b/laceworksdk/api/v1/token.py similarity index 98% rename from laceworksdk/api/token.py rename to laceworksdk/api/v1/token.py index 0837af4..a497f63 100644 --- a/laceworksdk/api/token.py +++ b/laceworksdk/api/v1/token.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class TokenAPI(object): +class TokenAPI: """ Lacework Agent Access Token API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return TokenAPI object. """ - super(TokenAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/vulnerability.py b/laceworksdk/api/v1/vulnerability.py similarity index 99% rename from laceworksdk/api/vulnerability.py rename to laceworksdk/api/v1/vulnerability.py index b104bbc..20713fb 100644 --- a/laceworksdk/api/vulnerability.py +++ b/laceworksdk/api/v1/vulnerability.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) -class VulnerabilityAPI(object): +class VulnerabilityAPI: """ Lacework Vulnerability API. """ @@ -22,7 +22,7 @@ def __init__(self, session): :return VulnerabilityAPI object. """ - super(VulnerabilityAPI, self).__init__() + super().__init__() self._session = session diff --git a/laceworksdk/api/v2/__init__.py b/laceworksdk/api/v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laceworksdk/api/v2/agent_access_tokens.py b/laceworksdk/api/v2/agent_access_tokens.py new file mode 100644 index 0000000..d9b329c --- /dev/null +++ b/laceworksdk/api/v2/agent_access_tokens.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +""" +Lacework AgentAccessTokens API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class AgentAccessTokensAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the AgentAccessTokensAPI object. + + :param session: An instance of the HttpSession class + + :return AgentAccessTokensAPI object. + """ + + super().__init__(session, "AgentAccessTokens") + + def create(self, + alias=None, + enabled=True, + **request_params): + """ + A method to create a new AgentAccessTokens object. + + :param alias: A string representing the object alias. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + token_alias=alias, + token_enabled=int(bool(enabled)), + **request_params + ) + + def get(self, + id=None): + """ + A method to get AgentAccessTokens objects. + + :param id: A string representing the object ID. + + :return response json + """ + + return super().get( + id=id + ) + + def get_by_id(self, + id): + """ + A method to get an AgentAccessTokens object by ID. + + :param id: A string representing the object ID. + + :return response json + """ + + return self.get(id=id) + + def search(self, + json=None, + query_data=None): + """ + A method to search AgentAccessTokens objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + id, + alias=None, + enabled=None, + **request_params): + """ + A method to update an AgentAccessTokens object. + + :param id: A string representing the object ID. + :param alias: A string representing the object alias. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=id, + token_alias=alias, + token_enabled=int(bool(enabled)), + **request_params + ) + + def delete(self): + pass diff --git a/laceworksdk/api/v2/alert_channels.py b/laceworksdk/api/v2/alert_channels.py new file mode 100644 index 0000000..e900c88 --- /dev/null +++ b/laceworksdk/api/v2/alert_channels.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +""" +Lacework AlertChannels API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class AlertChannelsAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the AlertChannelsAPI object. + + :param session: An instance of the HttpSession class + + :return AlertChannelsAPI object. + """ + + super().__init__(session, "AlertChannels") + + def create(self, + name, + type, + enabled, + data, + **request_params): + """ + A method to create a new AlertChannels object. + + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param data: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + name=name, + type=type, + enabled=int(bool(enabled)), + data=data, + **request_params + ) + + def get(self, + guid=None, + type=None): + """ + A method to get AlertChannels objects. + + :param guid: A string representing the object GUID. + :param type: A string representing the object type. + + :return response json + """ + + return super().get( + id=guid, + resource=type + ) + + def get_by_guid(self, + guid): + """ + A method to get AlertChannels objects by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def get_by_type(self, + type): + """ + A method to get AlertChannels objects by type. + + :param type: A string representing the object type. + + :return response json + """ + + return self.get(type=type) + + def search(self, + json=None, + query_data=None): + """ + A method to search AlertChannels objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + name=None, + type=None, + enabled=None, + data=None, + **request_params): + """ + A method to update an AlertChannels object. + + :param guid: A string representing the object GUID. + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param data: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + name=name, + type=type, + enabled=int(bool(enabled)), + data=data, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete an AlertChannels object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) + + def test(self, + guid): + """ + A method to test an AlertChannels object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + response = self._session.post(self.build_url(resource=guid, action="test")) + + return response diff --git a/laceworksdk/api/v2/alert_rules.py b/laceworksdk/api/v2/alert_rules.py new file mode 100644 index 0000000..d257400 --- /dev/null +++ b/laceworksdk/api/v2/alert_rules.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +""" +Lacework AlertRules API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class AlertRulesAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the AlertRulesAPI object. + + :param session: An instance of the HttpSession class + + :return AlertRulesAPI object. + """ + + super().__init__(session, "AlertRules") + + def create(self, + type, + filters, + intg_guid_list, + **request_params): + """ + A method to create a new AlertRules object. + + :param type: A string representing the type of the object. + ('Event') + :param filters: A filter object for the object configuration. + obj: + :param name: A string representing the object name. + :param description: A string representing the object description. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param resourceGroups: A list of resource groups to define for the object. + :param eventCategory: A list of event categories to define for the object. + ("Compliance", "App", "Cloud", "File", "Machine", "User", "Platform", "K8sActivity") + :param severity: A list of alert severities to define for the object. + (1, 2, 3, 4, 5) + :param intg_guid_list: A list of integration GUIDs representing the alert channels to use. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + type=type, + filters=self._format_filters(filters), + intg_guid_list=intg_guid_list, + **request_params + ) + + def get(self, + guid=None): + """ + A method to get AlertRules objects. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().get(id=guid) + + def get_by_guid(self, + guid): + """ + A method to get an AlertRules object by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def search(self, + json=None, + query_data=None): + """ + A method to search AlertRules objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + type=None, + filters=None, + intg_guid_list=None, + **request_params): + """ + A method to update an AlertRules object. + + :param guid: A string representing the object GUID. + :param type: A string representing the type of the object. + ('Event') + :param filters: A filter object for the object configuration. + obj: + :param name: A string representing the object name. + :param description: A string representing the object description. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param resourceGroups: A list of resource groups to define for the object. + :param eventCategory: A list of event categories to define for the object. + ("Compliance", "App", "Cloud", "File", "Machine", "User", "Platform", "K8sActivity") + :param severity: A list of alert severities to define for the object. + (1, 2, 3, 4, 5) + :param intg_guid_list: A list of integration GUIDs representing the alert channels to use. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + type=type, + filters=self._format_filters(filters), + intg_guid_list=intg_guid_list, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete an AlertRules object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) diff --git a/laceworksdk/api/v2/audit_logs.py b/laceworksdk/api/v2/audit_logs.py new file mode 100644 index 0000000..cf7bda1 --- /dev/null +++ b/laceworksdk/api/v2/audit_logs.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +""" +Lacework AuditLogs API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class AuditLogsAPI(BaseEndpoint): + + def __init__(self, session): + """ + Initializes the AuditLogsAPI object. + + :param session: An instance of the HttpSession class + + :return AuditLogsAPI object. + """ + + super().__init__(session, "AuditLogs") + + def get(self, + start_time=None, + end_time=None, + **request_params): + """ + A method to get AuditLogs objects. + + :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. + :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + params = self.build_dict_from_items( + request_params, + start_time=start_time, + end_time=end_time + ) + + response = self._session.get(self.build_url(), params=params) + + return response.json() + + def search(self, + json=None, + query_data=None): + """ + A method to search AuditLogs objects. + + :param json: A dictionary containing the necessary search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + # TODO: Remove this on v1.0 release - provided for back compat + if query_data: + json = query_data + + response = self._session.post(self.build_url(action="search"), json=json) + + return response.json() diff --git a/laceworksdk/api/v2/cloud_accounts.py b/laceworksdk/api/v2/cloud_accounts.py new file mode 100644 index 0000000..8d50621 --- /dev/null +++ b/laceworksdk/api/v2/cloud_accounts.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +""" +Lacework CloudAccounts API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class CloudAccountsAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the CloudAccountsAPI object. + + :param session: An instance of the HttpSession class + + :return CloudAccountsAPI object. + """ + + super().__init__(session, "CloudAccounts") + + def create(self, + name, + type, + enabled, + data, + **request_params): + """ + A method to create a new CloudAccounts object. + + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param data: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + name=name, + type=type, + enabled=int(bool(enabled)), + data=data, + **request_params + ) + + def get(self, + guid=None, + type=None): + """ + A method to get CloudAccounts objects. + + :param guid: A string representing the object GUID. + :param type: A string representing the object type. + + :return response json + """ + + return super().get(id=guid, resource=type) + + def get_by_guid(self, + guid): + """ + A method to get CloudAccounts objects by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def get_by_type(self, + type): + """ + A method to get CloudAccounts objects by type. + + :param type: A string representing the object type. + + :return response json + """ + + return self.get(type=type) + + def search(self, + json=None, + query_data=None): + """ + A method to search CloudAccounts objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + name=None, + type=None, + enabled=None, + data=None, + **request_params): + """ + A method to update an CloudAccounts object. + + :param guid: A string representing the object GUID. + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param data: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + name=name, + type=type, + enabled=int(bool(enabled)), + data=data, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete an CloudAccounts object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) diff --git a/laceworksdk/api/v2/cloud_activities.py b/laceworksdk/api/v2/cloud_activities.py new file mode 100644 index 0000000..f54a16e --- /dev/null +++ b/laceworksdk/api/v2/cloud_activities.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +""" +Lacework CloudActivities API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class CloudActivitiesAPI(BaseEndpoint): + + def __init__(self, session): + """ + Initializes the CloudActivitiesAPI object. + + :param session: An instance of the HttpSession class + + :return CloudActivitiesAPI object. + """ + + super().__init__(session, "CloudActivities") + + def get(self, + start_time=None, + end_time=None, + **request_params): + """ + A method to get CloudActivities objects. + + :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. + :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + params = self.build_dict_from_items( + request_params, + start_time=start_time, + end_time=end_time + ) + + response = self._session.get(self.build_url(), params=params) + + return response.json() + + def get_pages(self, + **request_params): + """ + A method to get pages of objects objects. + + :param request_params: request parameters. + + :return a generator which yields pages of CloudActivities objects returned by the Lacework API. + """ + + params = self.build_dict_from_items( + request_params + ) + + for page in self._session.get_pages(self.build_url(), params=params): + yield page.json() + + def get_data_items(self, + **request_params): + """ + A method to get data items. + + :param request_params: request parameters. + + :return a generator which yields individual CloudActivities objects returned by the Lacework API. + """ + + params = self.build_dict_from_items( + request_params + ) + + for item in self._session.get_data_items(self.build_url(), params=params): + yield item + + def search(self, + json=None, + query_data=None): + """ + A method to search CloudActivities objects. + + :param json: A dictionary containing the necessary search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + # TODO: Remove this on v1.0 release - provided for back compat + if query_data: + json = query_data + + response = self._session.post(self.build_url(action="search"), json=json) + + return response.json() diff --git a/laceworksdk/api/v2/container_registries.py b/laceworksdk/api/v2/container_registries.py new file mode 100644 index 0000000..646697d --- /dev/null +++ b/laceworksdk/api/v2/container_registries.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +""" +Lacework ContainerRegistries API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class ContainerRegistriesAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the ContainerRegistriesAPI object. + + :param session: An instance of the HttpSession class + + :return ContainerRegistriesAPI object. + """ + + super().__init__(session, "ContainerRegistries") + + def create(self, + name, + type, + enabled, + data, + **request_params): + """ + A method to create a new ContainerRegistries object. + + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param data: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + name=name, + type=type, + enabled=int(bool(enabled)), + data=data, + **request_params + ) + + def get(self, + guid=None, + type=None): + """ + A method to get ContainerRegistries objects. + + :param guid: A string representing the object GUID. + :param type: A string representing the object type. + + :return response json + """ + + return super().get(id=guid, resource=type) + + def get_by_guid(self, + guid): + """ + A method to get ContainerRegistries objects by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def get_by_type(self, + type): + """ + A method to get ContainerRegistries objects by type. + + :param type: A string representing the object type. + + :return response json + """ + + return self.get(type=type) + + def search(self, + json=None, + query_data=None): + """ + A method to search ContainerRegistries objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + name=None, + type=None, + enabled=None, + data=None, + **request_params): + """ + A method to update an ContainerRegistries object. + + :param guid: A string representing the object GUID. + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param data: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + name=name, + type=type, + enabled=int(bool(enabled)), + data=data, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete a ContainerRegistries object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) diff --git a/laceworksdk/api/v2/contract_info.py b/laceworksdk/api/v2/contract_info.py new file mode 100644 index 0000000..c5e5e02 --- /dev/null +++ b/laceworksdk/api/v2/contract_info.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +""" +Lacework ContractInfo API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class ContractInfoAPI(BaseEndpoint): + + def __init__(self, session): + """ + Initializes the ContractInfoAPI object. + + :param session: An instance of the HttpSession class + + :return ContractInfoAPI object. + """ + + super().__init__(session, "ContractInfo") + + def get(self, + start_time=None, + end_time=None, + **request_params): + """ + A method to get ContractInfo objects. + + :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. + :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + params = self.build_dict_from_items( + request_params, + start_time=start_time, + end_time=end_time + ) + + response = self._session.get(self.build_url(), params=params) + + return response.json() diff --git a/laceworksdk/api/v2/datasources.py b/laceworksdk/api/v2/datasources.py new file mode 100644 index 0000000..b4c5c98 --- /dev/null +++ b/laceworksdk/api/v2/datasources.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" +Lacework Datasources API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class DatasourcesAPI(BaseEndpoint): + + _DEFAULT_DESCRIPTION = "No description available." + + def __init__(self, session): + """ + Initializes the Datasources object. + + :param session: An instance of the HttpSession class + + :return DatasourcesAPI object. + """ + + super().__init__(session, "Datasources") + + def get(self, + type=None): + """ + A method to get Datasources objects. + + :param type: A string representing the object type. + + :return response json + """ + + response = self._session.get(self.build_url(resource=type)) + + return response.json() + + def get_by_type(self, + type): + """ + A method to get a Datasources object by type. + + :param type: A string representing the object type. + + :return response json + """ + + return self.get(type=type) + + def get_datasource(self, + datasource): + """ + A method to get the schema for a particular datasource. + + :param datasource: A string representing the datasource to check for. + + :return response json + """ + + return self.get(type=datasource) + + def list_data_sources(self): + """ + A method to list the datasources that are available. + + :return A list of tuples with two entries, source name and description. + """ + + response_json = self.get() + + return_sources = [] + data_sources = response_json.get("data", []) + for data_source in data_sources: + description = data_source.get( + "description", self._DEFAULT_DESCRIPTION) + if description == "None": + description = self._DEFAULT_DESCRIPTION + + return_sources.append( + (data_source.get("name", "No name"), description)) + + return return_sources diff --git a/laceworksdk/api/v2/policies.py b/laceworksdk/api/v2/policies.py new file mode 100644 index 0000000..be6f241 --- /dev/null +++ b/laceworksdk/api/v2/policies.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +""" +Lacework Policies API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class PoliciesAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the PoliciesAPI object. + + :param session: An instance of the HttpSession class + + :return PoliciesAPI object. + """ + + super().__init__(session, "Policies") + + def create(self, + policy_type, + query_id, + enabled, + title, + description, + remediation, + severity, + alert_enabled, + alert_profile, + evaluator_id=None, + limit=None, + eval_frequency=None, + **request_params): + """ + A method to create a new Policies object. + + :param policy_type: A string representing the object policy type. + :param query_id: A string representing the object query ID. + :param enabled: A boolean representing whether the object is enabled. + :param title: A string representing the object title. + :param description: A string representing the object description. + :param remediation: A string representing the remediation strategy for the object. + :param severity: A string representing the object severity. + ("info", "low", "medium", "high", "critical") + :param alert_enabled: A boolean representing whether alerting is enabled. + :param alert_profile: A string representing the alert profile. + :param evaluator_id: A string representing the evaluator in which the object is to be run. + :param limit: An integer representing the number of results to return. + :param eval_frequency: A string representing the frequency in which to evaluate the object. + ("Hourly", "Daily") + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + policy_type=policy_type, + query_id=query_id, + enabled=int(bool(enabled)), + title=title, + description=description, + remediation=remediation, + severity=severity, + alert_enabled=alert_enabled, + alert_profile=alert_profile, + evaluator_id=evaluator_id, + limit=limit, + eval_frequency=eval_frequency, + **request_params + ) + + def get(self, + policy_id=None): + """ + A method to get Policies objects. + + :param policy_id: A string representing the object policy ID. + + :return response json + """ + + return super().get(id=policy_id) + + def get_by_id(self, + policy_id): + """ + A method to get a Policies object by policy ID. + + :param policy_id: A string representing the object policy ID. + + :return response json + """ + + return self.get(policy_id=policy_id) + + def update(self, # noqa: C901 + policy_id, + policy_type=None, + query_id=None, + enabled=None, + title=None, + description=None, + remediation=None, + severity=None, + alert_enabled=None, + alert_profile=None, + limit=None, + eval_frequency=None, + **request_params): + """ + A method to update a Lacework Query Language (LQL) policy. + + :param policy_id: A string representing the object policy ID. + :param policy_type: A string representing the object policy type. + :param query_id: A string representing the object query ID. + :param enabled: A boolean representing whether the object is enabled. + :param title: A string representing the object title. + :param description: A string representing the object description. + :param remediation: A string representing the remediation strategy for the object. + :param severity: A string representing the object severity. + ("info", "low", "medium", "high", "critical") + :param alert_enabled: A boolean representing whether alerting is enabled. + :param alert_profile: A string representing the alert profile. + :param limit: An integer representing the number of results to return. + :param eval_frequency: A string representing the frequency in which to evaluate the object. + ("Hourly", "Daily") + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=policy_id, + policy_type=policy_type, + query_id=query_id, + enabled=bool(enabled), + title=title, + description=description, + remediation=remediation, + severity=severity, + alert_enabled=bool(alert_enabled), + alert_profile=alert_profile, + limit=limit, + eval_frequency=eval_frequency, + **request_params + ) + + def delete(self, + policy_id): + """ + A method to delete a Policies object. + + :param policy_id: A string representing the object policy ID. + + :return response json + """ + + return super().delete(id=policy_id) diff --git a/laceworksdk/api/v2/queries.py b/laceworksdk/api/v2/queries.py new file mode 100644 index 0000000..a6339df --- /dev/null +++ b/laceworksdk/api/v2/queries.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +""" +Lacework Queries API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class QueriesAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the QueriesAPI object. + + :param session: An instance of the HttpSession class + + :return QueriesAPI object. + """ + + super().__init__(session, "Queries") + + def create(self, + query_id, + query_text, + evaluator_id=None, + **request_params): + """ + A method to create a new Queries object. + + :param query_id: A string representing the object query ID. + :param query_text: A string representing the object query text. + :param evaluator_id: A string representing the evaluator in which the + query is to be run. This is an optional parameter, with the + default behaviour of omitting the value while sending the API call. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + query_id=query_id, + query_text=query_text, + evaluator_id=evaluator_id, + **request_params + ) + + def get(self, + query_id=None): + """ + A method to get Queries objects. + + :param query_id: A string representing the object query ID. + + :return response json + """ + + return super().get(id=query_id) + + def get_by_id(self, + query_id): + """ + A method to get a Queries object by query ID. + + :param query_id: A string representing the object query ID. + + :return response json + """ + + return self.get(query_id=query_id) + + def execute(self, + evaluator_id=None, + query_id=None, + query_text=None, + arguments={}): + """ + A method to execute a Queries object. + + :param evaluator_id: A string representing the evaluator in which the query object is to be run. + :param query_id: A string representing the object query ID. + :param query_text: A string representing the object query text. + :param arguments: A dictionary of key/value pairs to be used as arguments in the query object. + + :return response json + """ + + json = { + "arguments": [] + } + + # Build the Queries request URI + if query_id is None: + json["query"] = { + "queryText": query_text + } + if evaluator_id: + json["query"]["evaluatorId"] = evaluator_id + + for key, value in arguments.items(): + json["arguments"].append({ + "name": key, + "value": value + }) + + response = self._session.post(self.build_url(action="execute")) + + return response.json() + + def execute_by_id(self, + query_id, + arguments={}): + """ + A method to execute a Queries object by query ID. + + :param query_id: A string representing the object query ID. + :param arguments: A dictionary of key/value pairs to be used as arguments in the query object. + + :return response json + """ + + json = { + "arguments": [] + } + + for key, value in arguments.items(): + json["arguments"].append({ + "name": key, + "value": value + }) + + response = self._session.post(self.build_url(action="execute", id=query_id), json=json) + + return response.json() + + def validate(self, + query_text, + evaluator_id=None, + **request_params): + """ + A method to validate a Queries object. + + :param query_text: A string representing the object query text. + :param evaluator_id: A string representing the evaluator in which the + query is to be run. Optional parameter, defaults to omitting + the evaluator from the validation request. + + :return response json + """ + + json = self.build_dict_from_items( + request_params, + query_text=query_text, + evaluator_id=evaluator_id + ) + + response = self._session.post(self.build_url(action="validate"), json=json) + + return response.json() + + def update(self, + query_id, + query_text, + **request_params): + """ + A method to update a Queries object. + + :param query_id: A string representing the object query ID. + :param query_text: A string representing the object query text. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=query_id, + query_text=query_text, + **request_params + ) + + def delete(self, + query_id): + """ + A method to delete a Queries object. + + :param query_id: A string representing the object query ID. + + :return response json + """ + + return super().delete(id=query_id) diff --git a/laceworksdk/api/v2/report_rules.py b/laceworksdk/api/v2/report_rules.py new file mode 100644 index 0000000..97f949a --- /dev/null +++ b/laceworksdk/api/v2/report_rules.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +""" +Lacework ReportRules API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class ReportRulesAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the ReportRulesAPI object. + + :param session: An instance of the HttpSession class + + :return ReportRulesAPI object. + """ + + super().__init__(session, "ReportRules") + + def create(self, + type, + filters, + intg_guid_list, + report_notification_types, + **request_params): + """ + A method to create a new ReportRules object. + + :param type: A string representing the type of the object. + ('Report') + :param filters: A filter object for the object configuration. + obj: + :param name: A string representing the object name. + :param description: A string representing the object description. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param resourceGroups: A list of resource groups to define for the object. + :param severity: A list of alert severities to define for the object. + (1, 2, 3, 4, 5) + :param intg_guid_list: A list of integration GUIDs representing the report channels to use. + :param report_notification_types: An object of booleans for the types of reports that should be sent. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + type=type, + filters=self._format_filters(filters), + intg_guid_list=intg_guid_list, + report_notification_types=report_notification_types, + **request_params + ) + + def get(self, + guid=None): + """ + A method to get ReportRules objects. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().get(id=guid) + + def get_by_guid(self, + guid): + """ + A method to get a ReportRules object by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def search(self, + json=None, + query_data=None): + """ + A method to search ReportRules objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + type=None, + filters=None, + intg_guid_list=None, + report_notification_types=None, + **request_params): + """ + A method to update a ReportRules object. + + :param guid: A string representing the object GUID. + :param type: A string representing the type of the object. + ('Report') + :param filters: A filter object for the object configuration. + obj: + :param name: A string representing the object name. + :param description: A string representing the object description. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param resourceGroups: A list of resource groups to define for the object. + :param severity: A list of alert severities to define for the object. + (1, 2, 3, 4, 5) + :param intg_guid_list: A list of integration GUIDs representing the report channels to use. + :param report_notification_types: An object of booleans for the types of reports that should be sent. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + type=type, + filters=self._format_filters(filters), + intg_guid_list=intg_guid_list, + report_notification_types=report_notification_types, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete a ReportRules object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) diff --git a/laceworksdk/api/v2/resource_groups.py b/laceworksdk/api/v2/resource_groups.py new file mode 100644 index 0000000..57971d8 --- /dev/null +++ b/laceworksdk/api/v2/resource_groups.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" +Lacework ResourceGroups API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class ResourceGroupsAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the ResourceGroupsAPI object. + + :param session: An instance of the HttpSession class + + :return ResourceGroupsAPI object. + """ + + super().__init__(session, "ResourceGroups") + + def create(self, + name, + type, + enabled, + props, + **request_params): + """ + A method to create a new ResourceGroups object. + + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param props: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + resource_name=name, + resource_type=type, + enabled=int(bool(enabled)), + props=props, + **request_params + ) + + def get(self, + guid=None): + """ + A method to get ResourceGroups objects. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().get(id=guid) + + def get_by_guid(self, + guid): + """ + A method to get ResourceGroups objects by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def search(self, + json=None, + query_data=None): + """ + A method to search ResourceGroups objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + name=None, + type=None, + enabled=None, + props=None, + **request_params): + """ + A method to update an ResourceGroups object. + + :param guid: A string representing the object GUID. + :param name: A string representing the object name. + :param type: A string representing the object type. + :param enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param props: A JSON object matching the schema for the specified type. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + resource_name=name, + resource_type=type, + enabled=int(bool(enabled)), + props=props, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete a ResourceGroups object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) diff --git a/laceworksdk/api/schemas.py b/laceworksdk/api/v2/schemas.py similarity index 55% rename from laceworksdk/api/schemas.py rename to laceworksdk/api/v2/schemas.py index e91f218..da733b8 100644 --- a/laceworksdk/api/schemas.py +++ b/laceworksdk/api/v2/schemas.py @@ -3,12 +3,10 @@ Lacework Schemas API wrapper. """ -import logging +from laceworksdk.api.base_endpoint import BaseEndpoint -logger = logging.getLogger(__name__) - -class SchemasAPI(object): +class SchemasAPI(BaseEndpoint): """ Lacework Schemas API. """ @@ -22,30 +20,21 @@ def __init__(self, session): :return SchemasAPI object. """ - super(SchemasAPI, self).__init__() - - self._session = session + super().__init__(session, "schemas") def get(self, type=None, subtype=None): """ - A method to list all schema types, or fetch a specific schema + A method to get schema objects. + + :param guid: A string representing the object type. + :param type: A string representing the object subtype. :return response json """ - logger.info("Fetching schema info from Lacework...") - - # Build the Schema request URI - if type and subtype: - api_uri = f"/api/v2/schemas/{type}/{subtype}" - elif type: - api_uri = f"/api/v2/schemas/{type}" - else: - api_uri = "/api/v2/schemas" - - response = self._session.get(api_uri) + response = self._session.get(self.build_url(id=subtype, resource=type)) return response.json() diff --git a/laceworksdk/api/v2/team_members.py b/laceworksdk/api/v2/team_members.py new file mode 100644 index 0000000..66240f2 --- /dev/null +++ b/laceworksdk/api/v2/team_members.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +""" +Lacework TeamMembers API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class TeamMembersAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the TeamMembersAPI object. + + :param session: An instance of the HttpSession class + + :return TeamMembersAPI object. + """ + + super().__init__(session, "TeamMembers") + + def create(self, + user_name, + user_enabled, + props, + org_admin=None, + org_user=None, + admin_role_accounts=None, + user_role_accounts=None, + **request_params): + """ + A method to create a new TeamMembers object. + + :param user_name: A string representing the email address of the user. + :param user_enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param props: An object containing object configuration + obj: + :param firstName: The first name of the object. + :param lastName: The last name of the team m. + :param company: The company of the object. + :param accountAdmin: A boolean representing if the object is an account admin. + :param org_admin: A boolean representing if the object is an organization admin. + (Organization-level Access Required) + :param org_user: A boolean representing if the object is an organization user. + (Organization-level Access Required) + :param admin_role_accounts: A list of strings representing accounts where the object is an admin. + (Organization-level Access Required) + :param user_role_accounts: A list of strings representing accounts where the object is a user. + (Organization-level Access Required) + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + user_name=user_name, + user_enabled=int(bool(user_enabled)), + props=props, + org_admin=org_admin, + org_user=org_user, + admin_role_accounts=admin_role_accounts, + user_role_accounts=user_role_accounts, + **request_params + ) + + def get(self, guid=None): + """ + A method to get TeamMembers objects. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().get(id=guid) + + def get_by_guid(self, guid): + """ + A method to get a TeamMembers object by GUID. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return self.get(guid=guid) + + def search(self, + json=None, + query_data=None): + """ + A method to search TeamMembers objects. + + :param json: A dictionary containing the desired search parameters. + (filters, returns) + + :return response json + """ + + return super().search(json=json, query_data=query_data) + + def update(self, + guid, + user_name=None, + user_enabled=None, + props=None, + org_admin=None, + org_user=None, + admin_role_accounts=None, + user_role_accounts=None, + **request_params): + """ + A method to update a TeamMembers object. + + :param guid: A string representing the object GUID. + :param user_name: A string representing the email address of the object. + :param user_enabled: A boolean/integer representing whether the object is enabled. + (0 or 1) + :param props: An object containing object configuration + obj: + :param firstName: The first name of the object. + :param lastName: The last name of the team m. + :param company: The company of the object. + :param accountAdmin: A boolean representing if the object is an account admin. + :param org_admin: A boolean representing if the object is an organization admin. + (Organization-level Access Required) + :param org_user: A boolean representing if the object is an organization user. + (Organization-level Access Required) + :param admin_role_accounts: A list of strings representing accounts where the object is an admin. + (Organization-level Access Required) + :param user_role_accounts: A list of strings representing accounts where the object is a user. + (Organization-level Access Required) + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=guid, + user_name=user_name, + user_enabled=int(bool(user_enabled)), + props=props, + org_admin=org_admin, + org_user=org_user, + admin_role_accounts=admin_role_accounts, + user_role_accounts=user_role_accounts, + **request_params + ) + + def delete(self, + guid): + """ + A method to delete a TeamMembers object. + + :param guid: A string representing the object GUID. + + :return response json + """ + + return super().delete(id=guid) diff --git a/laceworksdk/api/v2/user_profile.py b/laceworksdk/api/v2/user_profile.py new file mode 100644 index 0000000..cfb9ff6 --- /dev/null +++ b/laceworksdk/api/v2/user_profile.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Lacework UserProfile API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class UserProfileAPI(BaseEndpoint): + + def __init__(self, session): + """ + Initializes the UserProfileAPI object. + + :param session: An instance of the HttpSession class + + :return UserProfileAPI object. + """ + + super().__init__(session, "UserProfile") + + def get(self): + """ + A method to get UserProfile object. + + :return response json + """ + + response = self._session.get(self.build_url()) + + return response.json() diff --git a/tests/api/v1/test_account.py b/tests/api/v1/test_account.py index 9dbf284..b8a7427 100644 --- a/tests/api/v1/test_account.py +++ b/tests/api/v1/test_account.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from laceworksdk.api.account import AccountAPI +from laceworksdk.api.v1.account import AccountAPI # Tests diff --git a/tests/api/v1/test_compliance.py b/tests/api/v1/test_compliance.py index a0d5f40..8947470 100644 --- a/tests/api/v1/test_compliance.py +++ b/tests/api/v1/test_compliance.py @@ -9,7 +9,7 @@ import pytest from dotenv import load_dotenv -from laceworksdk.api.compliance import ComplianceAPI +from laceworksdk.api.v1.compliance import ComplianceAPI load_dotenv() diff --git a/tests/api/v1/test_custom_compliance_config.py b/tests/api/v1/test_custom_compliance_config.py index cc5764c..3f16040 100644 --- a/tests/api/v1/test_custom_compliance_config.py +++ b/tests/api/v1/test_custom_compliance_config.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from laceworksdk.api.custom_compliance_config import CustomComplianceConfigAPI +from laceworksdk.api.v1.custom_compliance_config import CustomComplianceConfigAPI # Tests diff --git a/tests/api/v1/test_download_file.py b/tests/api/v1/test_download_file.py index a9a1b24..54297a0 100644 --- a/tests/api/v1/test_download_file.py +++ b/tests/api/v1/test_download_file.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from laceworksdk.api.download_file import DownloadFileAPI +from laceworksdk.api.v1.download_file import DownloadFileAPI # Tests diff --git a/tests/api/v1/test_events.py b/tests/api/v1/test_events.py index 97cb6d7..7da1989 100644 --- a/tests/api/v1/test_events.py +++ b/tests/api/v1/test_events.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta, timezone -from laceworksdk.api.events import EventsAPI +from laceworksdk.api.v1.events import EventsAPI # Build start/end times diff --git a/tests/api/v1/test_integrations.py b/tests/api/v1/test_integrations.py index 03ed9f5..8290acf 100644 --- a/tests/api/v1/test_integrations.py +++ b/tests/api/v1/test_integrations.py @@ -8,7 +8,7 @@ import pytest -from laceworksdk.api.integrations import IntegrationsAPI +from laceworksdk.api.v1.integrations import IntegrationsAPI INTEGRATION_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v1/test_recommendations.py b/tests/api/v1/test_recommendations.py index c3a15e7..5794180 100644 --- a/tests/api/v1/test_recommendations.py +++ b/tests/api/v1/test_recommendations.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from laceworksdk.api.recommendations import RecommendationsAPI +from laceworksdk.api.v1.recommendations import RecommendationsAPI # Tests diff --git a/tests/api/v1/test_run_reports.py b/tests/api/v1/test_run_reports.py index 3b46008..946d824 100644 --- a/tests/api/v1/test_run_reports.py +++ b/tests/api/v1/test_run_reports.py @@ -5,7 +5,7 @@ import pytest -from laceworksdk.api.run_reports import RunReportsAPI +from laceworksdk.api.v1.run_reports import RunReportsAPI # Tests diff --git a/tests/api/v1/test_suppressions.py b/tests/api/v1/test_suppressions.py index 9fcbae6..40eaf4c 100644 --- a/tests/api/v1/test_suppressions.py +++ b/tests/api/v1/test_suppressions.py @@ -6,7 +6,7 @@ import random import string -from laceworksdk.api.suppressions import SuppressionsAPI +from laceworksdk.api.v1.suppressions import SuppressionsAPI RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v1/test_token.py b/tests/api/v1/test_token.py index c82894f..dc11a47 100644 --- a/tests/api/v1/test_token.py +++ b/tests/api/v1/test_token.py @@ -5,7 +5,7 @@ import random -from laceworksdk.api.token import TokenAPI +from laceworksdk.api.v1.token import TokenAPI # Tests diff --git a/tests/api/v1/test_vulnerability.py b/tests/api/v1/test_vulnerability.py index e4d3da2..29df586 100644 --- a/tests/api/v1/test_vulnerability.py +++ b/tests/api/v1/test_vulnerability.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta, timezone -from laceworksdk.api.vulnerability import VulnerabilityAPI +from laceworksdk.api.v1.vulnerability import VulnerabilityAPI # Build start/end times diff --git a/tests/api/v2/test_agent_access_tokens.py b/tests/api/v2/test_agent_access_tokens.py index e5dbdb3..7259f28 100644 --- a/tests/api/v2/test_agent_access_tokens.py +++ b/tests/api/v2/test_agent_access_tokens.py @@ -8,7 +8,7 @@ import pytest -from laceworksdk.api.agent_access_tokens import AgentAccessTokensAPI +from laceworksdk.api.v2.agent_access_tokens import AgentAccessTokensAPI AGENT_ACCESS_TOKEN_ID = None AGENT_ACCESS_TOKEN_ALIAS = None diff --git a/tests/api/v2/test_alert_channels.py b/tests/api/v2/test_alert_channels.py index 9c2cf93..d76cbaa 100644 --- a/tests/api/v2/test_alert_channels.py +++ b/tests/api/v2/test_alert_channels.py @@ -6,9 +6,10 @@ import random import string -from laceworksdk.api.alert_channels import AlertChannelsAPI +from laceworksdk.api.v2.alert_channels import AlertChannelsAPI INTEGRATION_GUID = None +INTEGRATION_GUID_ORG = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) @@ -54,6 +55,23 @@ def test_alert_channels_api_create(api): INTEGRATION_GUID = response["data"]["intgGuid"] +def test_alert_channels_api_create_org(api): + api.set_org_level_access(True) + response = api.alert_channels.create( + name=f"Slack Test Org {RANDOM_TEXT}", + type="SlackChannel", + enabled=1, + data={ + "slackUrl": f"https://hooks.slack.com/services/TEST/WEBHOOK/{RANDOM_TEXT}" + } + ) + api.set_org_level_access(False) + assert "data" in response.keys() + + global INTEGRATION_GUID_ORG + INTEGRATION_GUID_ORG = response["data"]["intgGuid"] + + def test_alert_channels_api_get_by_guid(api): assert INTEGRATION_GUID is not None if INTEGRATION_GUID: @@ -121,3 +139,12 @@ def test_alert_channels_api_delete(api): if INTEGRATION_GUID: response = api.alert_channels.delete(INTEGRATION_GUID) assert response.status_code == 204 + + +def test_alert_channels_api_delete_org(api): + assert INTEGRATION_GUID_ORG is not None + if INTEGRATION_GUID_ORG: + api.set_org_level_access(True) + response = api.alert_channels.delete(INTEGRATION_GUID_ORG) + api.set_org_level_access(False) + assert response.status_code == 204 diff --git a/tests/api/v2/test_alert_rules.py b/tests/api/v2/test_alert_rules.py index 0baa375..83f1044 100644 --- a/tests/api/v2/test_alert_rules.py +++ b/tests/api/v2/test_alert_rules.py @@ -8,7 +8,7 @@ import pytest -from laceworksdk.api.alert_rules import AlertRulesAPI +from laceworksdk.api.v2.alert_rules import AlertRulesAPI ALERT_RULE_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v2/test_audit_logs.py b/tests/api/v2/test_audit_logs.py index 1e93fc8..a8f23d2 100644 --- a/tests/api/v2/test_audit_logs.py +++ b/tests/api/v2/test_audit_logs.py @@ -7,7 +7,7 @@ import pytest -from laceworksdk.api.audit_logs import AuditLogsAPI +from laceworksdk.api.v2.audit_logs import AuditLogsAPI # Build start/end times current_time = datetime.now(timezone.utc) diff --git a/tests/api/v2/test_cloud_accounts.py b/tests/api/v2/test_cloud_accounts.py index 55420ef..342239f 100644 --- a/tests/api/v2/test_cloud_accounts.py +++ b/tests/api/v2/test_cloud_accounts.py @@ -6,7 +6,7 @@ import random import string -from laceworksdk.api.cloud_accounts import CloudAccountsAPI +from laceworksdk.api.v2.cloud_accounts import CloudAccountsAPI INTEGRATION_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v2/test_cloud_activities.py b/tests/api/v2/test_cloud_activities.py index 25dd71c..6104d70 100644 --- a/tests/api/v2/test_cloud_activities.py +++ b/tests/api/v2/test_cloud_activities.py @@ -3,11 +3,10 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import pytest - from datetime import datetime, timedelta, timezone +from unittest import TestCase -from laceworksdk.api.cloud_activities import CloudActivitiesAPI +from laceworksdk.api.v2.cloud_activities import CloudActivitiesAPI # Build start/end times current_time = datetime.now(timezone.utc) @@ -26,7 +25,6 @@ def test_cloud_activities_api_env_object_creation(api_env): assert isinstance(api_env.cloud_activities, CloudActivitiesAPI) -@pytest.mark.ci_exempt def test_cloud_activities_api_get(api): response = api.cloud_activities.get() assert "data" in response.keys() @@ -37,6 +35,41 @@ def test_cloud_activities_api_get_by_date(api): assert "data" in response.keys() +def test_cloud_activities_api_get_by_date_camelcase(api): + response = api.cloud_activities.get(startTime=start_time, endTime=end_time) + assert "data" in response.keys() + + +def test_cloud_activities_api_get_duplicate_key(api): + tester = TestCase() + with tester.assertRaises(KeyError): + api.cloud_activities.get(start_time=start_time, startTime=start_time, endTime=end_time) + + +def test_cloud_activities_api_get_pages(api): + response = api.cloud_activities.get_pages() + + for page in response: + assert "data" in page.keys() + + +def test_cloud_activities_api_get_data_items(api): + response = api.cloud_activities.get_data_items(start_time=start_time, end_time=end_time) + + event_keys = set([ + "endTime", + "entityMap", + "eventActor", + "eventId", + "eventModel", + "eventType", + "startTime" + ]) + + for item in response: + assert event_keys.issubset(item.keys()) + + def test_cloud_activities_api_search(api): response = api.cloud_activities.search(query_data={ "timeFilter": { diff --git a/tests/api/v2/test_container_registries.py b/tests/api/v2/test_container_registries.py index 6b89410..d3a24c3 100644 --- a/tests/api/v2/test_container_registries.py +++ b/tests/api/v2/test_container_registries.py @@ -6,7 +6,7 @@ import random import string -from laceworksdk.api.container_registries import ContainerRegistriesAPI +from laceworksdk.api.v2.container_registries import ContainerRegistriesAPI INTEGRATION_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v2/test_contract_info.py b/tests/api/v2/test_contract_info.py index e5362cb..5f5140d 100644 --- a/tests/api/v2/test_contract_info.py +++ b/tests/api/v2/test_contract_info.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone -from laceworksdk.api.contract_info import ContractInfoAPI +from laceworksdk.api.v2.contract_info import ContractInfoAPI # Build start/end times current_time = datetime.now(timezone.utc) diff --git a/tests/api/v2/test_datasources.py b/tests/api/v2/test_datasources.py new file mode 100644 index 0000000..b9a429e --- /dev/null +++ b/tests/api/v2/test_datasources.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +import random + +from laceworksdk.api.v2.datasources import DatasourcesAPI + + +# Tests + +def test_datasources_api_object_creation(api): + assert isinstance(api.datasources, DatasourcesAPI) + + +def test_datasources_api_get(api): + response = api.datasources.get() + assert "data" in response.keys() + + +def test_datasources_api_get_type(api): + response = api.datasources.get() + + if len(response) > 0: + datasource_type = random.choice(response["data"])["name"] + + response = api.datasources.get_by_type(type=datasource_type) + + assert "data" in response.keys() diff --git a/tests/api/v2/test_policies.py b/tests/api/v2/test_policies.py index 24de1e9..0cda30d 100644 --- a/tests/api/v2/test_policies.py +++ b/tests/api/v2/test_policies.py @@ -9,7 +9,7 @@ from datetime import datetime, timedelta, timezone -from laceworksdk.api.policies import PoliciesAPI +from laceworksdk.api.v2.policies import PoliciesAPI # Build start/end times current_time = datetime.now(timezone.utc) diff --git a/tests/api/v2/test_queries.py b/tests/api/v2/test_queries.py index 7dcc50b..7e446b4 100644 --- a/tests/api/v2/test_queries.py +++ b/tests/api/v2/test_queries.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta, timezone -from laceworksdk.api.queries import QueriesAPI +from laceworksdk.api.v2.queries import QueriesAPI # Build start/end times current_time = datetime.now(timezone.utc) diff --git a/tests/api/v2/test_report_rules.py b/tests/api/v2/test_report_rules.py index 1e51cf9..2049260 100644 --- a/tests/api/v2/test_report_rules.py +++ b/tests/api/v2/test_report_rules.py @@ -8,7 +8,7 @@ import pytest -from laceworksdk.api.report_rules import ReportRulesAPI +from laceworksdk.api.v2.report_rules import ReportRulesAPI REPORT_RULE_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v2/test_resource_groups.py b/tests/api/v2/test_resource_groups.py index 13400b5..e334f2b 100644 --- a/tests/api/v2/test_resource_groups.py +++ b/tests/api/v2/test_resource_groups.py @@ -6,7 +6,7 @@ import random import string -from laceworksdk.api.resource_groups import ResourceGroupsAPI +from laceworksdk.api.v2.resource_groups import ResourceGroupsAPI RESOURCE_GROUP_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) diff --git a/tests/api/v2/test_schemas.py b/tests/api/v2/test_schemas.py index 5092d7c..a767880 100644 --- a/tests/api/v2/test_schemas.py +++ b/tests/api/v2/test_schemas.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from laceworksdk.api.schemas import SchemasAPI +from laceworksdk.api.v2.schemas import SchemasAPI # Tests diff --git a/tests/api/v2/test_team_members.py b/tests/api/v2/test_team_members.py index e802b4b..510bf28 100644 --- a/tests/api/v2/test_team_members.py +++ b/tests/api/v2/test_team_members.py @@ -6,7 +6,7 @@ import random import string -from laceworksdk.api.team_members import TeamMembersAPI +from laceworksdk.api.v2.team_members import TeamMembersAPI TEAM_MEMBER_GUID = None RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) @@ -29,14 +29,14 @@ def test_team_members_api_get(api): def test_team_members_api_create(api): response = api.team_members.create( - username=f"{RANDOM_TEXT}@lacework.net", + user_name=f"{RANDOM_TEXT}@lacework.net", props={ "firstName": "John", "lastName": "Doe", "company": "Lacework", "accountAdmin": True }, - enabled=True + user_enabled=True ) assert "data" in response.keys() @@ -76,7 +76,7 @@ def test_team_members_api_update(api): if TEAM_MEMBER_GUID: response = api.team_members.update( TEAM_MEMBER_GUID, - enabled=False + user_enabled=False ) assert "data" in response.keys() diff --git a/tests/api/v2/test_user_profile.py b/tests/api/v2/test_user_profile.py index 7ea3139..b548e6b 100644 --- a/tests/api/v2/test_user_profile.py +++ b/tests/api/v2/test_user_profile.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from laceworksdk.api.user_profile import UserProfileAPI +from laceworksdk.api.v2.user_profile import UserProfileAPI # Tests @@ -17,5 +17,5 @@ def test_user_profile_api_env_object_creation(api_env): def test_user_profile_api_get(api): - response = api.schemas.get() + response = api.user_profile.get() assert len(response) > 0 From 40c66b66628faa85ebb9a485d5db97d4cf0d0656 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 11:45:29 -0500 Subject: [PATCH 05/30] feat: implemented all new APIv2 endpoints including pagination support --- laceworksdk/api/__init__.py | 16 +- laceworksdk/api/v2/activities.py | 134 ++++++++ laceworksdk/api/v2/alert_profiles.py | 114 +++++++ laceworksdk/api/v2/alerts.py | 79 +++++ laceworksdk/api/v2/configs.py | 50 +++ laceworksdk/api/v2/entities.py | 414 ++++++++++++++++++++++++ laceworksdk/api/v2/organization_info.py | 31 ++ laceworksdk/api/v2/vulnerabilities.py | 167 ++++++++++ tests/api/test_laceworksdk.py | 36 +++ tests/api/v2/test_activities.py | 109 +++++++ tests/api/v2/test_alert_profiles.py | 84 +++++ tests/api/v2/test_alerts.py | 73 +++++ tests/api/v2/test_configs.py | 44 +++ tests/api/v2/test_entities.py | 319 ++++++++++++++++++ tests/api/v2/test_organization_info.py | 17 + tests/api/v2/test_vulnerabilities.py | 110 +++++++ 16 files changed, 1796 insertions(+), 1 deletion(-) create mode 100644 laceworksdk/api/v2/activities.py create mode 100644 laceworksdk/api/v2/alert_profiles.py create mode 100644 laceworksdk/api/v2/alerts.py create mode 100644 laceworksdk/api/v2/configs.py create mode 100644 laceworksdk/api/v2/entities.py create mode 100644 laceworksdk/api/v2/organization_info.py create mode 100644 laceworksdk/api/v2/vulnerabilities.py create mode 100644 tests/api/test_laceworksdk.py create mode 100644 tests/api/v2/test_activities.py create mode 100644 tests/api/v2/test_alert_profiles.py create mode 100644 tests/api/v2/test_alerts.py create mode 100644 tests/api/v2/test_configs.py create mode 100644 tests/api/v2/test_entities.py create mode 100644 tests/api/v2/test_organization_info.py create mode 100644 tests/api/v2/test_vulnerabilities.py diff --git a/laceworksdk/api/__init__.py b/laceworksdk/api/__init__.py index 875e5d0..2f76d85 100644 --- a/laceworksdk/api/__init__.py +++ b/laceworksdk/api/__init__.py @@ -21,15 +21,21 @@ from .v1.suppressions import SuppressionsAPI from .v1.token import TokenAPI +from .v2.activities import ActivitiesAPI from .v2.agent_access_tokens import AgentAccessTokensAPI from .v2.alert_channels import AlertChannelsAPI +from .v2.alert_profiles import AlertProfilesAPI from .v2.alert_rules import AlertRulesAPI +from .v2.alerts import AlertsAPI from .v2.audit_logs import AuditLogsAPI from .v2.cloud_accounts import CloudAccountsAPI from .v2.cloud_activities import CloudActivitiesAPI +from .v2.configs import ConfigsAPI from .v2.container_registries import ContainerRegistriesAPI from .v2.contract_info import ContractInfoAPI from .v2.datasources import DatasourcesAPI +from .v2.entities import EntitiesAPI +from .v2.organization_info import OrganizationInfoAPI from .v2.policies import PoliciesAPI from .v2.queries import QueriesAPI from .v2.report_rules import ReportRulesAPI @@ -37,6 +43,7 @@ from .v2.schemas import SchemasAPI from .v2.team_members import TeamMembersAPI from .v2.user_profile import UserProfileAPI +from .v2.vulnerabilities import VulnerabilitiesAPI from laceworksdk.config import ( LACEWORK_ACCOUNT_ENVIRONMENT_VARIABLE, @@ -124,20 +131,26 @@ def __init__(self, # API Wrappers self.account = AccountAPI(self._session) + self.activities = ActivitiesAPI(self._session) self.agent_access_tokens = AgentAccessTokensAPI(self._session) self.alert_channels = AlertChannelsAPI(self._session) + self.alert_profiles = AlertProfilesAPI(self._session) self.alert_rules = AlertRulesAPI(self._session) + self.alerts = AlertsAPI(self._session) self.audit_logs = AuditLogsAPI(self._session) self.cloud_accounts = CloudAccountsAPI(self._session) self.cloud_activities = CloudActivitiesAPI(self._session) self.compliance = ComplianceAPI(self._session) self.compliance.config = CustomComplianceConfigAPI(self._session) + self.configs = ConfigsAPI(self._session) self.container_registries = ContainerRegistriesAPI(self._session) self.contract_info = ContractInfoAPI(self._session) self.datasources = DatasourcesAPI(self._session) + self.entities = EntitiesAPI(self._session) self.events = EventsAPI(self._session) self.files = DownloadFileAPI(self._session) self.integrations = IntegrationsAPI(self._session) + self.organization_info = OrganizationInfoAPI(self._session) self.policies = PoliciesAPI(self._session) self.queries = QueriesAPI(self._session) self.recommendations = RecommendationsAPI(self._session) @@ -149,7 +162,8 @@ def __init__(self, self.team_members = TeamMembersAPI(self._session) self.tokens = TokenAPI(self._session) self.user_profile = UserProfileAPI(self._session) - self.vulnerabilities = VulnerabilityAPI(self._session) + self.vulnerabilities = VulnerabilitiesAPI(self._session) + def set_org_level_access(self, org_level_access): """ A method to set whether the client should use organization-level API calls. diff --git a/laceworksdk/api/v2/activities.py b/laceworksdk/api/v2/activities.py new file mode 100644 index 0000000..9e07a9d --- /dev/null +++ b/laceworksdk/api/v2/activities.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +""" +Lacework Activities API wrapper. +""" + +from laceworksdk.api.search_endpoint import SearchEndpoint + + +class ActivitiesAPI: + + def __init__(self, session): + """ + Initializes the ActivitiesAPI object. + + :param session: An instance of the HttpSession class + + :return ActivitiesAPI object. + """ + + super().__init__() + self._base_path = "Activities" + + self.changed_files = ChangedFilesAPI(session, self._base_path) + self.connections = ConnectionsAPI(session, self._base_path) + self.dns = DnsAPI(session, self._base_path) + self.user_logins = UserLoginsAPI(session, self._base_path) + + +class ChangedFilesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ChangedFilesAPI object. + + :param session: An instance of the HttpSession class + + :return ChangedFilesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Changed Files objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="ChangedFiles", json=json) + + +class ConnectionsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ConnectionsAPI object. + + :param session: An instance of the HttpSession class + + :return ConnectionsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Connections objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Connections", json=json) + + +class DnsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the DnsAPI object. + + :param session: An instance of the HttpSession class + + :return DnsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search DNS lookup objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="DNSs", json=json) + + +class UserLoginsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the UserLoginsAPI object. + + :param session: An instance of the HttpSession class + + :return UserLoginsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search User Logins objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="UserLogins", json=json) diff --git a/laceworksdk/api/v2/alert_profiles.py b/laceworksdk/api/v2/alert_profiles.py new file mode 100644 index 0000000..a4854f0 --- /dev/null +++ b/laceworksdk/api/v2/alert_profiles.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +""" +Lacework AlertProfiles API wrapper. +""" + +from laceworksdk.api.crud_endpoint import CrudEndpoint + + +class AlertProfilesAPI(CrudEndpoint): + + def __init__(self, session): + """ + Initializes the AlertProfilesAPI object. + + :param session: An instance of the HttpSession class + + :return AlertProfilesAPI object. + """ + + super().__init__(session, "AlertProfiles") + + def create(self, + alert_profile_id, + alerts, + extends, + **request_params): + """ + A method to create a new AlertProfiles object. + + :param alert_profile_id: A string representing the id of the object. + :param alerts: A list of objects containing alert details for the parent object. + obj: + :param name: A string representing the name of the alert. + :param eventName: A string representing the name to show in Event Triage. + :param description: A string representing the description to show in Event Triage. + :param subject: A string representing the subject to show in the Event Dossier. + :param extends: A string representing the base alert profile object. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().create( + alert_profile_id=alert_profile_id, + alerts=alerts, + extends=extends, + **request_params + ) + + def get(self, + id=None): + """ + A method to get AlertProfiles objects. + + :param id: A string representing the object ID. + + :return response json + """ + + return super().get(id=id) + + def get_by_id(self, + id): + """ + A method to get an AlertProfiles object by ID. + + :param id: A string representing the object ID. + + :return response json + """ + + return self.get(id=id) + + def search(self, **request_params): + pass + + def update(self, + id, + alerts=None, + **request_params): + """ + A method to update an AlertProfiles object. + + :param id: A string representing the object ID. + :param alerts: A list of objects containing alert details for the parent object. + obj: + :param name: A string representing the name of the alert. + :param eventName: A string representing the name to show in Event Triage. + :param description: A string representing the description to show in Event Triage. + :param subject: A string representing the subject to show in the Event Dossier. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + return super().update( + id=id, + alerts=alerts, + **request_params + ) + + def delete(self, + id): + """ + A method to delete an AlertProfiles object. + + :param guid: A string representing the object ID. + + :return response json + """ + + return super().delete(id=id) diff --git a/laceworksdk/api/v2/alerts.py b/laceworksdk/api/v2/alerts.py new file mode 100644 index 0000000..8f11980 --- /dev/null +++ b/laceworksdk/api/v2/alerts.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" +Lacework Alerts API wrapper. +""" + +from laceworksdk.api.search_endpoint import SearchEndpoint + + +class AlertsAPI(SearchEndpoint): + + def __init__(self, session): + """ + Initializes the AlertsAPI object. + + :param session: An instance of the HttpSession class + + :return AlertsAPI object. + """ + + super().__init__(session, "Alerts") + + def get(self, + start_time=None, + end_time=None, + **request_params): + """ + A method to get Alerts objects. + + :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. + :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + params = self.build_dict_from_items( + request_params, + start_time=start_time, + end_time=end_time + ) + + response = self._session.get(self.build_url(), params=params) + + return response.json() + + def get_details(self, + id, + **request_params): + """ + A method to get Alerts objects by ID. + + :param id: A string representing the object ID. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + params = self.build_dict_from_items( + request_params + ) + + response = self._session.get(self.build_url(id=id), params=params) + + return response.json() + + def search(self, + json=None): + """ + A method to search Alerts objects. + + :param json: A dictionary containing the necessary search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(json=json) diff --git a/laceworksdk/api/v2/configs.py b/laceworksdk/api/v2/configs.py new file mode 100644 index 0000000..c0b8b11 --- /dev/null +++ b/laceworksdk/api/v2/configs.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +Lacework Configs API wrapper. +""" + +from laceworksdk.api.search_endpoint import SearchEndpoint + + +class ConfigsAPI: + + def __init__(self, session): + """ + Initializes the ConfigsAPI object. + + :param session: An instance of the HttpSession class + + :return ConfigsAPI object. + """ + + super().__init__() + self._base_path = "Configs" + + self.compliance_evaluations = ComplianceEvaluationsAPI(session, self._base_path) + + +class ComplianceEvaluationsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ComplianceEvaluationsAPI object. + + :param session: An instance of the HttpSession class + + :return ComplianceEvaluationsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search ComplianceEvaluations objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="ComplianceEvaluations", json=json) diff --git a/laceworksdk/api/v2/entities.py b/laceworksdk/api/v2/entities.py new file mode 100644 index 0000000..ddffb3c --- /dev/null +++ b/laceworksdk/api/v2/entities.py @@ -0,0 +1,414 @@ +# -*- coding: utf-8 -*- +""" +Lacework Entities API wrapper. +""" + +from laceworksdk.api.search_endpoint import SearchEndpoint + + +class EntitiesAPI: + + def __init__(self, session): + """ + Initializes the EntitiesAPI object. + + :param session: An instance of the HttpSession class + + :return EntitiesAPI object. + """ + + super().__init__() + self._base_path = "Entities" + + self.applications = ApplicationsAPI(session, self._base_path) + self.command_lines = CommandLinesAPI(session, self._base_path) + self.containers = ContainersAPI(session, self._base_path) + self.files = FilesAPI(session, self._base_path) + self.images = ImagesAPI(session, self._base_path) + self.internal_ip_addresses = InternalIPAddressesAPI(session, self._base_path) + self.k8s_pods = K8sPodsAPI(session, self._base_path) + self.machines = MachinesAPI(session, self._base_path) + self.machine_details = MachineDetailsAPI(session, self._base_path) + self.network_interfaces = NetworkInterfacesAPI(session, self._base_path) + self.new_file_hashes = NewFileHashesAPI(session, self._base_path) + self.packages = PackagesAPI(session, self._base_path) + self.processes = ProcessesAPI(session, self._base_path) + self.users = UsersAPI(session, self._base_path) + + +class ApplicationsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ApplicationsAPI object. + + :param session: An instance of the HttpSession class + + :return ApplicationsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Applications objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Applications", json=json) + + +class CommandLinesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the CommandLinesAPI object. + + :param session: An instance of the HttpSession class + + :return CommandLinesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search CommandLines objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="CommandLines", json=json) + + +class ContainersAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ContainersAPI object. + + :param session: An instance of the HttpSession class + + :return ContainersAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Containers objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Containers", json=json) + + +class FilesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the FilesAPI object. + + :param session: An instance of the HttpSession class + + :return FilesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Files objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Files", json=json) + + +class ImagesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ImagesAPI object. + + :param session: An instance of the HttpSession class + + :return ImagesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Images objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Images", json=json) + + +class InternalIPAddressesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the InternalIPAddressesAPI object. + + :param session: An instance of the HttpSession class + + :return InternalIPAddressesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search InternalIPAddresses objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="InternalIPAddresses", json=json) + + +class K8sPodsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the K8sPodsAPI object. + + :param session: An instance of the HttpSession class + + :return K8sPodsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search K8sPods objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="K8sPods", json=json) + + +class MachinesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the MachinesAPI object. + + :param session: An instance of the HttpSession class + + :return MachinesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Machines objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Machines", json=json) + + +class MachineDetailsAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the MachineDetailsAPI object. + + :param session: An instance of the HttpSession class + + :return MachineDetailsAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search MachineDetails objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="MachineDetails", json=json) + + +class NetworkInterfacesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the NetworkInterfacesAPI object. + + :param session: An instance of the HttpSession class + + :return NetworkInterfacesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search NetworkInterfaces objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="NetworkInterfaces", json=json) + + +class NewFileHashesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the NewFileHashesAPI object. + + :param session: An instance of the HttpSession class + + :return NewFileHashesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search NewFileHashes objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="NewFileHashes", json=json) + + +class PackagesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the PackagesAPI object. + + :param session: An instance of the HttpSession class + + :return PackagesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Packages objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Packages", json=json) + + +class ProcessesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ProcessesAPI object. + + :param session: An instance of the HttpSession class + + :return ProcessesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Processes objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Processes", json=json) + + +class UsersAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the UsersAPI object. + + :param session: An instance of the HttpSession class + + :return UsersAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Users objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Users", json=json) diff --git a/laceworksdk/api/v2/organization_info.py b/laceworksdk/api/v2/organization_info.py new file mode 100644 index 0000000..78efe07 --- /dev/null +++ b/laceworksdk/api/v2/organization_info.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +""" +Lacework OrganizationInfo API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint + + +class OrganizationInfoAPI(BaseEndpoint): + + def __init__(self, session): + """ + Initializes the OrganizationInfoAPI object. + + :param session: An instance of the HttpSession class + + :return OrganizationInfoAPI object. + """ + + super().__init__(session, "OrganizationInfo") + + def get(self): + """ + A method to get OrganizationInfo object. + + :return response json + """ + + response = self._session.get(self.build_url()) + + return response.json() diff --git a/laceworksdk/api/v2/vulnerabilities.py b/laceworksdk/api/v2/vulnerabilities.py new file mode 100644 index 0000000..7412056 --- /dev/null +++ b/laceworksdk/api/v2/vulnerabilities.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +""" +Lacework Vulnerabilities API wrapper. +""" + +from laceworksdk.api.base_endpoint import BaseEndpoint +from laceworksdk.api.search_endpoint import SearchEndpoint +from laceworksdk.api.v1.vulnerability import VulnerabilityAPI + + +class VulnerabilitiesAPI(VulnerabilityAPI): + + def __init__(self, session): + """ + Initializes the VulnerabilitiesAPI object. + + :param session: An instance of the HttpSession class + + :return VulnerabilitiesAPI object. + """ + + super().__init__(session) + self._base_path = "Vulnerabilities" + + self.containers = ContainerVulnerabilitiesAPI(session, self._base_path) + self.hosts = HostVulnerabilitiesAPI(session, self._base_path) + self.packages = SoftwarePackagesAPI(session, self._base_path) + + +class ContainerVulnerabilitiesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the ContainerVulnerabilitiesAPI object. + + :param session: An instance of the HttpSession class + + :return ContainerVulnerabilitiesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Container Vulnerabilities objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Containers", json=json) + + def scan(self, + registry, + repository, + tag, + **request_params): + """ + A method to issue Container Vulnerability scans. + + :param registry: A string representing the container registry to use. + :param repository: A string representing the container repository to use. + :param tag: A string representing the container tag to use. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return response json + """ + + json = self.build_dict_from_items( + **request_params, + registry=registry, + repository=repository, + tag=tag + ) + + response = self._session.post(self.build_url(resource="Containers", action="scan"), json=json) + + return response.json() + + def status(self, + request_id): + """ + A method to get the status of a Container Vulnerability scan. + + :param rquest_id: A string representing the request ID of the container scan. + + :return response json + """ + + if request_id is None or len(request_id) == 0: + raise ValueError("The value 'request_id' must be a valid container scan request ID.") + + response = self._session.get(self.build_url(id=request_id, resource="Containers", action="scan")) + + return response.json() + + +class HostVulnerabilitiesAPI(SearchEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the HostVulnerabilitiesAPI object. + + :param session: An instance of the HttpSession class + + :return HostVulnerabilitiesAPI object. + """ + + super().__init__(session, base_path) + + def search(self, + json=None): + """ + A method to search Host Vulnerabilities objects. + + :param json: A dictionary containing the desired search parameters. + (timeFilter, filters, returns) + + :return response json + """ + + return super().search(resource="Hosts", json=json) + + +class SoftwarePackagesAPI(BaseEndpoint): + + def __init__(self, session, base_path): + """ + Initializes the SoftwarePackagesAPI object. + + :param session: An instance of the HttpSession class + + :return SoftwarePackagesAPI object. + """ + + super().__init__(session, base_path) + + def scan(self, + os_pkg_info_list, + **request_params): + """ + A method to initiate a software package vulnerability scan. + + :param os_pkg_info_list: A list of packages to be scanned given the OS, OS Version, Package, and Package Version. + :obj + :param os: A string representing the name of the operating system. + :param osVer: A string representing the version of the operating system. + :param pkg: A string representing the name of the software package. + :param pkgVer: A string representing the verion of the software package. + :param request_params: Additional request parameters. + (provides support for parameters that may be added in the future) + + :return: response json + """ + + json = self.build_dict_from_items( + **request_params, + os_pkg_info_list=os_pkg_info_list + ) + + response = self._session.post(self.build_url(resource="SoftwarePackages", action="scan"), json=json) + + return response.json() diff --git a/tests/api/test_laceworksdk.py b/tests/api/test_laceworksdk.py new file mode 100644 index 0000000..a4f2fbe --- /dev/null +++ b/tests/api/test_laceworksdk.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from laceworksdk import LaceworkClient + + +# Tests + +def test_lacework_client_api_object_creation(api): + assert isinstance(api, LaceworkClient) + + +def test_lacework_client_api_env_object_creation(api_env): + assert isinstance(api_env, LaceworkClient) + + +def test_lacework_client_api_set_org(api): + + api.set_org_level_access(True) + assert api._session._org_level_access is True + + api.set_org_level_access(False) + assert api._session._org_level_access is False + + +def test_lacework_client_api_set_subaccount(api): + + old_subaccount = api._session._subaccount + + api.set_subaccount("testing") + assert api._session._subaccount == "testing" + + api.set_subaccount(old_subaccount) + assert api._session._subaccount == old_subaccount diff --git a/tests/api/v2/test_activities.py b/tests/api/v2/test_activities.py new file mode 100644 index 0000000..ce555db --- /dev/null +++ b/tests/api/v2/test_activities.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from datetime import datetime, timedelta, timezone + +from laceworksdk.api.v2.activities import ( + ActivitiesAPI, + ChangedFilesAPI, + ConnectionsAPI, + DnsAPI, + UserLoginsAPI +) + + +SCAN_REQUEST_ID = None + +# Build start/end times +current_time = datetime.now(timezone.utc) +start_time = current_time - timedelta(days=1) +start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") +end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +# Tests + +def test_activities_api_object_creation(api): + assert isinstance(api.activities, ActivitiesAPI) + + +def test_activities_changed_files_api_object_creation(api): + assert isinstance(api.activities.changed_files, ChangedFilesAPI) + + +def test_activities_changed_files_api_search_by_date(api): + response = api.activities.changed_files.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_activities_connections_api_object_creation(api): + assert isinstance(api.activities.connections, ConnectionsAPI) + + +def test_activities_connections_api_search_by_date(api): + response = api.activities.connections.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_activities_dns_api_object_creation(api): + assert isinstance(api.activities.dns, DnsAPI) + + +def test_activities_dns_api_search_by_date(api): + response = api.activities.dns.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_activities_user_logins_api_object_creation(api): + assert isinstance(api.activities.user_logins, UserLoginsAPI) + + +def test_activities_user_logins_api_search_by_date(api): + response = api.activities.user_logins.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 diff --git a/tests/api/v2/test_alert_profiles.py b/tests/api/v2/test_alert_profiles.py new file mode 100644 index 0000000..f3b72ed --- /dev/null +++ b/tests/api/v2/test_alert_profiles.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +import random +import string + +import pytest + +from laceworksdk.api.v2.alert_profiles import AlertProfilesAPI + +ALERT_PROFILE_GUID = None +RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) + + +# Tests + +def test_alert_profiles_api_object_creation(api): + assert isinstance(api.alert_profiles, AlertProfilesAPI) + + +def test_alert_profiles_api_get(api): + response = api.alert_profiles.get() + assert "data" in response.keys() + + +# TODO: Remove ci_exempt for all tests once v4.50 ships +@pytest.mark.ci_exempt +def test_alert_profiles_api_create(api): + response = api.alert_profiles.create( + alert_profile_id=f"Test_{RANDOM_TEXT}_AlertProfileID", + alerts=[ + { + "name": f"HE_User_NewViolation_{RANDOM_TEXT}", + "eventName": f"Alert Event Name {RANDOM_TEXT}", + "description": f"Alert Event Description {RANDOM_TEXT}", + "subject": f"Alert Event Subject {RANDOM_TEXT}" + } + ], + extends="LW_HE_USERS_DEFAULT_PROFILE" + ) + + assert "data" in response.keys() + + global ALERT_PROFILE_GUID + ALERT_PROFILE_GUID = response["data"]["alertProfileId"] + + +@pytest.mark.ci_exempt +def test_alert_profiles_api_get_by_guid(api): + assert ALERT_PROFILE_GUID is not None + if ALERT_PROFILE_GUID: + response = api.alert_profiles.get_by_id(id=ALERT_PROFILE_GUID) + + assert "data" in response.keys() + assert response["data"]["alertProfileId"] == ALERT_PROFILE_GUID + + +@pytest.mark.ci_exempt +def test_alert_profiles_api_update(api): + assert ALERT_PROFILE_GUID is not None + if ALERT_PROFILE_GUID: + response = api.alert_profiles.update( + ALERT_PROFILE_GUID, + alerts=[ + { + "name": f"HE_User_NewViolation_{RANDOM_TEXT}_Updated", + "eventName": f"Alert Event Name {RANDOM_TEXT} (Updated)", + "description": f"Alert Event Description {RANDOM_TEXT} (Updated)", + "subject": f"Alert Event Subject {RANDOM_TEXT} (Updated)" + } + ] + ) + + assert "data" in response.keys() + + +@pytest.mark.ci_exempt +def test_alert_profiles_api_delete(api): + assert ALERT_PROFILE_GUID is not None + if ALERT_PROFILE_GUID: + response = api.alert_profiles.delete(ALERT_PROFILE_GUID) + assert response.status_code == 204 diff --git a/tests/api/v2/test_alerts.py b/tests/api/v2/test_alerts.py new file mode 100644 index 0000000..c7daf0a --- /dev/null +++ b/tests/api/v2/test_alerts.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +import random + +from datetime import datetime, timedelta, timezone +from unittest import TestCase + +from laceworksdk.api.v2.alerts import AlertsAPI + +# Build start/end times +current_time = datetime.now(timezone.utc) +start_time = current_time - timedelta(days=1) +start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") +end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +# Tests + +def test_alerts_api_object_creation(api): + assert isinstance(api.alerts, AlertsAPI) + + +def test_alerts_api_get(api): + response = api.alerts.get() + assert "data" in response.keys() + + +def test_alerts_api_get_by_date(api): + response = api.alerts.get(start_time=start_time, end_time=end_time) + assert "data" in response.keys() + + +def test_alerts_api_get_by_date_camelcase(api): + response = api.alerts.get(startTime=start_time, endTime=end_time) + assert "data" in response.keys() + + +def test_alerts_api_get_duplicate_key(api): + tester = TestCase() + with tester.assertRaises(KeyError): + api.alerts.get(start_time=start_time, startTime=start_time, endTime=end_time) + + +def test_alerts_api_get_details(api): + response = api.alerts.get() + alert_id = random.choice(response["data"])["alertId"] + + response = api.alerts.get_details(alert_id) + + assert "data" in response.keys() + + +def test_alerts_api_search(api): + response = api.alerts.search(json={ + "timeFilter": { + "startTime": start_time, + "endTime": end_time + }, + "filters": [ + { + "expression": "eq", + "field": "alertModel", + "value": "AwsApiTracker" + } + ], + "returns": [] + }) + + for page in response: + assert "data" in page.keys() diff --git a/tests/api/v2/test_configs.py b/tests/api/v2/test_configs.py new file mode 100644 index 0000000..9b1cc3c --- /dev/null +++ b/tests/api/v2/test_configs.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from datetime import datetime, timedelta, timezone + +from laceworksdk.api.v2.configs import ( + ConfigsAPI, + ComplianceEvaluationsAPI +) + +# Build start/end times +current_time = datetime.now(timezone.utc) +start_time = current_time - timedelta(days=1) +start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") +end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +# Tests + +def test_configs_api_object_creation(api): + assert isinstance(api.configs, ConfigsAPI) + + +def test_configs_changed_files_api_object_creation(api): + assert isinstance(api.configs.compliance_evaluations, ComplianceEvaluationsAPI) + + +def test_configs_changed_files_api_search_by_date(api): + response = api.configs.compliance_evaluations.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + }, + "dataset": "AwsCompliance" + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 diff --git a/tests/api/v2/test_entities.py b/tests/api/v2/test_entities.py new file mode 100644 index 0000000..f6ebf8a --- /dev/null +++ b/tests/api/v2/test_entities.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from datetime import datetime, timedelta, timezone + +from laceworksdk.api.v2.entities import ( + EntitiesAPI, + ApplicationsAPI, + CommandLinesAPI, + ContainersAPI, + FilesAPI, + ImagesAPI, + InternalIPAddressesAPI, + K8sPodsAPI, + MachinesAPI, + MachineDetailsAPI, + NetworkInterfacesAPI, + NewFileHashesAPI, + PackagesAPI, + ProcessesAPI, + UsersAPI +) + + +SCAN_REQUEST_ID = None + +# Build start/end times +current_time = datetime.now(timezone.utc) +start_time = current_time - timedelta(days=1) +start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") +end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +# Tests + +def test_entities_api_object_creation(api): + assert isinstance(api.entities, EntitiesAPI) + + +def test_entities_applications_api_object_creation(api): + assert isinstance(api.entities.applications, ApplicationsAPI) + + +def test_entities_applications_api_search_by_date(api): + response = api.entities.applications.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_command_lines_api_object_creation(api): + assert isinstance(api.entities.command_lines, CommandLinesAPI) + + +def test_entities_command_lines_api_search_by_date(api): + response = api.entities.command_lines.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_containers_api_object_creation(api): + assert isinstance(api.entities.containers, ContainersAPI) + + +def test_entities_containers_api_search_by_date(api): + response = api.entities.containers.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_files_api_object_creation(api): + assert isinstance(api.entities.files, FilesAPI) + + +def test_entities_files_api_search_by_date(api): + response = api.entities.files.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_images_api_object_creation(api): + assert isinstance(api.entities.images, ImagesAPI) + + +def test_entities_images_api_search_by_date(api): + response = api.entities.images.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_internal_ip_addresses_api_object_creation(api): + assert isinstance(api.entities.internal_ip_addresses, InternalIPAddressesAPI) + + +def test_entities_internal_ip_addresses_api_search_by_date(api): + response = api.entities.internal_ip_addresses.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_k8s_pods_api_object_creation(api): + assert isinstance(api.entities.k8s_pods, K8sPodsAPI) + + +def test_entities_k8s_pods_api_search_by_date(api): + response = api.entities.k8s_pods.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_machines_api_object_creation(api): + assert isinstance(api.entities.machines, MachinesAPI) + + +def test_entities_machines_api_search_by_date(api): + response = api.entities.machines.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_machine_details_api_object_creation(api): + assert isinstance(api.entities.machine_details, MachineDetailsAPI) + + +def test_entities_machine_details_api_search_by_date(api): + response = api.entities.machine_details.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_network_interfaces_api_object_creation(api): + assert isinstance(api.entities.network_interfaces, NetworkInterfacesAPI) + + +def test_entities_network_interfaces_api_search_by_date(api): + response = api.entities.network_interfaces.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_new_file_hashes_api_object_creation(api): + assert isinstance(api.entities.new_file_hashes, NewFileHashesAPI) + + +def test_entities_new_file_hashes_api_search_by_date(api): + response = api.entities.new_file_hashes.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_packages_api_object_creation(api): + assert isinstance(api.entities.packages, PackagesAPI) + + +def test_entities_packages_api_search_by_date(api): + response = api.entities.packages.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_processes_api_object_creation(api): + assert isinstance(api.entities.processes, ProcessesAPI) + + +def test_entities_processes_api_search_by_date(api): + response = api.entities.processes.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_entities_users_api_object_creation(api): + assert isinstance(api.entities.users, UsersAPI) + + +def test_entities_users_api_search_by_date(api): + response = api.entities.users.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 diff --git a/tests/api/v2/test_organization_info.py b/tests/api/v2/test_organization_info.py new file mode 100644 index 0000000..03dc243 --- /dev/null +++ b/tests/api/v2/test_organization_info.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from laceworksdk.api.v2.organization_info import OrganizationInfoAPI + + +# Tests + +def test_organization_info_api_object_creation(api): + assert isinstance(api.organization_info, OrganizationInfoAPI) + + +def test_organization_info_api_get(api): + response = api.organization_info.get() + assert len(response) > 0 diff --git a/tests/api/v2/test_vulnerabilities.py b/tests/api/v2/test_vulnerabilities.py new file mode 100644 index 0000000..9b192ea --- /dev/null +++ b/tests/api/v2/test_vulnerabilities.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from datetime import datetime, timedelta, timezone + +from laceworksdk.api.v2.vulnerabilities import ( + ContainerVulnerabilitiesAPI, + HostVulnerabilitiesAPI, + SoftwarePackagesAPI, + VulnerabilitiesAPI +) + + +SCAN_REQUEST_ID = None + +# Build start/end times +current_time = datetime.now(timezone.utc) +start_time = current_time - timedelta(days=6) +start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") +end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + +# Tests + +def test_vulnerabilities_api_object_creation(api): + assert isinstance(api.vulnerabilities, VulnerabilitiesAPI) + + +def test_vulnerabilities_containers_api_object_creation(api): + assert isinstance(api.vulnerabilities.containers, ContainerVulnerabilitiesAPI) + + +def test_vulnerabilities_containers_api_search_by_date(api): + response = api.vulnerabilities.containers.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_vulnerabilities_containers_api_scan(api): + global SCAN_REQUEST_ID + response = api.vulnerabilities.containers.scan("index.docker.io", + "alannix/vulnerable-struts", + "latest") + assert "data" in response.keys() + if isinstance(response["data"], list): + SCAN_REQUEST_ID = response["data"][0].get("requestId") + elif isinstance(response["data"], dict): + SCAN_REQUEST_ID = response["data"].get("requestId") + + +def test_vulnerabilities_containers_api_scan_status(api): + if SCAN_REQUEST_ID: + response = api.vulnerabilities.containers.status(request_id=SCAN_REQUEST_ID) + assert "data" in response.keys() + + if isinstance(response["data"], list): + assert "status" in response["data"][0].keys() + elif isinstance(response["data"], dict): + assert "status" in response["data"].keys() + + +def test_vulnerabilities_hosts_api_object_creation(api): + assert isinstance(api.vulnerabilities.hosts, HostVulnerabilitiesAPI) + + +def test_vulnerabilities_hosts_api_search_by_date(api): + response = api.vulnerabilities.hosts.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + page_count = 0 + for page in response: + if page_count > 1: + return + assert len(page["data"]) == page.get("paging", {}).get("rows") + page_count += 1 + + +def test_vulnerabilities_packages_api_object_creation(api): + assert isinstance(api.vulnerabilities.packages, SoftwarePackagesAPI) + + +def test_vulnerabilities_packages_api_scan(api): + response = api.vulnerabilities.packages.scan(os_pkg_info_list=[{ + "os": "Ubuntu", + "osVer": "18.04", + "pkg": "openssl", + "pkgVer": "1.1.1-1ubuntu2.1~18.04.5" + }, { + "os": "Ubuntu", + "osVer": "20.04", + "pkg": "openssl", + "pkgVer": "1.1.1-1ubuntu2.1~20.04" + }]) + assert "data" in response.keys() From 5145e53980aa1bedf587b6f6f66a503643d8d526 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 11:45:48 -0500 Subject: [PATCH 06/30] chore: included flake8-quotes in dev dependencies --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 170751c..30fa0c9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ autopep8~=1.5 flake8~=3.9 +flake8-quotes~=3.2 pytest~=6.2 pytest-rerunfailures~=10.1 From b88455694e12bff38074f13d0cf759eb7ae3ce5f Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 12 Jan 2022 17:20:16 -0500 Subject: [PATCH 07/30] docs: small update to README.md for latest API endpoints --- README.md | 97 ++++++++++++++++++++++++------------------------------- 1 file changed, 42 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 06bb73e..aadcaae 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,42 @@ **laceworksdk** is a community developed Python library for interacting with the Lacework APIs. The purpose of this library is to simplify the common tasks required for interacting with the Lacework API, and allow -users write simple code to automate tasks related to their Lacework instance. From data retrieval to configuration, +users write simple code to automate tasks related to their Lacework instance(s). From data retrieval to configuration, this library aims to expose all publicly available APIs. For example, the following code would authenticate, -fetch events, fetch host vulnerabilities, and fetch container vulnerabilities - in 5 lines of code. +fetch events, fetch host vulnerabilities, and fetch container vulnerabilities. The latest version of the SDK supports +expressive searches as enabled by v2 of the Lacework APIs. -``` +```python from laceworksdk import LaceworkClient +lw = LaceworkClient() # This would leverage your default Lacework CLI profile. lw = LaceworkClient(account="ACCOUNT", + subaccount="SUBACCOUNT", api_key="API KEY", api_secret="API SECRET") events = lw.events.get_for_date_range(start_time=start_time, end_time=end_time) -host_vulns = lw.vulnerabilities.get_host_vulnerabilities() - -container_vulns = lw.vulnerabilities.get_container_vulnerabilities(image_digest="sha256:123") +host_vulns = lw.vulnerabilities.hosts.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } +}) + +container_vulns = lw.vulnerabilities.containers.search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + }, + "filters": [ + { + "field": "imageId", + "expression": "eq", + "value": "sha256:657922eb2d64b0a34fe7339f8b48afb9f2f44635d7d6eaa92af69591d29b3330" + } + ] +}) ``` ## Requirements @@ -34,30 +54,32 @@ container_vulns = lw.vulnerabilities.get_container_vulnerabilities(image_digest= ## How-To -The following data points are required to instantiate a LaceworkClient instance: +The following information is required to instantiate a LaceworkClient instance: - `account`: The Lacework account/organization domain. (`xxxxx`.lacework.net) -- `subaccount`: (Optional) The Lacework sub-account domain. (`xxxxx`.lacework.net) - - This is only used if leveraging the Manage@Scale organization feature of Lacework - `api_key`: The API Key that was generated from the Lacework UI/API. - `api_secret`: The API Secret that was generated from the Lacework UI/API. -To generate an API Key and Secret, do the following: +Optionally, you can also set a Lacework Sub-Account using the `subaccount` parameter. + +To generate API credentials, you'll need to do the following in Lacework: 1. In the Lacework web interface, go to Settings -> API Keys -2. Create a new API Key, or download information for an existing one. +2. Create a new API Key and download information the credentials. -### Environment Variables +## Environment Variables -The `account`, `subaccount`, `api_key`, and `api_secret` can also be set using environment variables or -saved in ~/.lacework.toml configuration file (same file as the Lacework CLI uses). +If you wish to configure the LaceworkClient instance using environment variables, this module honors the same +variables used by the Lacework CLI. The `account`, `subaccount`, `api_key`, `api_secret`, and `profile` parameters +can all be configured as specified below. -| Environment Variable | Description | Required | -| -------------------- | ---------------------------------------------------------------- | :------: | -| `LW_ACCOUNT` | Lacework account/organization domain (i.e. `xxxxx`.lacework.net) | Y | -| `LW_SUBACCOUNT` | Lacework sub-account domain (i.e. `xxxxx`.lacework.net) | N | -| `LW_API_KEY` | Lacework API Access Key | Y | -| `LW_API_SECRET` | Lacework API Access Secret | Y | +| Environment Variable | Description | Required | +| -------------------- | -------------------------------------------------------------------- | :------: | +| `LW_PROFILE` | Lacework CLI profile to use (configured at ~/.lacework.toml) | N | +| `LW_ACCOUNT` | Lacework account/organization domain (i.e. ``.lacework.net) | Y | +| `LW_SUBACCOUNT` | Lacework sub-account | N | +| `LW_API_KEY` | Lacework API Access Key | Y | +| `LW_API_SECRET` | Lacework API Access Secret | Y | ## Installation @@ -75,41 +97,6 @@ Installing and upgrading `laceworksdk` is easy: Are you looking for some sample scripts? Check out the [examples](examples/) folder! -## Implemented APIs - -### API v1 - -- [x] Account API -- [x] Compliance API -- [x] Custom Compliance Config API -- [x] Download File API -- [x] Events API -- [x] Integrations API -- [x] Recommendations API -- [x] Run Reports API -- [x] Suppressions API -- [x] Token API -- [x] Vulnerability API - -### API v2 - -- [x] Access Tokens -- [x] Agent Access Tokens -- [x] Alert Channels -- [x] Alert Rules -- [x] Audit Logs -- [x] Cloud Accounts -- [x] Cloud Activities -- [x] Container Registries -- [x] Contract Info -- [x] Policies -- [x] Queries -- [x] Report Rules -- [x] Resource Groups -- [x] Schemas -- [x] Team Members -- [x] User Profile - ### Contributing To install/configure the necessary requirements for contributing to this project, simply create a virtual environment, install `requirements.txt` and `requirements-dev.txt`, and set up a version file using the commands below: From f4ceeb6c5c2537c99ede9039b741e7a026eeb29a Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 19 Jan 2022 11:04:45 -0500 Subject: [PATCH 08/30] refactor: corrected docs and simplified `build_dict_from_items` method --- laceworksdk/api/base_endpoint.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/laceworksdk/api/base_endpoint.py b/laceworksdk/api/base_endpoint.py index 688362e..916c716 100644 --- a/laceworksdk/api/base_endpoint.py +++ b/laceworksdk/api/base_endpoint.py @@ -22,7 +22,10 @@ def __init__(self, def build_dict_from_items(self, *dicts, **items): """ - A method to build a dictionary based on inputs, pruning items that are None + A method to build a dictionary based on inputs, pruning items that are None. + + :raises KeyError: In case there is a duplicate key name in the dictionary. + :returns: A single dict built from the input. """ dict_list = list(dicts) @@ -32,11 +35,12 @@ def build_dict_from_items(self, *dicts, **items): for d in dict_list: for key, value in d.items(): camel_key = self._convert_lower_camel_case(key) - if value is not None: - if camel_key not in result.keys(): - result[camel_key] = value - else: - raise KeyError(f"Attempted to insert duplicate key '{camel_key}'") + if value is None: + continue + if camel_key in result.keys(): + raise KeyError(f"Attempted to insert duplicate key '{camel_key}'") + + result[camel_key] = value return result From 8bc018a50af06352721f6cc68f6db59d4c2a0e41 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 19 Jan 2022 13:05:45 -0500 Subject: [PATCH 09/30] fix: sanitizing access to 'query_data' --- laceworksdk/api/crud_endpoint.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/laceworksdk/api/crud_endpoint.py b/laceworksdk/api/crud_endpoint.py index fa2e67d..b8135b1 100644 --- a/laceworksdk/api/crud_endpoint.py +++ b/laceworksdk/api/crud_endpoint.py @@ -64,8 +64,9 @@ def search(self, json=None, **kwargs): """ # TODO: Remove this on v1.0 release - provided for back compat - if kwargs["query_data"]: - json = kwargs["query_data"] + query_data = kwargs.get("query_data") + if query_data: + json = query_data response = self._session.post(self.build_url(action="search"), json=json) From 11f4fcc55235c5b21a4d28258f1e3f7a1a00aa2f Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Wed, 19 Jan 2022 13:07:43 -0500 Subject: [PATCH 10/30] fix: narrowing down exception handling for pagination iteration --- laceworksdk/http_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/laceworksdk/http_session.py b/laceworksdk/http_session.py index 45e0d57..a18449c 100644 --- a/laceworksdk/http_session.py +++ b/laceworksdk/http_session.py @@ -296,7 +296,8 @@ def get_pages(self, uri, params=None, **kwargs): try: response_json = response.json() next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage") - except Exception: + except json.JSONDecodeError as e: + logger.error(f"Failed to decode response from Lacework as JSON: {e}") next_page = None if next_page: From 48ea0d6af305e30da39cad4cccc63b8651ee63a8 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Fri, 21 Jan 2022 22:22:25 -0500 Subject: [PATCH 11/30] refactor: moved resource attribute to base of SeachEndpoint --- laceworksdk/api/search_endpoint.py | 6 + laceworksdk/api/v2/activities.py | 137 +++------ laceworksdk/api/v2/configs.py | 41 +-- laceworksdk/api/v2/entities.py | 428 ++++++++------------------ laceworksdk/api/v2/vulnerabilities.py | 92 +++--- 5 files changed, 225 insertions(+), 479 deletions(-) diff --git a/laceworksdk/api/search_endpoint.py b/laceworksdk/api/search_endpoint.py index 82ae846..35790b1 100644 --- a/laceworksdk/api/search_endpoint.py +++ b/laceworksdk/api/search_endpoint.py @@ -5,6 +5,9 @@ class SearchEndpoint(BaseEndpoint): + # If defined, this is the resource used in the URL path + RESOURCE = "" + def __init__(self, session, object_type, @@ -27,6 +30,9 @@ def search(self, json=None, resource=None, **kwargs): :return a generator which yields a page of objects at a time as returned by the Lacework API. """ + if not resource and self.RESOURCE: + resource = self.RESOURCE + response = self._session.post(self.build_url(resource=resource, action="search"), json=json) while True: diff --git a/laceworksdk/api/v2/activities.py b/laceworksdk/api/v2/activities.py index 9e07a9d..eabf933 100644 --- a/laceworksdk/api/v2/activities.py +++ b/laceworksdk/api/v2/activities.py @@ -7,10 +7,25 @@ class ActivitiesAPI: + """A class used to represent the Activities API endpoint. + + The Activities API endpoint is simply a parent for different types of + activities that can be queried. + + Attributes + ---------- + changed_files: + A ChangedFilesAPI instance. + connections: + A ConnectionsAPI instance. + dns: + A DnsAPI instance. + user_logins: + A UserLoginsAPI instance. + """ def __init__(self, session): - """ - Initializes the ActivitiesAPI object. + """Initializes the ActivitiesAPI object. :param session: An instance of the HttpSession class @@ -27,108 +42,44 @@ def __init__(self, session): class ChangedFilesAPI(SearchEndpoint): + """A class used to represent the Changed Files API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ChangedFilesAPI object. - - :param session: An instance of the HttpSession class - - :return ChangedFilesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ - A method to search Changed Files objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="ChangedFiles", json=json) + Methods + ------- + search(json=None) + A method to search ChangedFiles objects. + """ + RESOURCE = "ChangedFiles" class ConnectionsAPI(SearchEndpoint): + """A class used to represent the Connections API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ConnectionsAPI object. - - :param session: An instance of the HttpSession class - - :return ConnectionsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Connections objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Connections", json=json) + """ + RESOURCE = "Connections" class DnsAPI(SearchEndpoint): + """A class used to represent the DNS Lookup API endpoint. - def __init__(self, session, base_path): - """ - Initializes the DnsAPI object. - - :param session: An instance of the HttpSession class - - :return DnsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search DNS lookup objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="DNSs", json=json) + """ + RESOURCE = "DNSs" class UserLoginsAPI(SearchEndpoint): - - def __init__(self, session, base_path): - """ - Initializes the UserLoginsAPI object. - - :param session: An instance of the HttpSession class - - :return UserLoginsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ - A method to search User Logins objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="UserLogins", json=json) + """A class used to represent the UserLogins API endpoint. + + Methods + ------- + search(json=None) + A method to search UserLogins objects. + """ + RESOURCE = "UserLogins" diff --git a/laceworksdk/api/v2/configs.py b/laceworksdk/api/v2/configs.py index c0b8b11..2951b87 100644 --- a/laceworksdk/api/v2/configs.py +++ b/laceworksdk/api/v2/configs.py @@ -7,10 +7,19 @@ class ConfigsAPI: + """A class used to represent the Configs API endpoint. + + The Configs API endpoint is simply a parent for different types of + configs that can be queried. + + Attributes + ---------- + compliance_evaluations: + A ComplianceEvaluationsAPI instance. + """ def __init__(self, session): - """ - Initializes the ConfigsAPI object. + """Initializes the ConfigsAPI object. :param session: An instance of the HttpSession class @@ -24,27 +33,11 @@ def __init__(self, session): class ComplianceEvaluationsAPI(SearchEndpoint): + """A class used to represent the Compliance Evaluations API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ComplianceEvaluationsAPI object. - - :param session: An instance of the HttpSession class - - :return ComplianceEvaluationsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search ComplianceEvaluations objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="ComplianceEvaluations", json=json) + """ + RESOURCE = "ComplianceEvaluations" diff --git a/laceworksdk/api/v2/entities.py b/laceworksdk/api/v2/entities.py index ddffb3c..9ac5c70 100644 --- a/laceworksdk/api/v2/entities.py +++ b/laceworksdk/api/v2/entities.py @@ -7,6 +7,42 @@ class EntitiesAPI: + """A class used to represent the Entities API endpoint. + + The Entities API endpoint is simply a parent for different types of + entities that can be queried. + + Attributes + ---------- + applications: + A ApplicationsAPI instance. + command_lines: + A CommandLinesAPI instance. + containers: + A ContainersAPI instance. + files: + A FilesAPI instance. + images: + A ImagesAPI instance. + internal_ip_addresses: + A InternalIPAddressesAPI instance. + k8s_pods: + A K8sPodsAPI instance. + machines: + A MachinesAPI instance. + machine_details: + A MachineDetailsAPI instance. + network_interfaces: + A NetworkInterfacesAPI instance. + new_file_hashes: + A NewFileHashesAPI instance. + packages: + A PackagesAPI instance. + processes: + A ProcessesAPI instance. + users: + A UsersAPI instance. + """ def __init__(self, session): """ @@ -37,378 +73,154 @@ def __init__(self, session): class ApplicationsAPI(SearchEndpoint): + """A class used to represent the Applications API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ApplicationsAPI object. - - :param session: An instance of the HttpSession class - - :return ApplicationsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Applications objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Applications", json=json) + """ + RESOURCE = "Applications" class CommandLinesAPI(SearchEndpoint): + """A class used to represent the Command Lines API endpoint. - def __init__(self, session, base_path): - """ - Initializes the CommandLinesAPI object. - - :param session: An instance of the HttpSession class - - :return CommandLinesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search CommandLines objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="CommandLines", json=json) + """ + RESOURCE = "CommandLines" class ContainersAPI(SearchEndpoint): + """A class used to represent the Containers API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ContainersAPI object. - - :param session: An instance of the HttpSession class - - :return ContainersAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Containers objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Containers", json=json) + """ + RESOURCE = "Containers" class FilesAPI(SearchEndpoint): + """A class used to represent the Files API endpoint. - def __init__(self, session, base_path): - """ - Initializes the FilesAPI object. - - :param session: An instance of the HttpSession class - - :return FilesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Files objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Files", json=json) + """ + RESOURCE = "Files" class ImagesAPI(SearchEndpoint): + """A class used to represent the Images API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ImagesAPI object. - - :param session: An instance of the HttpSession class - - :return ImagesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Images objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Images", json=json) + """ + RESOURCE = "Images" class InternalIPAddressesAPI(SearchEndpoint): + """A class used to represent the Internal IP Addresses API endpoint. - def __init__(self, session, base_path): - """ - Initializes the InternalIPAddressesAPI object. - - :param session: An instance of the HttpSession class - - :return InternalIPAddressesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search InternalIPAddresses objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="InternalIPAddresses", json=json) + """ + RESOURCE = "InternalIPAddresses" class K8sPodsAPI(SearchEndpoint): + """A class used to represent the K8s Pods API endpoint. - def __init__(self, session, base_path): - """ - Initializes the K8sPodsAPI object. - - :param session: An instance of the HttpSession class - - :return K8sPodsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search K8sPods objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="K8sPods", json=json) + """ + RESOURCE = "K8sPods" class MachinesAPI(SearchEndpoint): + """A class used to represent the Machines API endpoint. - def __init__(self, session, base_path): - """ - Initializes the MachinesAPI object. - - :param session: An instance of the HttpSession class - - :return MachinesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Machines objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Machines", json=json) + """ + RESOURCE = "Machines" class MachineDetailsAPI(SearchEndpoint): + """A class used to represent the Machine Details API endpoint. - def __init__(self, session, base_path): - """ - Initializes the MachineDetailsAPI object. - - :param session: An instance of the HttpSession class - - :return MachineDetailsAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search MachineDetails objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="MachineDetails", json=json) + """ + RESOURCE = "MachineDetails" class NetworkInterfacesAPI(SearchEndpoint): + """A class used to represent the Network Interfaces API endpoint. - def __init__(self, session, base_path): - """ - Initializes the NetworkInterfacesAPI object. - - :param session: An instance of the HttpSession class - - :return NetworkInterfacesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search NetworkInterfaces objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="NetworkInterfaces", json=json) + """ + RESOURCE = "NetworkInterfaces" class NewFileHashesAPI(SearchEndpoint): + """A class used to represent the New File Hashes API endpoint. - def __init__(self, session, base_path): - """ - Initializes the NewFileHashesAPI object. - - :param session: An instance of the HttpSession class - - :return NewFileHashesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search NewFileHashes objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="NewFileHashes", json=json) + """ + RESOURCE = "NewFileHashes" class PackagesAPI(SearchEndpoint): + """A class used to represent the Packages API endpoint. - def __init__(self, session, base_path): - """ - Initializes the PackagesAPI object. - - :param session: An instance of the HttpSession class - - :return PackagesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Packages objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Packages", json=json) + """ + RESOURCE = "Packages" class ProcessesAPI(SearchEndpoint): + """A class used to represent the Processes API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ProcessesAPI object. - - :param session: An instance of the HttpSession class - - :return ProcessesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Processes objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Processes", json=json) + """ + RESOURCE = "Processes" class UsersAPI(SearchEndpoint): + """A class used to represent the Users API endpoint. - def __init__(self, session, base_path): - """ - Initializes the UsersAPI object. - - :param session: An instance of the HttpSession class - - :return UsersAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Users objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Users", json=json) + """ + RESOURCE = "Users" diff --git a/laceworksdk/api/v2/vulnerabilities.py b/laceworksdk/api/v2/vulnerabilities.py index 7412056..cb80ffe 100644 --- a/laceworksdk/api/v2/vulnerabilities.py +++ b/laceworksdk/api/v2/vulnerabilities.py @@ -9,6 +9,22 @@ class VulnerabilitiesAPI(VulnerabilityAPI): + """A class used to represent the Vulnerabilities API endpoint. + + The Vulnerabilities API endpoint is simply a parent for different types of + vulnerabilities that can be queried. Due to namespace overlap with the v1 + API, this class is a subclass of VulnerabilityAPI to expose those methods + and provide backwards compatibility. + + Attributes + ---------- + containers: + A ContainerVulnerabilitiesAPI instance. + hosts: + A HostVulnerabilitiesAPI instance. + packages: + A SoftwarePackagesAPI instance. + """ def __init__(self, session): """ @@ -28,30 +44,18 @@ def __init__(self, session): class ContainerVulnerabilitiesAPI(SearchEndpoint): + """A class used to represent the Container Vulnerabilities API endpoint. - def __init__(self, session, base_path): - """ - Initializes the ContainerVulnerabilitiesAPI object. - - :param session: An instance of the HttpSession class - - :return ContainerVulnerabilitiesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Container Vulnerabilities objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Containers", json=json) + scan(registry, repository, tag, **request_params) + A method to issue a Container Vulnerability scan. + status(request_id) + A method to get the status of a Container Vulnerability scan. + """ + RESOURCE = "Containers" def scan(self, registry, @@ -100,44 +104,24 @@ def status(self, class HostVulnerabilitiesAPI(SearchEndpoint): + """A class used to represent the Host Vulnerabilities API endpoint. - def __init__(self, session, base_path): - """ - Initializes the HostVulnerabilitiesAPI object. - - :param session: An instance of the HttpSession class - - :return HostVulnerabilitiesAPI object. - """ - - super().__init__(session, base_path) - - def search(self, - json=None): - """ + Methods + ------- + search(json=None) A method to search Host Vulnerabilities objects. - - :param json: A dictionary containing the desired search parameters. - (timeFilter, filters, returns) - - :return response json - """ - - return super().search(resource="Hosts", json=json) + """ + RESOURCE = "Hosts" class SoftwarePackagesAPI(BaseEndpoint): + """A class used to represent the Software Packages API endpoint. - def __init__(self, session, base_path): - """ - Initializes the SoftwarePackagesAPI object. - - :param session: An instance of the HttpSession class - - :return SoftwarePackagesAPI object. - """ - - super().__init__(session, base_path) + Methods + ------- + scan(os_pkg_info_list, **request_params) + A method to initiate a Software Package vulnerability scan. + """ def scan(self, os_pkg_info_list, From af5f0ec8bb36cf20f2ded3a567e281cee9cc79fc Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Fri, 21 Jan 2022 22:23:09 -0500 Subject: [PATCH 12/30] refactor: removed unnecessary override method --- laceworksdk/api/v2/agent_access_tokens.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/laceworksdk/api/v2/agent_access_tokens.py b/laceworksdk/api/v2/agent_access_tokens.py index d9b329c..6fee5d5 100644 --- a/laceworksdk/api/v2/agent_access_tokens.py +++ b/laceworksdk/api/v2/agent_access_tokens.py @@ -41,20 +41,6 @@ def create(self, **request_params ) - def get(self, - id=None): - """ - A method to get AgentAccessTokens objects. - - :param id: A string representing the object ID. - - :return response json - """ - - return super().get( - id=id - ) - def get_by_id(self, id): """ From b6bbde207f2c3a6f0e9a17274b747f616b640b9a Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Fri, 21 Jan 2022 22:24:16 -0500 Subject: [PATCH 13/30] docs: added docstrings for 'query_data' where needed --- laceworksdk/api/v2/agent_access_tokens.py | 3 +++ laceworksdk/api/v2/alert_channels.py | 3 +++ laceworksdk/api/v2/alert_rules.py | 3 +++ laceworksdk/api/v2/cloud_accounts.py | 3 +++ laceworksdk/api/v2/container_registries.py | 3 +++ laceworksdk/api/v2/report_rules.py | 3 +++ laceworksdk/api/v2/resource_groups.py | 3 +++ laceworksdk/api/v2/team_members.py | 3 +++ 8 files changed, 24 insertions(+) diff --git a/laceworksdk/api/v2/agent_access_tokens.py b/laceworksdk/api/v2/agent_access_tokens.py index 6fee5d5..79205df 100644 --- a/laceworksdk/api/v2/agent_access_tokens.py +++ b/laceworksdk/api/v2/agent_access_tokens.py @@ -61,6 +61,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/alert_channels.py b/laceworksdk/api/v2/alert_channels.py index e900c88..bce3366 100644 --- a/laceworksdk/api/v2/alert_channels.py +++ b/laceworksdk/api/v2/alert_channels.py @@ -96,6 +96,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/alert_rules.py b/laceworksdk/api/v2/alert_rules.py index d257400..ce8db87 100644 --- a/laceworksdk/api/v2/alert_rules.py +++ b/laceworksdk/api/v2/alert_rules.py @@ -86,6 +86,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/cloud_accounts.py b/laceworksdk/api/v2/cloud_accounts.py index 8d50621..90ddcd5 100644 --- a/laceworksdk/api/v2/cloud_accounts.py +++ b/laceworksdk/api/v2/cloud_accounts.py @@ -93,6 +93,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/container_registries.py b/laceworksdk/api/v2/container_registries.py index 646697d..6836fd6 100644 --- a/laceworksdk/api/v2/container_registries.py +++ b/laceworksdk/api/v2/container_registries.py @@ -93,6 +93,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/report_rules.py b/laceworksdk/api/v2/report_rules.py index 97f949a..219cf58 100644 --- a/laceworksdk/api/v2/report_rules.py +++ b/laceworksdk/api/v2/report_rules.py @@ -87,6 +87,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/resource_groups.py b/laceworksdk/api/v2/resource_groups.py index 57971d8..8fdcaba 100644 --- a/laceworksdk/api/v2/resource_groups.py +++ b/laceworksdk/api/v2/resource_groups.py @@ -79,6 +79,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ diff --git a/laceworksdk/api/v2/team_members.py b/laceworksdk/api/v2/team_members.py index 66240f2..6630bdd 100644 --- a/laceworksdk/api/v2/team_members.py +++ b/laceworksdk/api/v2/team_members.py @@ -95,6 +95,9 @@ def search(self, :param json: A dictionary containing the desired search parameters. (filters, returns) + :param query_data: (DEPRECATED: Use 'json' moving forward) + A dictionary containing the desired search parameters. + (filters, returns) :return response json """ From 96b50b154c5d82286f135f60ade254d23af0deab Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Fri, 21 Jan 2022 22:24:51 -0500 Subject: [PATCH 14/30] docs: added docstrings for methods which are passed --- laceworksdk/api/v2/agent_access_tokens.py | 5 +++++ laceworksdk/api/v2/alert_profiles.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/laceworksdk/api/v2/agent_access_tokens.py b/laceworksdk/api/v2/agent_access_tokens.py index 79205df..40f096a 100644 --- a/laceworksdk/api/v2/agent_access_tokens.py +++ b/laceworksdk/api/v2/agent_access_tokens.py @@ -96,4 +96,9 @@ def update(self, ) def delete(self): + """ + A method to 'pass' when attempting to delete an AgentAccessToken object. + + Lacework does not currently allow for agent access tokens to be deleted. + """ pass diff --git a/laceworksdk/api/v2/alert_profiles.py b/laceworksdk/api/v2/alert_profiles.py index a4854f0..c6a3ea1 100644 --- a/laceworksdk/api/v2/alert_profiles.py +++ b/laceworksdk/api/v2/alert_profiles.py @@ -73,6 +73,11 @@ def get_by_id(self, return self.get(id=id) def search(self, **request_params): + """ + A method to 'pass' when attempting to search AlertProfiles objects. + + Search functionality is not yet implemented for Alert Profiles. + """ pass def update(self, From 2397048b9fd68f8182a6d3207e6031252bb3f48c Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Fri, 21 Jan 2022 22:25:43 -0500 Subject: [PATCH 15/30] refactor: changed LaceworkException name to match conventions --- laceworksdk/__init__.py | 2 +- laceworksdk/exceptions.py | 6 +++--- tests/test_laceworksdk.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/laceworksdk/__init__.py b/laceworksdk/__init__.py index cad51e1..a2d2e1d 100644 --- a/laceworksdk/__init__.py +++ b/laceworksdk/__init__.py @@ -8,7 +8,7 @@ import logging from .api import LaceworkClient # noqa: F401 -from .exceptions import ApiError, laceworksdkException # noqa: F401 +from .exceptions import ApiError, LaceworksdkException # noqa: F401 # Initialize Package Logging logger = logging.getLogger(__name__) diff --git a/laceworksdk/exceptions.py b/laceworksdk/exceptions.py index 08590f1..f70116a 100644 --- a/laceworksdk/exceptions.py +++ b/laceworksdk/exceptions.py @@ -10,14 +10,14 @@ logger = logging.getLogger(__name__) -class laceworksdkException(Exception): +class LaceworksdkException(Exception): """ Base class for all lacework package exceptions. """ pass -class ApiError(laceworksdkException): +class ApiError(LaceworksdkException): """ Errors returned in response to requests sent to the Lacework APIs. Several data attributes are available for inspection. @@ -72,7 +72,7 @@ def __repr__(self): ) -class MalformedResponse(laceworksdkException): +class MalformedResponse(LaceworksdkException): """Raised when a malformed response is received from Lacework.""" pass diff --git a/tests/test_laceworksdk.py b/tests/test_laceworksdk.py index a5656f3..dda09dd 100644 --- a/tests/test_laceworksdk.py +++ b/tests/test_laceworksdk.py @@ -17,4 +17,4 @@ def test_package_contents(self): # Lacework Exceptions assert hasattr(laceworksdk, "ApiError") - assert hasattr(laceworksdk, "laceworksdkException") + assert hasattr(laceworksdk, "LaceworksdkException") From 689901e0b2dd3207db9a9d0000b336f05596ec65 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Fri, 21 Jan 2022 22:26:48 -0500 Subject: [PATCH 16/30] tests: massively de-duped code for new search API tests --- tests/api/test_search_endpoint.py | 50 +++++ tests/api/v2/test_activities.py | 103 ++------- tests/api/v2/test_configs.py | 49 ++--- tests/api/v2/test_entities.py | 317 +++------------------------ tests/api/v2/test_vulnerabilities.py | 135 ++++-------- 5 files changed, 159 insertions(+), 495 deletions(-) create mode 100644 tests/api/test_search_endpoint.py diff --git a/tests/api/test_search_endpoint.py b/tests/api/test_search_endpoint.py new file mode 100644 index 0000000..3fdf4ce --- /dev/null +++ b/tests/api/test_search_endpoint.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from datetime import datetime, timedelta, timezone + + +class SearchEndpoint: + + BASE_OBJECT_TYPE = None + DAY_DELTA = 1 + OBJECT_MAP = {} + MAX_PAGES = 2 + + def test_object_creation(self, api_object): + + assert isinstance(api_object, self.BASE_OBJECT_TYPE) + + for attribute, object_type in self.OBJECT_MAP.items(): + assert isinstance(getattr(api_object, attribute), object_type) + + def test_api_search_by_date(self, api_object): + start_time, end_time = self._get_start_end_times(self.DAY_DELTA) + + for attribute in self.OBJECT_MAP.keys(): + response = getattr(api_object, attribute).search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + } + }) + + self._assert_pages(response, self.MAX_PAGES) + + def _assert_pages(self, response, max_pages): + page_count = 0 + for page in response: + if page_count >= max_pages: + return + assert len(page["data"]) == page.get("paging", {}).get("rows", 0) + page_count += 1 + + def _get_start_end_times(self, day_delta): + current_time = datetime.now(timezone.utc) + start_time = current_time - timedelta(days=day_delta) + start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") + end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + return start_time, end_time diff --git a/tests/api/v2/test_activities.py b/tests/api/v2/test_activities.py index ce555db..f10712d 100644 --- a/tests/api/v2/test_activities.py +++ b/tests/api/v2/test_activities.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.activities import ( ActivitiesAPI, @@ -12,98 +12,21 @@ DnsAPI, UserLoginsAPI ) - - -SCAN_REQUEST_ID = None - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=1) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") - +from tests.api.test_search_endpoint import SearchEndpoint # Tests -def test_activities_api_object_creation(api): - assert isinstance(api.activities, ActivitiesAPI) - - -def test_activities_changed_files_api_object_creation(api): - assert isinstance(api.activities.changed_files, ChangedFilesAPI) - - -def test_activities_changed_files_api_search_by_date(api): - response = api.activities.changed_files.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_activities_connections_api_object_creation(api): - assert isinstance(api.activities.connections, ConnectionsAPI) - - -def test_activities_connections_api_search_by_date(api): - response = api.activities.connections.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_activities_dns_api_object_creation(api): - assert isinstance(api.activities.dns, DnsAPI) - - -def test_activities_dns_api_search_by_date(api): - response = api.activities.dns.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_activities_user_logins_api_object_creation(api): - assert isinstance(api.activities.user_logins, UserLoginsAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.activities -def test_activities_user_logins_api_search_by_date(api): - response = api.activities.user_logins.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 +class TestActivitiesEndpoint(SearchEndpoint): + BASE_OBJECT_TYPE = ActivitiesAPI + OBJECT_MAP = { + "changed_files": ChangedFilesAPI, + "connections": ConnectionsAPI, + "dns": DnsAPI, + "user_logins": UserLoginsAPI + } diff --git a/tests/api/v2/test_configs.py b/tests/api/v2/test_configs.py index 9b1cc3c..3454f1e 100644 --- a/tests/api/v2/test_configs.py +++ b/tests/api/v2/test_configs.py @@ -3,42 +3,39 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.configs import ( ConfigsAPI, ComplianceEvaluationsAPI ) - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=1) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") - +from tests.api.test_search_endpoint import SearchEndpoint # Tests -def test_configs_api_object_creation(api): - assert isinstance(api.configs, ConfigsAPI) + +@pytest.fixture(scope="module") +def api_object(api): + return api.configs -def test_configs_changed_files_api_object_creation(api): - assert isinstance(api.configs.compliance_evaluations, ComplianceEvaluationsAPI) +class TestConfigsEndpoint(SearchEndpoint): + BASE_OBJECT_TYPE = ConfigsAPI + OBJECT_MAP = { + "compliance_evaluations": ComplianceEvaluationsAPI, + } + @pytest.mark.parametrize("dataset", ["AwsCompliance", "AzureCompliance", "GcpCompliance"]) + def test_api_search_by_date(self, api_object, dataset): + start_time, end_time = self._get_start_end_times(self.DAY_DELTA) -def test_configs_changed_files_api_search_by_date(api): - response = api.configs.compliance_evaluations.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - }, - "dataset": "AwsCompliance" - }) + for attribute in self.OBJECT_MAP.keys(): + response = getattr(api_object, attribute).search(json={ + "timeFilters": { + "startTime": start_time, + "endTime": end_time + }, + "dataset": dataset + }) - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 + self._assert_pages(response, self.MAX_PAGES) diff --git a/tests/api/v2/test_entities.py b/tests/api/v2/test_entities.py index f6ebf8a..c877cab 100644 --- a/tests/api/v2/test_entities.py +++ b/tests/api/v2/test_entities.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.entities import ( EntitiesAPI, @@ -22,298 +22,31 @@ ProcessesAPI, UsersAPI ) - - -SCAN_REQUEST_ID = None - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=1) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") - +from tests.api.test_search_endpoint import SearchEndpoint # Tests -def test_entities_api_object_creation(api): - assert isinstance(api.entities, EntitiesAPI) - - -def test_entities_applications_api_object_creation(api): - assert isinstance(api.entities.applications, ApplicationsAPI) - - -def test_entities_applications_api_search_by_date(api): - response = api.entities.applications.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_command_lines_api_object_creation(api): - assert isinstance(api.entities.command_lines, CommandLinesAPI) - - -def test_entities_command_lines_api_search_by_date(api): - response = api.entities.command_lines.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_containers_api_object_creation(api): - assert isinstance(api.entities.containers, ContainersAPI) - - -def test_entities_containers_api_search_by_date(api): - response = api.entities.containers.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_files_api_object_creation(api): - assert isinstance(api.entities.files, FilesAPI) - - -def test_entities_files_api_search_by_date(api): - response = api.entities.files.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_images_api_object_creation(api): - assert isinstance(api.entities.images, ImagesAPI) - - -def test_entities_images_api_search_by_date(api): - response = api.entities.images.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_internal_ip_addresses_api_object_creation(api): - assert isinstance(api.entities.internal_ip_addresses, InternalIPAddressesAPI) - - -def test_entities_internal_ip_addresses_api_search_by_date(api): - response = api.entities.internal_ip_addresses.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_k8s_pods_api_object_creation(api): - assert isinstance(api.entities.k8s_pods, K8sPodsAPI) - - -def test_entities_k8s_pods_api_search_by_date(api): - response = api.entities.k8s_pods.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_machines_api_object_creation(api): - assert isinstance(api.entities.machines, MachinesAPI) - - -def test_entities_machines_api_search_by_date(api): - response = api.entities.machines.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_machine_details_api_object_creation(api): - assert isinstance(api.entities.machine_details, MachineDetailsAPI) - - -def test_entities_machine_details_api_search_by_date(api): - response = api.entities.machine_details.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_network_interfaces_api_object_creation(api): - assert isinstance(api.entities.network_interfaces, NetworkInterfacesAPI) - - -def test_entities_network_interfaces_api_search_by_date(api): - response = api.entities.network_interfaces.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_new_file_hashes_api_object_creation(api): - assert isinstance(api.entities.new_file_hashes, NewFileHashesAPI) - - -def test_entities_new_file_hashes_api_search_by_date(api): - response = api.entities.new_file_hashes.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_packages_api_object_creation(api): - assert isinstance(api.entities.packages, PackagesAPI) - - -def test_entities_packages_api_search_by_date(api): - response = api.entities.packages.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_processes_api_object_creation(api): - assert isinstance(api.entities.processes, ProcessesAPI) - - -def test_entities_processes_api_search_by_date(api): - response = api.entities.processes.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_entities_users_api_object_creation(api): - assert isinstance(api.entities.users, UsersAPI) - - -def test_entities_users_api_search_by_date(api): - response = api.entities.users.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 +@pytest.fixture(scope="module") +def api_object(api): + return api.entities + + +class TestEntitiesEndpoint(SearchEndpoint): + BASE_OBJECT_TYPE = EntitiesAPI + OBJECT_MAP = { + "applications": ApplicationsAPI, + "command_lines": CommandLinesAPI, + "containers": ContainersAPI, + "files": FilesAPI, + "images": ImagesAPI, + "internal_ip_addresses": InternalIPAddressesAPI, + "k8s_pods": K8sPodsAPI, + "machines": MachinesAPI, + "machine_details": MachineDetailsAPI, + "network_interfaces": NetworkInterfacesAPI, + "new_file_hashes": NewFileHashesAPI, + "packages": PackagesAPI, + "processes": ProcessesAPI, + "users": UsersAPI + } diff --git a/tests/api/v2/test_vulnerabilities.py b/tests/api/v2/test_vulnerabilities.py index 9b192ea..c6520ab 100644 --- a/tests/api/v2/test_vulnerabilities.py +++ b/tests/api/v2/test_vulnerabilities.py @@ -3,7 +3,7 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.vulnerabilities import ( ContainerVulnerabilitiesAPI, @@ -11,100 +11,61 @@ SoftwarePackagesAPI, VulnerabilitiesAPI ) - - -SCAN_REQUEST_ID = None - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=6) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") - +from tests.api.test_search_endpoint import SearchEndpoint # Tests -def test_vulnerabilities_api_object_creation(api): - assert isinstance(api.vulnerabilities, VulnerabilitiesAPI) - -def test_vulnerabilities_containers_api_object_creation(api): - assert isinstance(api.vulnerabilities.containers, ContainerVulnerabilitiesAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.vulnerabilities -def test_vulnerabilities_containers_api_search_by_date(api): - response = api.vulnerabilities.containers.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) +class TestVulnerabilitesEndpoint(SearchEndpoint): + BASE_OBJECT_TYPE = VulnerabilitiesAPI + OBJECT_MAP = { + "containers": ContainerVulnerabilitiesAPI, + "hosts": HostVulnerabilitiesAPI + } - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 + def test_vulnerabilities_containers_api_scan(self, api_object, request): + response = api_object.containers.scan("index.docker.io", + "alannix/vulnerable-struts", + "latest") - -def test_vulnerabilities_containers_api_scan(api): - global SCAN_REQUEST_ID - response = api.vulnerabilities.containers.scan("index.docker.io", - "alannix/vulnerable-struts", - "latest") - assert "data" in response.keys() - if isinstance(response["data"], list): - SCAN_REQUEST_ID = response["data"][0].get("requestId") - elif isinstance(response["data"], dict): - SCAN_REQUEST_ID = response["data"].get("requestId") - - -def test_vulnerabilities_containers_api_scan_status(api): - if SCAN_REQUEST_ID: - response = api.vulnerabilities.containers.status(request_id=SCAN_REQUEST_ID) assert "data" in response.keys() - if isinstance(response["data"], list): - assert "status" in response["data"][0].keys() + scan_request_id = response["data"][0].get("requestId") elif isinstance(response["data"], dict): - assert "status" in response["data"].keys() - - -def test_vulnerabilities_hosts_api_object_creation(api): - assert isinstance(api.vulnerabilities.hosts, HostVulnerabilitiesAPI) - - -def test_vulnerabilities_hosts_api_search_by_date(api): - response = api.vulnerabilities.hosts.search(json={ - "timeFilters": { - "startTime": start_time, - "endTime": end_time - } - }) - - page_count = 0 - for page in response: - if page_count > 1: - return - assert len(page["data"]) == page.get("paging", {}).get("rows") - page_count += 1 - - -def test_vulnerabilities_packages_api_object_creation(api): - assert isinstance(api.vulnerabilities.packages, SoftwarePackagesAPI) - - -def test_vulnerabilities_packages_api_scan(api): - response = api.vulnerabilities.packages.scan(os_pkg_info_list=[{ - "os": "Ubuntu", - "osVer": "18.04", - "pkg": "openssl", - "pkgVer": "1.1.1-1ubuntu2.1~18.04.5" - }, { - "os": "Ubuntu", - "osVer": "20.04", - "pkg": "openssl", - "pkgVer": "1.1.1-1ubuntu2.1~20.04" - }]) - assert "data" in response.keys() + scan_request_id = response["data"].get("requestId") + + request.config.cache.set("scan_request_id", scan_request_id) + + def test_vulnerabilities_containers_api_scan_status(self, api_object, request): + scan_request_id = request.config.cache.get("scan_request_id", None) + assert scan_request_id is not None + if scan_request_id: + response = api_object.containers.status(request_id=scan_request_id) + assert "data" in response.keys() + + if isinstance(response["data"], list): + assert "status" in response["data"][0].keys() + elif isinstance(response["data"], dict): + assert "status" in response["data"].keys() + + def test_vulnerabilities_packages_api_object_creation(api, api_object): + assert isinstance(api_object.packages, SoftwarePackagesAPI) + + def test_vulnerabilities_packages_api_scan(api, api_object): + response = api_object.packages.scan(os_pkg_info_list=[{ + "os": "Ubuntu", + "osVer": "18.04", + "pkg": "openssl", + "pkgVer": "1.1.1-1ubuntu2.1~18.04.5" + }, { + "os": "Ubuntu", + "osVer": "20.04", + "pkg": "openssl", + "pkgVer": "1.1.1-1ubuntu2.1~20.04" + }]) + assert "data" in response.keys() From 3e6a62e2b374f7fc8634956e924063891e563f48 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 09:15:55 -0500 Subject: [PATCH 17/30] chore: importing Retry directly from urllib Ref: https://github.com/psf/requests/blob/v2.22.0/requests/packages.py --- laceworksdk/http_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laceworksdk/http_session.py b/laceworksdk/http_session.py index a18449c..75fb604 100644 --- a/laceworksdk/http_session.py +++ b/laceworksdk/http_session.py @@ -9,7 +9,7 @@ from datetime import datetime, timezone from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry +from urllib3.util.retry import Retry from laceworksdk import version from laceworksdk.config import ( From 9525938388fb58a24d713187b24eaaa0b7e64880 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 09:17:18 -0500 Subject: [PATCH 18/30] fix: passthrough Lacework response when maximum retries reached --- laceworksdk/http_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/laceworksdk/http_session.py b/laceworksdk/http_session.py index 75fb604..46dea2e 100644 --- a/laceworksdk/http_session.py +++ b/laceworksdk/http_session.py @@ -77,7 +77,8 @@ def _retry_session(self, total=retries, backoff_factor=backoff_factor, status_forcelist=status_forcelist, - allowed_methods=allowed_methods + allowed_methods=allowed_methods, + raise_on_status=False ) # Build the adapter with the retry criteria From 861fbaa869af6cf26b7250fee3692233dc1916bd Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 09:56:39 -0500 Subject: [PATCH 19/30] fix: supply json to the query execute function --- laceworksdk/api/v2/queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laceworksdk/api/v2/queries.py b/laceworksdk/api/v2/queries.py index a6339df..21a84c6 100644 --- a/laceworksdk/api/v2/queries.py +++ b/laceworksdk/api/v2/queries.py @@ -103,7 +103,7 @@ def execute(self, "value": value }) - response = self._session.post(self.build_url(action="execute")) + response = self._session.post(self.build_url(action="execute"), json=json) return response.json() From 152bee441587dc15993dd10201953122fb66a229 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 12:59:49 -0500 Subject: [PATCH 20/30] docs: improved function documentation for the BaseEndpoint class --- laceworksdk/api/base_endpoint.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/laceworksdk/api/base_endpoint.py b/laceworksdk/api/base_endpoint.py index 916c716..6e0a557 100644 --- a/laceworksdk/api/base_endpoint.py +++ b/laceworksdk/api/base_endpoint.py @@ -2,7 +2,7 @@ class BaseEndpoint: """ - Lacework BaseEndpoint Class. + A class used to implement base functionality for Lacework API Endpoints """ def __init__(self, @@ -47,6 +47,10 @@ def build_dict_from_items(self, *dicts, **items): def build_url(self, id=None, resource=None, action=None): """ Builds the URL to use based on the endpoint path, resource, type, and ID. + + :param id: A string representing the ID of an object to use in the URL + :param resource: A string representing the type of resource to append to the URL + :param action: A string representing the type of action to append to the URL """ result = f"{self._endpoint_root}/{self._object_type}" @@ -64,6 +68,10 @@ def build_url(self, id=None, resource=None, action=None): def _convert_lower_camel_case(param_name): """ Convert a Pythonic variable name to lowerCamelCase. + + This function will take an underscored parameter name like 'query_text' and convert it + to lowerCamelCase of 'queryText'. If a parameter with no underscores is provided, it will + assume that the value is already in lowerCamelCase format. """ words = param_name.split("_") From 0817ad95e96fa3068cc69a264d42117bc4f0cee7 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 14:09:59 -0500 Subject: [PATCH 21/30] chore: removed references to `query_data` in favor of `json` --- examples/example_alert_channels.py | 2 +- examples/example_audit_logs.py | 2 +- examples/example_cloudtrail.py | 2 +- laceworksdk/api/crud_endpoint.py | 5 ----- laceworksdk/api/v2/audit_logs.py | 7 +------ laceworksdk/api/v2/cloud_activities.py | 7 +------ 6 files changed, 5 insertions(+), 20 deletions(-) diff --git a/examples/example_alert_channels.py b/examples/example_alert_channels.py index e0abf00..6b54c3b 100644 --- a/examples/example_alert_channels.py +++ b/examples/example_alert_channels.py @@ -26,7 +26,7 @@ lacework_client.alert_channels.get() # Search Alert Channels - lacework_client.alert_channels.search(query_data={ + lacework_client.alert_channels.search(json={ "filters": [ { "expression": "eq", diff --git a/examples/example_audit_logs.py b/examples/example_audit_logs.py index d0b20aa..34165cd 100644 --- a/examples/example_audit_logs.py +++ b/examples/example_audit_logs.py @@ -36,7 +36,7 @@ lacework_client.audit_logs.get(start_time=start_time, end_time=end_time) # Search Audit Logs - lacework_client.audit_logs.search(query_data={ + lacework_client.audit_logs.search(json={ "timeFilter": { "startTime": start_time, "endTime": end_time diff --git a/examples/example_cloudtrail.py b/examples/example_cloudtrail.py index 5461292..668dea9 100644 --- a/examples/example_cloudtrail.py +++ b/examples/example_cloudtrail.py @@ -36,7 +36,7 @@ lacework_client.cloudtrail.get(start_time=start_time, end_time=end_time) # Search CloudTrail - lacework_client.cloudtrail.search(query_data={ + lacework_client.cloudtrail.search(json={ "timeFilter": { "startTime": start_time, "endTime": end_time diff --git a/laceworksdk/api/crud_endpoint.py b/laceworksdk/api/crud_endpoint.py index b8135b1..559dbb0 100644 --- a/laceworksdk/api/crud_endpoint.py +++ b/laceworksdk/api/crud_endpoint.py @@ -63,11 +63,6 @@ def search(self, json=None, **kwargs): :return response json """ - # TODO: Remove this on v1.0 release - provided for back compat - query_data = kwargs.get("query_data") - if query_data: - json = query_data - response = self._session.post(self.build_url(action="search"), json=json) return response.json() diff --git a/laceworksdk/api/v2/audit_logs.py b/laceworksdk/api/v2/audit_logs.py index cf7bda1..404e1d9 100644 --- a/laceworksdk/api/v2/audit_logs.py +++ b/laceworksdk/api/v2/audit_logs.py @@ -45,8 +45,7 @@ def get(self, return response.json() def search(self, - json=None, - query_data=None): + json=None): """ A method to search AuditLogs objects. @@ -56,10 +55,6 @@ def search(self, :return response json """ - # TODO: Remove this on v1.0 release - provided for back compat - if query_data: - json = query_data - response = self._session.post(self.build_url(action="search"), json=json) return response.json() diff --git a/laceworksdk/api/v2/cloud_activities.py b/laceworksdk/api/v2/cloud_activities.py index f54a16e..a09720b 100644 --- a/laceworksdk/api/v2/cloud_activities.py +++ b/laceworksdk/api/v2/cloud_activities.py @@ -79,8 +79,7 @@ def get_data_items(self, yield item def search(self, - json=None, - query_data=None): + json=None): """ A method to search CloudActivities objects. @@ -90,10 +89,6 @@ def search(self, :return response json """ - # TODO: Remove this on v1.0 release - provided for back compat - if query_data: - json = query_data - response = self._session.post(self.build_url(action="search"), json=json) return response.json() From 784f4707e9f15c72f2cda194828986de91562560 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 14:11:04 -0500 Subject: [PATCH 22/30] chore: removed redundant `search()` method overrides --- laceworksdk/api/v2/agent_access_tokens.py | 17 ----------------- laceworksdk/api/v2/alert_channels.py | 17 ----------------- laceworksdk/api/v2/alert_rules.py | 17 ----------------- laceworksdk/api/v2/cloud_accounts.py | 17 ----------------- laceworksdk/api/v2/container_registries.py | 17 ----------------- laceworksdk/api/v2/report_rules.py | 17 ----------------- laceworksdk/api/v2/resource_groups.py | 17 ----------------- laceworksdk/api/v2/team_members.py | 17 ----------------- 8 files changed, 136 deletions(-) diff --git a/laceworksdk/api/v2/agent_access_tokens.py b/laceworksdk/api/v2/agent_access_tokens.py index 40f096a..14d02e4 100644 --- a/laceworksdk/api/v2/agent_access_tokens.py +++ b/laceworksdk/api/v2/agent_access_tokens.py @@ -53,23 +53,6 @@ def get_by_id(self, return self.get(id=id) - def search(self, - json=None, - query_data=None): - """ - A method to search AgentAccessTokens objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, id, alias=None, diff --git a/laceworksdk/api/v2/alert_channels.py b/laceworksdk/api/v2/alert_channels.py index bce3366..be62213 100644 --- a/laceworksdk/api/v2/alert_channels.py +++ b/laceworksdk/api/v2/alert_channels.py @@ -88,23 +88,6 @@ def get_by_type(self, return self.get(type=type) - def search(self, - json=None, - query_data=None): - """ - A method to search AlertChannels objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, name=None, diff --git a/laceworksdk/api/v2/alert_rules.py b/laceworksdk/api/v2/alert_rules.py index ce8db87..62b698b 100644 --- a/laceworksdk/api/v2/alert_rules.py +++ b/laceworksdk/api/v2/alert_rules.py @@ -78,23 +78,6 @@ def get_by_guid(self, return self.get(guid=guid) - def search(self, - json=None, - query_data=None): - """ - A method to search AlertRules objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, type=None, diff --git a/laceworksdk/api/v2/cloud_accounts.py b/laceworksdk/api/v2/cloud_accounts.py index 90ddcd5..1598404 100644 --- a/laceworksdk/api/v2/cloud_accounts.py +++ b/laceworksdk/api/v2/cloud_accounts.py @@ -85,23 +85,6 @@ def get_by_type(self, return self.get(type=type) - def search(self, - json=None, - query_data=None): - """ - A method to search CloudAccounts objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, name=None, diff --git a/laceworksdk/api/v2/container_registries.py b/laceworksdk/api/v2/container_registries.py index 6836fd6..25e015c 100644 --- a/laceworksdk/api/v2/container_registries.py +++ b/laceworksdk/api/v2/container_registries.py @@ -85,23 +85,6 @@ def get_by_type(self, return self.get(type=type) - def search(self, - json=None, - query_data=None): - """ - A method to search ContainerRegistries objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, name=None, diff --git a/laceworksdk/api/v2/report_rules.py b/laceworksdk/api/v2/report_rules.py index 219cf58..2f3ea10 100644 --- a/laceworksdk/api/v2/report_rules.py +++ b/laceworksdk/api/v2/report_rules.py @@ -79,23 +79,6 @@ def get_by_guid(self, return self.get(guid=guid) - def search(self, - json=None, - query_data=None): - """ - A method to search ReportRules objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, type=None, diff --git a/laceworksdk/api/v2/resource_groups.py b/laceworksdk/api/v2/resource_groups.py index 8fdcaba..4d4c26a 100644 --- a/laceworksdk/api/v2/resource_groups.py +++ b/laceworksdk/api/v2/resource_groups.py @@ -71,23 +71,6 @@ def get_by_guid(self, return self.get(guid=guid) - def search(self, - json=None, - query_data=None): - """ - A method to search ResourceGroups objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, name=None, diff --git a/laceworksdk/api/v2/team_members.py b/laceworksdk/api/v2/team_members.py index 6630bdd..417af30 100644 --- a/laceworksdk/api/v2/team_members.py +++ b/laceworksdk/api/v2/team_members.py @@ -87,23 +87,6 @@ def get_by_guid(self, guid): return self.get(guid=guid) - def search(self, - json=None, - query_data=None): - """ - A method to search TeamMembers objects. - - :param json: A dictionary containing the desired search parameters. - (filters, returns) - :param query_data: (DEPRECATED: Use 'json' moving forward) - A dictionary containing the desired search parameters. - (filters, returns) - - :return response json - """ - - return super().search(json=json, query_data=query_data) - def update(self, guid, user_name=None, From 7d123b86dc0384da2132136b92bbed7e76360d64 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 14:11:59 -0500 Subject: [PATCH 23/30] fix: improved consistency of variable naming in AgentAccessTokensAPI --- laceworksdk/api/v2/agent_access_tokens.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/laceworksdk/api/v2/agent_access_tokens.py b/laceworksdk/api/v2/agent_access_tokens.py index 14d02e4..f14ec8d 100644 --- a/laceworksdk/api/v2/agent_access_tokens.py +++ b/laceworksdk/api/v2/agent_access_tokens.py @@ -55,8 +55,7 @@ def get_by_id(self, def update(self, id, - alias=None, - enabled=None, + token_enabled=None, **request_params): """ A method to update an AgentAccessTokens object. @@ -73,8 +72,7 @@ def update(self, return super().update( id=id, - token_alias=alias, - token_enabled=int(bool(enabled)), + token_enabled=int(bool(token_enabled)), **request_params ) @@ -84,4 +82,3 @@ def delete(self): Lacework does not currently allow for agent access tokens to be deleted. """ - pass From faf4ec4896a93b74da047b82e9adabc0f783a83e Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 14:12:39 -0500 Subject: [PATCH 24/30] chore: improved error message for JSONDecodeError --- laceworksdk/http_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/laceworksdk/http_session.py b/laceworksdk/http_session.py index 46dea2e..712b536 100644 --- a/laceworksdk/http_session.py +++ b/laceworksdk/http_session.py @@ -298,7 +298,7 @@ def get_pages(self, uri, params=None, **kwargs): response_json = response.json() next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage") except json.JSONDecodeError as e: - logger.error(f"Failed to decode response from Lacework as JSON: {e}") + logger.error(f"Failed to decode response from Lacework as JSON: {e}\nResponse text: {response.text}") next_page = None if next_page: From cf80f2d0dde79530f7b5b613ab3820372150f379 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 14:15:26 -0500 Subject: [PATCH 25/30] docs: added additional class docstrings --- laceworksdk/api/crud_endpoint.py | 3 +++ laceworksdk/api/search_endpoint.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/laceworksdk/api/crud_endpoint.py b/laceworksdk/api/crud_endpoint.py index 559dbb0..06fca19 100644 --- a/laceworksdk/api/crud_endpoint.py +++ b/laceworksdk/api/crud_endpoint.py @@ -4,6 +4,9 @@ class CrudEndpoint(BaseEndpoint): + """ + A class used to implement CRUD create/read/update/delete functionality for Lacework API Endpoints + """ def __init__(self, session, diff --git a/laceworksdk/api/search_endpoint.py b/laceworksdk/api/search_endpoint.py index 35790b1..03edc62 100644 --- a/laceworksdk/api/search_endpoint.py +++ b/laceworksdk/api/search_endpoint.py @@ -4,6 +4,9 @@ class SearchEndpoint(BaseEndpoint): + """ + A class used to implement Search functionality for Lacework API Endpoints + """ # If defined, this is the resource used in the URL path RESOURCE = "" From 384704b0b7a70c0e887651c4388a375bc4a797bb Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 22:33:21 -0500 Subject: [PATCH 26/30] fix: fixed bugs and consistency issues found in testing --- laceworksdk/api/v2/contract_info.py | 8 +------- laceworksdk/api/v2/queries.py | 2 +- laceworksdk/api/v2/resource_groups.py | 24 ++++++++++++------------ 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/laceworksdk/api/v2/contract_info.py b/laceworksdk/api/v2/contract_info.py index c5e5e02..15fcc65 100644 --- a/laceworksdk/api/v2/contract_info.py +++ b/laceworksdk/api/v2/contract_info.py @@ -20,14 +20,10 @@ def __init__(self, session): super().__init__(session, "ContractInfo") def get(self, - start_time=None, - end_time=None, **request_params): """ A method to get ContractInfo objects. - :param start_time: A "%Y-%m-%dT%H:%M:%SZ" structured timestamp to begin from. - :param end_time: A "%Y-%m-%dT%H:%M:%S%Z" structured timestamp to end at. :param request_params: Additional request parameters. (provides support for parameters that may be added in the future) @@ -35,9 +31,7 @@ def get(self, """ params = self.build_dict_from_items( - request_params, - start_time=start_time, - end_time=end_time + request_params ) response = self._session.get(self.build_url(), params=params) diff --git a/laceworksdk/api/v2/queries.py b/laceworksdk/api/v2/queries.py index 21a84c6..f2d841c 100644 --- a/laceworksdk/api/v2/queries.py +++ b/laceworksdk/api/v2/queries.py @@ -129,7 +129,7 @@ def execute_by_id(self, "value": value }) - response = self._session.post(self.build_url(action="execute", id=query_id), json=json) + response = self._session.post(self.build_url(resource=query_id, action="execute"), json=json) return response.json() diff --git a/laceworksdk/api/v2/resource_groups.py b/laceworksdk/api/v2/resource_groups.py index 4d4c26a..2361a0e 100644 --- a/laceworksdk/api/v2/resource_groups.py +++ b/laceworksdk/api/v2/resource_groups.py @@ -20,16 +20,16 @@ def __init__(self, session): super().__init__(session, "ResourceGroups") def create(self, - name, - type, + resource_name, + resource_type, enabled, props, **request_params): """ A method to create a new ResourceGroups object. - :param name: A string representing the object name. - :param type: A string representing the object type. + :param resource_name: A string representing the object name. + :param resource_type: A string representing the object type. :param enabled: A boolean/integer representing whether the object is enabled. (0 or 1) :param props: A JSON object matching the schema for the specified type. @@ -40,8 +40,8 @@ def create(self, """ return super().create( - resource_name=name, - resource_type=type, + resource_name=resource_name, + resource_type=resource_type, enabled=int(bool(enabled)), props=props, **request_params @@ -73,8 +73,8 @@ def get_by_guid(self, def update(self, guid, - name=None, - type=None, + resource_name=None, + resource_type=None, enabled=None, props=None, **request_params): @@ -82,8 +82,8 @@ def update(self, A method to update an ResourceGroups object. :param guid: A string representing the object GUID. - :param name: A string representing the object name. - :param type: A string representing the object type. + :param resource_name: A string representing the object name. + :param resource_type: A string representing the object type. :param enabled: A boolean/integer representing whether the object is enabled. (0 or 1) :param props: A JSON object matching the schema for the specified type. @@ -95,8 +95,8 @@ def update(self, return super().update( id=guid, - resource_name=name, - resource_type=type, + resource_name=resource_name, + resource_type=resource_type, enabled=int(bool(enabled)), props=props, **request_params From 7823cdc891569a46780e8e904fcc6a0ba26bf537 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 22:35:45 -0500 Subject: [PATCH 27/30] refactor: simplified and modernized all APIv2 tests --- requirements-dev.txt | 1 + tests/api/__init__.py | 47 ++++++ tests/api/test_base_endpoint.py | 79 ++++++++++ tests/api/test_crud_endpoint.py | 68 +++++++++ tests/api/test_read_endpoint.py | 40 +++++ tests/api/test_search_endpoint.py | 7 +- tests/api/v2/test_activities.py | 3 +- tests/api/v2/test_agent_access_tokens.py | 96 +++--------- tests/api/v2/test_alert_channels.py | 177 +++++++--------------- tests/api/v2/test_alert_profiles.py | 98 +++++------- tests/api/v2/test_alert_rules.py | 135 ++++------------- tests/api/v2/test_alerts.py | 78 +++------- tests/api/v2/test_audit_logs.py | 83 +++++----- tests/api/v2/test_cloud_accounts.py | 117 ++++---------- tests/api/v2/test_cloud_activities.py | 108 +++++-------- tests/api/v2/test_configs.py | 3 +- tests/api/v2/test_container_registries.py | 107 ++++--------- tests/api/v2/test_contract_info.py | 30 ++-- tests/api/v2/test_datasources.py | 27 ++-- tests/api/v2/test_entities.py | 3 +- tests/api/v2/test_organization_info.py | 25 ++- tests/api/v2/test_policies.py | 100 +++++------- tests/api/v2/test_queries.py | 112 ++++++-------- tests/api/v2/test_report_rules.py | 137 ++++------------- tests/api/v2/test_resource_groups.py | 104 ++++--------- tests/api/v2/test_schemas.py | 65 ++++---- tests/api/v2/test_team_members.py | 87 +++-------- tests/api/v2/test_user_profile.py | 28 +++- tests/api/v2/test_vulnerabilities.py | 3 +- 29 files changed, 807 insertions(+), 1161 deletions(-) create mode 100644 tests/api/test_base_endpoint.py create mode 100644 tests/api/test_crud_endpoint.py create mode 100644 tests/api/test_read_endpoint.py diff --git a/requirements-dev.txt b/requirements-dev.txt index 30fa0c9..7dd80c0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,5 @@ autopep8~=1.5 flake8~=3.9 flake8-quotes~=3.2 pytest~=6.2 +pytest-lazy-fixture~=0.6 pytest-rerunfailures~=10.1 diff --git a/tests/api/__init__.py b/tests/api/__init__.py index 6647d83..7b4124f 100644 --- a/tests/api/__init__.py +++ b/tests/api/__init__.py @@ -5,6 +5,8 @@ import logging import os +import random +import string import laceworksdk import pytest @@ -57,3 +59,48 @@ def api(account, subaccount, api_key, api_secret): @pytest.fixture(scope="session") def api_env(): return laceworksdk.LaceworkClient() + + +@pytest.fixture(scope="session") +def random_text(): + return "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) + + +@pytest.fixture(scope="session") +def email_alert_channel_guid(api): + response = api.alert_channels.search( + json={ + "filters": [ + { + "expression": "eq", + "field": "type", + "value": "EmailUser" + } + ], + "returns": [ + "intgGuid" + ] + } + ) + alert_channel_guid = response["data"][0]["intgGuid"] + return alert_channel_guid + + +@pytest.fixture(scope="session") +def aws_resource_group_guid(api): + response = api.resource_groups.search( + json={ + "filters": [ + { + "expression": "eq", + "field": "resourceType", + "value": "AWS" + } + ], + "returns": [ + "resourceGuid" + ] + } + ) + resource_group_guid = response["data"][0]["resourceGuid"] + return resource_group_guid diff --git a/tests/api/test_base_endpoint.py b/tests/api/test_base_endpoint.py new file mode 100644 index 0000000..292e3cc --- /dev/null +++ b/tests/api/test_base_endpoint.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +import random + +from datetime import datetime, timedelta, timezone + + +class BaseEndpoint: + + OBJECT_ID_NAME = None + OBJECT_TYPE = None + OBJECT_PARAM_EXCEPTIONS = [] + + def test_object_creation(self, api_object): + assert isinstance(api_object, self.OBJECT_TYPE) + + def _check_object_values(self, param_dict, response): + for key, value in param_dict.items(): + if key not in self.OBJECT_PARAM_EXCEPTIONS: + key = self._convert_lower_camel_case(key) + + if isinstance(value, dict): + assert value.items() <= response["data"][key].items() + else: + assert value == response["data"][key] + + def _get_object_classifier_test(self, + api_object, + classifier_name, + classifier_key=None): + if classifier_key is None: + classifier_key = classifier_name + + classifier_value = self._get_random_object(api_object, classifier_key) + + if classifier_value: + method = getattr(api_object, f"get_by_{classifier_name}") + + response = method(classifier_value) + + assert "data" in response.keys() + if isinstance(response["data"], list): + assert response["data"][0][classifier_key] == classifier_value + elif isinstance(response["data"], dict): + assert response["data"][classifier_key] == classifier_value + + def _get_random_object(self, api_object, key=None): + response = api_object.get() + + if len(response["data"]) > 0: + if key: + return random.choice(response["data"])[key] + else: + return random.choice(response["data"]) + else: + return None + + def _get_start_end_times(self, day_delta=1): + current_time = datetime.now(timezone.utc) + start_time = current_time - timedelta(days=day_delta) + start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") + end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + return start_time, end_time + + @staticmethod + def _convert_lower_camel_case(param_name): + words = param_name.split("_") + first_word = words[0] + + if len(words) == 1: + return first_word + + word_string = "".join([x.capitalize() or "_" for x in words[1:]]) + + return f"{first_word}{word_string}" diff --git a/tests/api/test_crud_endpoint.py b/tests/api/test_crud_endpoint.py new file mode 100644 index 0000000..8995391 --- /dev/null +++ b/tests/api/test_crud_endpoint.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +from tests.api.test_base_endpoint import BaseEndpoint + + +class CrudEndpoint(BaseEndpoint): + + def test_api_get(self, api_object): + response = api_object.get() + + assert "data" in response.keys() + + def test_api_create(self, api_object, api_object_create_body, request): + response = api_object.create(**api_object_create_body) + + assert "data" in response.keys() + self._check_object_values(api_object_create_body, response) + + request.config.cache.set(self.OBJECT_ID_NAME, response["data"][self.OBJECT_ID_NAME]) + + def test_api_search(self, api_object, request): + guid = request.config.cache.get(self.OBJECT_ID_NAME, None) + + if guid is None: + guid = self._get_random_object(api_object, self.OBJECT_ID_NAME) + + assert guid is not None + if guid: + response = api_object.search(json={ + "filters": [ + { + "expression": "eq", + "field": self.OBJECT_ID_NAME, + "value": guid + } + ], + "returns": [ + self.OBJECT_ID_NAME + ] + }) + + assert "data" in response.keys() + assert len(response["data"]) == 1 + assert response["data"][0][self.OBJECT_ID_NAME] == guid + + def test_api_update(self, api_object, api_object_update_body, request): + guid = request.config.cache.get(self.OBJECT_ID_NAME, None) + + if guid is None: + guid = self._get_random_object(api_object, self.OBJECT_ID_NAME) + + assert guid is not None + if guid: + response = api_object.update(guid, **api_object_update_body) + + assert "data" in response.keys() + + self._check_object_values(api_object_update_body, response) + + def test_api_delete(self, api_object, request): + guid = request.config.cache.get(self.OBJECT_ID_NAME, None) + assert guid is not None + if guid: + response = api_object.delete(guid) + assert response.status_code == 204 diff --git a/tests/api/test_read_endpoint.py b/tests/api/test_read_endpoint.py new file mode 100644 index 0000000..aafbc11 --- /dev/null +++ b/tests/api/test_read_endpoint.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +Test suite for the community-developed Python SDK for interacting with Lacework APIs. +""" + +import types + +from tests.api.test_base_endpoint import BaseEndpoint + + +class ReadEndpoint(BaseEndpoint): + + def test_api_get(self, api_object): + response = api_object.get() + + assert "data" in response.keys() + + def test_api_search(self, api_object): + random_object_id = self._get_random_object(api_object, self.OBJECT_ID_NAME) + assert random_object_id is not None + if random_object_id: + response = api_object.search(json={ + "filters": [ + { + "expression": "eq", + "field": self.OBJECT_ID_NAME, + "value": random_object_id + } + ], + "returns": [ + self.OBJECT_ID_NAME + ] + }) + + if isinstance(response, types.GeneratorType): + response = next(response) + + assert "data" in response.keys() + assert len(response["data"]) == 1 + assert response["data"][0][self.OBJECT_ID_NAME] == random_object_id diff --git a/tests/api/test_search_endpoint.py b/tests/api/test_search_endpoint.py index 3fdf4ce..8c82008 100644 --- a/tests/api/test_search_endpoint.py +++ b/tests/api/test_search_endpoint.py @@ -8,14 +8,15 @@ class SearchEndpoint: - BASE_OBJECT_TYPE = None - DAY_DELTA = 1 + OBJECT_TYPE = None OBJECT_MAP = {} + + DAY_DELTA = 1 MAX_PAGES = 2 def test_object_creation(self, api_object): - assert isinstance(api_object, self.BASE_OBJECT_TYPE) + assert isinstance(api_object, self.OBJECT_TYPE) for attribute, object_type in self.OBJECT_MAP.items(): assert isinstance(getattr(api_object, attribute), object_type) diff --git a/tests/api/v2/test_activities.py b/tests/api/v2/test_activities.py index f10712d..7b2a229 100644 --- a/tests/api/v2/test_activities.py +++ b/tests/api/v2/test_activities.py @@ -23,7 +23,8 @@ def api_object(api): class TestActivitiesEndpoint(SearchEndpoint): - BASE_OBJECT_TYPE = ActivitiesAPI + + OBJECT_TYPE = ActivitiesAPI OBJECT_MAP = { "changed_files": ChangedFilesAPI, "connections": ConnectionsAPI, diff --git a/tests/api/v2/test_agent_access_tokens.py b/tests/api/v2/test_agent_access_tokens.py index 7259f28..f4e0e08 100644 --- a/tests/api/v2/test_agent_access_tokens.py +++ b/tests/api/v2/test_agent_access_tokens.py @@ -3,92 +3,40 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string - import pytest from laceworksdk.api.v2.agent_access_tokens import AgentAccessTokensAPI - -AGENT_ACCESS_TOKEN_ID = None -AGENT_ACCESS_TOKEN_ALIAS = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_agent_access_tokens_api_object_creation(api): - assert isinstance(api.agent_access_tokens, AgentAccessTokensAPI) - - -def test_agent_access_tokens_api_env_object_creation(api_env): - assert isinstance(api_env.agent_access_tokens, AgentAccessTokensAPI) - - -def test_agent_access_tokens_api_get(api): - response = api.agent_access_tokens.get() - assert "data" in response.keys() - - -@pytest.mark.flaky_test -def test_agent_access_tokens_api_get_by_id(api): - response = api.agent_access_tokens.get() - - if len(response) > 0: - global AGENT_ACCESS_TOKEN_ID, AGENT_ACCESS_TOKEN_ALIAS - - # Choose a random agent access token - agent_access_token = random.choice(response["data"]) - - AGENT_ACCESS_TOKEN_ID = agent_access_token["accessToken"] - AGENT_ACCESS_TOKEN_ALIAS = agent_access_token["tokenAlias"] - - response = api.agent_access_tokens.get_by_id(id=AGENT_ACCESS_TOKEN_ID) - - assert "data" in response.keys() - - -@pytest.mark.flaky_test -def test_agent_access_tokens_api_search(api): - assert AGENT_ACCESS_TOKEN_ID is not None - if AGENT_ACCESS_TOKEN_ID: - - response = api.agent_access_tokens.search( - query_data={ - "filters": [ - { - "expression": "eq", - "field": "accessToken", - "value": AGENT_ACCESS_TOKEN_ID - } - ], - "returns": [ - "createdTime" - ] - } - ) +@pytest.fixture(scope="module") +def api_object(api): + return api.agent_access_tokens - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "token_enabled": 1 + } -@pytest.mark.flaky_test -def test_agent_access_tokens_api_update(api): - assert AGENT_ACCESS_TOKEN_ID is not None - if AGENT_ACCESS_TOKEN_ID: - new_alias = f"{AGENT_ACCESS_TOKEN_ALIAS} {RANDOM_TEXT}" +class TestAgentAccessTokens(CrudEndpoint): - response = api.agent_access_tokens.update( - AGENT_ACCESS_TOKEN_ID, - alias=new_alias - ) + OBJECT_ID_NAME = "accessToken" + OBJECT_TYPE = AgentAccessTokensAPI - assert response["data"]["tokenAlias"] == new_alias + def test_api_create(self): + """ + Agent Access Tokens shouldn't be created with tests + """ - response = api.agent_access_tokens.update( - AGENT_ACCESS_TOKEN_ID, - alias=AGENT_ACCESS_TOKEN_ALIAS - ) + def test_api_get_by_id(self, api_object): + self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME) - assert "data" in response.keys() - assert response["data"]["tokenAlias"] == AGENT_ACCESS_TOKEN_ALIAS + def test_api_delete(self): + """ + Agent Access Tokens cannot currently be deleted + """ diff --git a/tests/api/v2/test_alert_channels.py b/tests/api/v2/test_alert_channels.py index d76cbaa..3ab547c 100644 --- a/tests/api/v2/test_alert_channels.py +++ b/tests/api/v2/test_alert_channels.py @@ -3,148 +3,77 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string +import pytest from laceworksdk.api.v2.alert_channels import AlertChannelsAPI - -INTEGRATION_GUID = None -INTEGRATION_GUID_ORG = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_alert_channels_api_object_creation(api): - assert isinstance(api.alert_channels, AlertChannelsAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.alert_channels -def test_alert_channels_api_env_object_creation(api_env): - assert isinstance(api_env.alert_channels, AlertChannelsAPI) +@pytest.fixture(scope="module") +def api_object_org(api): + api.set_org_level_access(True) + yield api.alert_channels + api.set_org_level_access(False) -def test_alert_channels_api_get(api): - response = api.alert_channels.get() - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object_create_body(random_text): + return { + "name": f"Slack Test {random_text}", + "type": "SlackChannel", + "enabled": 1, + "data": { + "slackUrl": f"https://hooks.slack.com/services/TEST/WEBHOOK/{random_text}" + } + } -def test_alert_channels_api_get_by_type(api): - response = api.alert_channels.get() +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "name": f"Slack Test {random_text} Updated", + "enabled": 0 + } - if len(response) > 0: - alert_channel_type = random.choice(response["data"])["type"] - response = api.alert_channels.get_by_type(type=alert_channel_type) +class TestAlertChannels(CrudEndpoint): - assert "data" in response.keys() + OBJECT_ID_NAME = "intgGuid" + OBJECT_TYPE = AlertChannelsAPI + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) -def test_alert_channels_api_create(api): - response = api.alert_channels.create( - name=f"Slack Test {RANDOM_TEXT}", - type="SlackChannel", - enabled=1, - data={ - "slackUrl": f"https://hooks.slack.com/services/TEST/WEBHOOK/{RANDOM_TEXT}" - } - ) + def test_api_get_by_type(self, api_object): + self._get_object_classifier_test(api_object, "type") - assert "data" in response.keys() + def test_api_test(self, api_object): + response = api_object.search(json={ + "filters": [ + { + "expression": "ilike", + "field": "name", + "value": "default email" + } + ], + "returns": [ + "intgGuid" + ] + }) - global INTEGRATION_GUID - INTEGRATION_GUID = response["data"]["intgGuid"] + if len(response["data"]) > 0: + default_email_guid = response["data"][0]["intgGuid"] + response = api_object.test(guid=default_email_guid) + assert response.status_code == 204 -def test_alert_channels_api_create_org(api): - api.set_org_level_access(True) - response = api.alert_channels.create( - name=f"Slack Test Org {RANDOM_TEXT}", - type="SlackChannel", - enabled=1, - data={ - "slackUrl": f"https://hooks.slack.com/services/TEST/WEBHOOK/{RANDOM_TEXT}" - } - ) - api.set_org_level_access(False) - assert "data" in response.keys() - - global INTEGRATION_GUID_ORG - INTEGRATION_GUID_ORG = response["data"]["intgGuid"] - - -def test_alert_channels_api_get_by_guid(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - response = api.alert_channels.get_by_guid(guid=INTEGRATION_GUID) - - assert "data" in response.keys() - assert response["data"]["intgGuid"] == INTEGRATION_GUID - - -def test_alert_channels_api_search(api): - response = api.alert_channels.search(query_data={ - "filters": [ - { - "expression": "eq", - "field": "type", - "value": "SlackChannel" - } - ], - "returns": [ - "intgGuid" - ] - }) - assert "data" in response.keys() - - -def test_alert_channels_api_test(api): - response = api.alert_channels.search(query_data={ - "filters": [ - { - "expression": "ilike", - "field": "name", - "value": "default email" - } - ], - "returns": [ - "intgGuid" - ] - }) - default_email_guid = response["data"][0]["intgGuid"] - - if default_email_guid: - response = api.alert_channels.test(guid=default_email_guid) - assert response.status_code == 204 - - -def test_alert_channels_api_update(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - new_name = f"Slack Test {RANDOM_TEXT} Updated" - new_enabled = False - - response = api.alert_channels.update( - INTEGRATION_GUID, - name=new_name, - enabled=new_enabled - ) - - assert "data" in response.keys() - assert response["data"]["name"] == new_name - assert response["data"]["enabled"] == int(new_enabled) - - -def test_alert_channels_api_delete(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - response = api.alert_channels.delete(INTEGRATION_GUID) - assert response.status_code == 204 - - -def test_alert_channels_api_delete_org(api): - assert INTEGRATION_GUID_ORG is not None - if INTEGRATION_GUID_ORG: - api.set_org_level_access(True) - response = api.alert_channels.delete(INTEGRATION_GUID_ORG) - api.set_org_level_access(False) - assert response.status_code == 204 +@pytest.mark.parametrize("api_object", [pytest.lazy_fixture("api_object_org")]) +class TestAlertChannelsOrg(TestAlertChannels): + pass diff --git a/tests/api/v2/test_alert_profiles.py b/tests/api/v2/test_alert_profiles.py index f3b72ed..beb2cfd 100644 --- a/tests/api/v2/test_alert_profiles.py +++ b/tests/api/v2/test_alert_profiles.py @@ -3,82 +3,60 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string - import pytest from laceworksdk.api.v2.alert_profiles import AlertProfilesAPI - -ALERT_PROFILE_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_alert_profiles_api_object_creation(api): - assert isinstance(api.alert_profiles, AlertProfilesAPI) - +@pytest.fixture(scope="module") +def api_object(api): + return api.alert_profiles -def test_alert_profiles_api_get(api): - response = api.alert_profiles.get() - assert "data" in response.keys() - -# TODO: Remove ci_exempt for all tests once v4.50 ships -@pytest.mark.ci_exempt -def test_alert_profiles_api_create(api): - response = api.alert_profiles.create( - alert_profile_id=f"Test_{RANDOM_TEXT}_AlertProfileID", - alerts=[ +@pytest.fixture(scope="module") +def api_object_create_body(random_text): + return { + "alert_profile_id": f"Test_{random_text}_AlertProfileID", + "alerts": [ { - "name": f"HE_User_NewViolation_{RANDOM_TEXT}", - "eventName": f"Alert Event Name {RANDOM_TEXT}", - "description": f"Alert Event Description {RANDOM_TEXT}", - "subject": f"Alert Event Subject {RANDOM_TEXT}" + "name": f"HE_User_NewViolation_{random_text}", + "eventName": f"Alert Event Name {random_text}", + "description": f"Alert Event Description {random_text}", + "subject": f"Alert Event Subject {random_text}" } ], - extends="LW_HE_USERS_DEFAULT_PROFILE" - ) - - assert "data" in response.keys() + "extends": "LW_HE_USERS_DEFAULT_PROFILE" + } - global ALERT_PROFILE_GUID - ALERT_PROFILE_GUID = response["data"]["alertProfileId"] - -@pytest.mark.ci_exempt -def test_alert_profiles_api_get_by_guid(api): - assert ALERT_PROFILE_GUID is not None - if ALERT_PROFILE_GUID: - response = api.alert_profiles.get_by_id(id=ALERT_PROFILE_GUID) - - assert "data" in response.keys() - assert response["data"]["alertProfileId"] == ALERT_PROFILE_GUID +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "alerts": [ + { + "name": f"HE_User_NewViolation_{random_text}_Updated", + "eventName": f"Alert Event Name {random_text} Updated", + "description": f"Alert Event Description {random_text} Updated", + "subject": f"Alert Event Subject {random_text} Updated" + } + ] + } -@pytest.mark.ci_exempt -def test_alert_profiles_api_update(api): - assert ALERT_PROFILE_GUID is not None - if ALERT_PROFILE_GUID: - response = api.alert_profiles.update( - ALERT_PROFILE_GUID, - alerts=[ - { - "name": f"HE_User_NewViolation_{RANDOM_TEXT}_Updated", - "eventName": f"Alert Event Name {RANDOM_TEXT} (Updated)", - "description": f"Alert Event Description {RANDOM_TEXT} (Updated)", - "subject": f"Alert Event Subject {RANDOM_TEXT} (Updated)" - } - ] - ) +class TestAlertProfiles(CrudEndpoint): - assert "data" in response.keys() + OBJECT_ID_NAME = "alertProfileId" + OBJECT_TYPE = AlertProfilesAPI + OBJECT_PARAM_EXCEPTIONS = ["alerts"] + def test_api_search(self): + """ + Search is unavailable for this endpoint. + """ + pass -@pytest.mark.ci_exempt -def test_alert_profiles_api_delete(api): - assert ALERT_PROFILE_GUID is not None - if ALERT_PROFILE_GUID: - response = api.alert_profiles.delete(ALERT_PROFILE_GUID) - assert response.status_code == 204 + def test_api_get_by_id(self, api_object): + self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME) diff --git a/tests/api/v2/test_alert_rules.py b/tests/api/v2/test_alert_rules.py index 83f1044..36ab924 100644 --- a/tests/api/v2/test_alert_rules.py +++ b/tests/api/v2/test_alert_rules.py @@ -3,130 +3,49 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string - import pytest from laceworksdk.api.v2.alert_rules import AlertRulesAPI - -ALERT_RULE_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_alert_rules_api_object_creation(api): - assert isinstance(api.alert_rules, AlertRulesAPI) - - -def test_alert_rules_api_env_object_creation(api_env): - assert isinstance(api_env.alert_rules, AlertRulesAPI) - - -def test_alert_rules_api_get(api): - response = api.alert_rules.get() - assert "data" in response.keys() - - -@pytest.mark.flaky_test -def test_alert_rules_api_create(api): - - response = api.alert_channels.search( - query_data={ - "filters": [ - { - "expression": "eq", - "field": "type", - "value": "EmailUser" - } - ], - "returns": [ - "intgGuid" - ] - } - ) - alert_channel_guid = response["data"][0]["intgGuid"] +@pytest.fixture(scope="module") +def api_object(api): + return api.alert_rules - response = api.resource_groups.search( - query_data={ - "filters": [ - { - "expression": "eq", - "field": "resourceType", - "value": "AWS" - } - ], - "returns": [ - "resourceGuid" - ] - } - ) - resource_group_guid = response["data"][0]["resourceGuid"] - response = api.alert_rules.create( - type="Event", - filters={ - "name": f"Test Alert Rule {RANDOM_TEXT}", - "description": f"Test Alert Rule Description {RANDOM_TEXT}", +@pytest.fixture(scope="module") +def api_object_create_body(random_text, email_alert_channel_guid): + return { + "type": "Event", + "filters": { + "name": f"Test Alert Rule {random_text}", + "description": f"Test Alert Rule Description {random_text}", "enabled": 1, - "resourceGroups": [resource_group_guid], + "resourceGroups": [], "eventCategory": ["Compliance"], "severity": [1, 2, 3] }, - intg_guid_list=[alert_channel_guid] - ) - - assert "data" in response.keys() - - global ALERT_RULE_GUID - ALERT_RULE_GUID = response["data"]["mcGuid"] - + "intg_guid_list": [email_alert_channel_guid] + } -@pytest.mark.flaky_test -def test_alert_rules_api_get_by_guid(api): - assert ALERT_RULE_GUID is not None - if ALERT_RULE_GUID: - response = api.alert_rules.get_by_guid(guid=ALERT_RULE_GUID) - - assert "data" in response.keys() - assert response["data"]["mcGuid"] == ALERT_RULE_GUID - - -def test_alert_rules_api_search(api): - response = api.alert_rules.search(query_data={ - "filters": [ - { - "expression": "ilike", - "field": "filters.name", - "value": "test%" - } - ], - "returns": [ - "mcGuid" - ] - }) - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "filters": { + "name": f"Test Alert Rule {random_text} Updated", + "enabled": 0 + } + } -@pytest.mark.flaky_test -def test_alert_rules_api_update(api): - assert ALERT_RULE_GUID is not None - if ALERT_RULE_GUID: - response = api.alert_rules.update( - ALERT_RULE_GUID, - filters={ - "name": f"Test Alert Rule {RANDOM_TEXT} (Updated)", - "enabled": False - } - ) - assert "data" in response.keys() +class TestAlertRules(CrudEndpoint): + OBJECT_ID_NAME = "mcGuid" + OBJECT_TYPE = AlertRulesAPI -@pytest.mark.flaky_test -def test_alert_rules_api_delete(api): - assert ALERT_RULE_GUID is not None - if ALERT_RULE_GUID: - response = api.alert_rules.delete(ALERT_RULE_GUID) - assert response.status_code == 204 + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) diff --git a/tests/api/v2/test_alerts.py b/tests/api/v2/test_alerts.py index c7daf0a..2c76a91 100644 --- a/tests/api/v2/test_alerts.py +++ b/tests/api/v2/test_alerts.py @@ -3,71 +3,43 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random +import pytest -from datetime import datetime, timedelta, timezone from unittest import TestCase from laceworksdk.api.v2.alerts import AlertsAPI - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=1) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") +from tests.api.test_read_endpoint import ReadEndpoint # Tests -def test_alerts_api_object_creation(api): - assert isinstance(api.alerts, AlertsAPI) - - -def test_alerts_api_get(api): - response = api.alerts.get() - assert "data" in response.keys() - - -def test_alerts_api_get_by_date(api): - response = api.alerts.get(start_time=start_time, end_time=end_time) - assert "data" in response.keys() - - -def test_alerts_api_get_by_date_camelcase(api): - response = api.alerts.get(startTime=start_time, endTime=end_time) - assert "data" in response.keys() - - -def test_alerts_api_get_duplicate_key(api): - tester = TestCase() - with tester.assertRaises(KeyError): - api.alerts.get(start_time=start_time, startTime=start_time, endTime=end_time) +@pytest.fixture(scope="module") +def api_object(api): + return api.alerts -def test_alerts_api_get_details(api): - response = api.alerts.get() - alert_id = random.choice(response["data"])["alertId"] +class TestAlerts(ReadEndpoint): - response = api.alerts.get_details(alert_id) + OBJECT_ID_NAME = "alertId" + OBJECT_TYPE = AlertsAPI - assert "data" in response.keys() + def test_get_by_date(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.get(start_time=start_time, end_time=end_time) + assert "data" in response.keys() + def test_get_by_date_camelcase(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.get(startTime=start_time, endTime=end_time) + assert "data" in response.keys() -def test_alerts_api_search(api): - response = api.alerts.search(json={ - "timeFilter": { - "startTime": start_time, - "endTime": end_time - }, - "filters": [ - { - "expression": "eq", - "field": "alertModel", - "value": "AwsApiTracker" - } - ], - "returns": [] - }) + def test_get_duplicate_key(self, api_object): + start_time, end_time = self._get_start_end_times() + tester = TestCase() + with tester.assertRaises(KeyError): + api_object.get(start_time=start_time, startTime=start_time, endTime=end_time) - for page in response: - assert "data" in page.keys() + def test_get_details(self, api_object): + guid = self._get_random_object(api_object, self.OBJECT_ID_NAME) + response = api_object.get_details(guid) + assert "data" in response.keys() diff --git a/tests/api/v2/test_audit_logs.py b/tests/api/v2/test_audit_logs.py index a8f23d2..366c765 100644 --- a/tests/api/v2/test_audit_logs.py +++ b/tests/api/v2/test_audit_logs.py @@ -3,57 +3,46 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone - import pytest from laceworksdk.api.v2.audit_logs import AuditLogsAPI - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=6) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") +from tests.api.test_read_endpoint import ReadEndpoint # Tests -def test_audit_logs_api_object_creation(api): - assert isinstance(api.audit_logs, AuditLogsAPI) - - -def test_audit_logs_api_env_object_creation(api_env): - assert isinstance(api_env.audit_logs, AuditLogsAPI) - - -def test_audit_logs_api_get(api): - response = api.audit_logs.get() - assert "data" in response.keys() - - -def test_audit_logs_api_get_by_date(api): - response = api.audit_logs.get(start_time=start_time, end_time=end_time) - assert "data" in response.keys() - - -@pytest.mark.flaky_test -def test_audit_logs_api_search(api): - response = api.audit_logs.search(query_data={ - "timeFilter": { - "startTime": start_time, - "endTime": end_time - }, - "filters": [ - { - "expression": "rlike", - "field": "userName", - "value": "lacework.net" - } - ], - "returns": [ - "accountName", - "userAction", - "userName" - ] - }) - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object(api): + return api.audit_logs + + +class TestAuditLogs(ReadEndpoint): + + OBJECT_TYPE = AuditLogsAPI + + def test_get_by_date(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.get(start_time=start_time, end_time=end_time) + assert "data" in response.keys() + + def test_api_search(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.search(json={ + "timeFilter": { + "startTime": start_time, + "endTime": end_time + }, + "filters": [ + { + "expression": "rlike", + "field": "userName", + "value": "lacework.net" + } + ], + "returns": [ + "accountName", + "userAction", + "userName" + ] + }) + assert "data" in response.keys() diff --git a/tests/api/v2/test_cloud_accounts.py b/tests/api/v2/test_cloud_accounts.py index 342239f..15ceb7c 100644 --- a/tests/api/v2/test_cloud_accounts.py +++ b/tests/api/v2/test_cloud_accounts.py @@ -3,104 +3,51 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string +import pytest from laceworksdk.api.v2.cloud_accounts import CloudAccountsAPI - -INTEGRATION_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_read_endpoint import ReadEndpoint # Tests -def test_cloud_accounts_api_object_creation(api): - assert isinstance(api.cloud_accounts, CloudAccountsAPI) - - -def test_cloud_accounts_api_env_object_creation(api_env): - assert isinstance(api_env.cloud_accounts, CloudAccountsAPI) - - -def test_cloud_accounts_api_get(api): - response = api.cloud_accounts.get() - assert "data" in response.keys() - - -def test_cloud_accounts_api_get_by_type(api): - response = api.cloud_accounts.get() - - if len(response) > 0: - cloud_account_type = random.choice(response["data"])["type"] - - response = api.cloud_accounts.get_by_type(type=cloud_account_type) - - assert "data" in response.keys() - - -""" def test_cloud_accounts_api_create(api): - response = api.cloud_accounts.create( - name=f"AWS Config Test {RANDOM_TEXT}", - type="AwsCfg", - enabled=1, - data={ - "crossAccountCredentials": { - "externalId": f"{RANDOM_TEXT}", - "roleArn": f"arn:aws:iam::434813966438:role/lacework-test-{RANDOM_TEXT}" - } - } - ) - - assert "data" in response.keys() - - global INTEGRATION_GUID - INTEGRATION_GUID = response["data"]["intgGuid"] - +@pytest.fixture(scope="module") +def api_object(api): + return api.cloud_accounts -def test_cloud_accounts_api_get_by_guid(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - response = api.cloud_accounts.get_by_guid(guid=INTEGRATION_GUID) - assert "data" in response.keys() - assert response["data"]["intgGuid"] == INTEGRATION_GUID """ +# TODO: Figure out a way to test creates/updates/deletes without breaking things +# @pytest.fixture(scope="module") +# def api_object_create_body(random_text): +# return { +# "name": f"AWS Config Test {random_text}", +# "type": "AwsCfg", +# "enabled": 1, +# "data": { +# "crossAccountCredentials": { +# "externalId": f"{random_text}", +# "roleArn": f"arn:aws:iam::434813966438:role/lacework-test-{random_text}" +# } +# } +# } -def test_cloud_accounts_api_search(api): - response = api.cloud_accounts.search(query_data={ - "filters": [ - { - "expression": "eq", - "field": "type", - "value": "AwsCfg" - } - ], - "returns": [ - "intgGuid" - ] - }) - assert "data" in response.keys() +# @pytest.fixture(scope="module") +# def api_object_update_body(random_text): +# return { +# "name": f"AWS Config Test {random_text} Updated", +# "enabled": 0 +# } -""" def test_cloud_accounts_api_update(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - new_name = f"AWS Config {RANDOM_TEXT} Updated" - new_enabled = False - response = api.cloud_accounts.update( - INTEGRATION_GUID, - name=new_name, - enabled=new_enabled - ) +class TestCloudAccounts(ReadEndpoint): - assert "data" in response.keys() - assert response["data"]["name"] == new_name - assert response["data"]["enabled"] == int(new_enabled) + OBJECT_ID_NAME = "intgGuid" + OBJECT_TYPE = CloudAccountsAPI + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) -def test_cloud_accounts_api_delete(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - response = api.cloud_accounts.delete(INTEGRATION_GUID) - assert response.status_code == 204 """ + def test_api_get_by_type(self, api_object): + self._get_object_classifier_test(api_object, "type") diff --git a/tests/api/v2/test_cloud_activities.py b/tests/api/v2/test_cloud_activities.py index 6104d70..71e3920 100644 --- a/tests/api/v2/test_cloud_activities.py +++ b/tests/api/v2/test_cloud_activities.py @@ -3,89 +3,61 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone +import pytest + from unittest import TestCase from laceworksdk.api.v2.cloud_activities import CloudActivitiesAPI - -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=1) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") +from tests.api.test_read_endpoint import ReadEndpoint # Tests -def test_cloud_activities_api_object_creation(api): - assert isinstance(api.cloud_activities, CloudActivitiesAPI) - - -def test_cloud_activities_api_env_object_creation(api_env): - assert isinstance(api_env.cloud_activities, CloudActivitiesAPI) - - -def test_cloud_activities_api_get(api): - response = api.cloud_activities.get() - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object(api): + return api.cloud_activities -def test_cloud_activities_api_get_by_date(api): - response = api.cloud_activities.get(start_time=start_time, end_time=end_time) - assert "data" in response.keys() +class TestCloudActivities(ReadEndpoint): + OBJECT_ID_NAME = "eventId" + OBJECT_TYPE = CloudActivitiesAPI -def test_cloud_activities_api_get_by_date_camelcase(api): - response = api.cloud_activities.get(startTime=start_time, endTime=end_time) - assert "data" in response.keys() + def test_get_by_date(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.get(start_time=start_time, end_time=end_time) + assert "data" in response.keys() + def test_get_by_date_camelcase(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.get(startTime=start_time, endTime=end_time) + assert "data" in response.keys() -def test_cloud_activities_api_get_duplicate_key(api): - tester = TestCase() - with tester.assertRaises(KeyError): - api.cloud_activities.get(start_time=start_time, startTime=start_time, endTime=end_time) + def test_get_duplicate_key(self, api_object): + start_time, end_time = self._get_start_end_times() + tester = TestCase() + with tester.assertRaises(KeyError): + api_object.get(start_time=start_time, startTime=start_time, endTime=end_time) + def test_get_pages(self, api_object): + response = api_object.get_pages() -def test_cloud_activities_api_get_pages(api): - response = api.cloud_activities.get_pages() + for page in response: + assert "data" in page.keys() - for page in response: - assert "data" in page.keys() + def test_get_data_items(self, api_object): + start_time, end_time = self._get_start_end_times() + response = api_object.get_data_items(start_time=start_time, end_time=end_time) - -def test_cloud_activities_api_get_data_items(api): - response = api.cloud_activities.get_data_items(start_time=start_time, end_time=end_time) - - event_keys = set([ - "endTime", - "entityMap", - "eventActor", - "eventId", - "eventModel", - "eventType", - "startTime" - ]) - - for item in response: - assert event_keys.issubset(item.keys()) - - -def test_cloud_activities_api_search(api): - response = api.cloud_activities.search(query_data={ - "timeFilter": { - "startTime": start_time, - "endTime": end_time - }, - "filters": [ - { - "expression": "eq", - "field": "eventModel", - "value": "CloudTrailCep" - } - ], - "returns": [ + event_keys = set([ + "endTime", + "entityMap", + "eventActor", + "eventId", + "eventModel", "eventType", - "eventActor" - ] - }) - assert "data" in response.keys() + "startTime" + ]) + + for item in response: + assert event_keys.issubset(item.keys()) diff --git a/tests/api/v2/test_configs.py b/tests/api/v2/test_configs.py index 3454f1e..b038f1c 100644 --- a/tests/api/v2/test_configs.py +++ b/tests/api/v2/test_configs.py @@ -20,7 +20,8 @@ def api_object(api): class TestConfigsEndpoint(SearchEndpoint): - BASE_OBJECT_TYPE = ConfigsAPI + + OBJECT_TYPE = ConfigsAPI OBJECT_MAP = { "compliance_evaluations": ComplianceEvaluationsAPI, } diff --git a/tests/api/v2/test_container_registries.py b/tests/api/v2/test_container_registries.py index d3a24c3..e8e932e 100644 --- a/tests/api/v2/test_container_registries.py +++ b/tests/api/v2/test_container_registries.py @@ -3,101 +3,46 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string +import pytest from laceworksdk.api.v2.container_registries import ContainerRegistriesAPI - -INTEGRATION_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_container_registries_api_object_creation(api): - assert isinstance(api.container_registries, ContainerRegistriesAPI) - - -def test_container_registries_api_env_object_creation(api_env): - assert isinstance(api_env.container_registries, ContainerRegistriesAPI) - - -def test_container_registries_api_get(api): - response = api.container_registries.get() - assert "data" in response.keys() - - -def test_container_registries_api_get_by_type(api): - response = api.container_registries.get() - - if len(response) > 0: - cloud_account_type = random.choice(response["data"])["type"] +@pytest.fixture(scope="module") +def api_object(api): + return api.container_registries - response = api.container_registries.get_by_type(type=cloud_account_type) - assert "data" in response.keys() - - -def test_container_registries_api_create(api): - response = api.container_registries.create( - name=f"Docker Hub Test {RANDOM_TEXT}", - type="ContVulnCfg", - enabled=1, - data={ +@pytest.fixture(scope="module") +def api_object_create_body(random_text): + return { + "name": f"Docker Hub Test {random_text}", + "type": "ContVulnCfg", + "enabled": 1, + "data": { "registryType": "INLINE_SCANNER" } - ) - - assert "data" in response.keys() - - global INTEGRATION_GUID - INTEGRATION_GUID = response["data"]["intgGuid"] - - -def test_container_registries_api_get_by_guid(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - response = api.container_registries.get_by_guid(guid=INTEGRATION_GUID) - - assert "data" in response.keys() - assert response["data"]["intgGuid"] == INTEGRATION_GUID - + } -def test_container_registries_api_search(api): - response = api.container_registries.search(query_data={ - "filters": [ - { - "expression": "eq", - "field": "type", - "value": "ContVulnCfg" - } - ], - "returns": [ - "intgGuid" - ] - }) - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "name": f"Docker Hub Test {random_text} Updated", + "enabled": 0 + } -def test_container_registries_api_update(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - new_name = f"Docker Hub Test {RANDOM_TEXT} Updated" - new_enabled = False - response = api.container_registries.update( - INTEGRATION_GUID, - name=new_name, - enabled=new_enabled - ) +class TestContainerRegistries(CrudEndpoint): - assert "data" in response.keys() - assert response["data"]["name"] == new_name - assert response["data"]["enabled"] == int(new_enabled) + OBJECT_ID_NAME = "intgGuid" + OBJECT_TYPE = ContainerRegistriesAPI + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) -def test_container_registries_api_delete(api): - assert INTEGRATION_GUID is not None - if INTEGRATION_GUID: - response = api.container_registries.delete(INTEGRATION_GUID) - assert response.status_code == 204 + def test_api_get_by_type(self, api_object): + self._get_object_classifier_test(api_object, "type") diff --git a/tests/api/v2/test_contract_info.py b/tests/api/v2/test_contract_info.py index 5f5140d..d58bbfc 100644 --- a/tests/api/v2/test_contract_info.py +++ b/tests/api/v2/test_contract_info.py @@ -3,32 +3,24 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.contract_info import ContractInfoAPI +from tests.api.test_base_endpoint import BaseEndpoint -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=6) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%SZ") # Tests +@pytest.fixture(scope="module") +def api_object(api): + return api.contract_info -def test_contract_info_api_object_creation(api): - assert isinstance(api.contract_info, ContractInfoAPI) +class TestContractInfo(BaseEndpoint): -def test_contract_info_api_env_object_creation(api_env): - assert isinstance(api_env.contract_info, ContractInfoAPI) + OBJECT_ID_NAME = "alertId" + OBJECT_TYPE = ContractInfoAPI - -def test_contract_info_api_get(api): - response = api.contract_info.get() - assert "data" in response.keys() - - -def test_contract_info_api_get_by_date(api): - response = api.contract_info.get(start_time=start_time, end_time=end_time) - assert "data" in response.keys() + def test_api_get(self, api_object): + response = api_object.get() + assert "data" in response.keys() diff --git a/tests/api/v2/test_datasources.py b/tests/api/v2/test_datasources.py index b9a429e..ac5d5c3 100644 --- a/tests/api/v2/test_datasources.py +++ b/tests/api/v2/test_datasources.py @@ -3,28 +3,27 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random +import pytest from laceworksdk.api.v2.datasources import DatasourcesAPI +from tests.api.test_base_endpoint import BaseEndpoint # Tests -def test_datasources_api_object_creation(api): - assert isinstance(api.datasources, DatasourcesAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.datasources -def test_datasources_api_get(api): - response = api.datasources.get() - assert "data" in response.keys() +class TestDatasources(BaseEndpoint): + OBJECT_ID_NAME = "name" + OBJECT_TYPE = DatasourcesAPI -def test_datasources_api_get_type(api): - response = api.datasources.get() - - if len(response) > 0: - datasource_type = random.choice(response["data"])["name"] - - response = api.datasources.get_by_type(type=datasource_type) - + def test_api_get(self, api_object): + response = api_object.get() assert "data" in response.keys() + + def test_api_get_by_type(self, api_object): + self._get_object_classifier_test(api_object, "type", self.OBJECT_ID_NAME) diff --git a/tests/api/v2/test_entities.py b/tests/api/v2/test_entities.py index c877cab..4ed7479 100644 --- a/tests/api/v2/test_entities.py +++ b/tests/api/v2/test_entities.py @@ -33,7 +33,8 @@ def api_object(api): class TestEntitiesEndpoint(SearchEndpoint): - BASE_OBJECT_TYPE = EntitiesAPI + + OBJECT_TYPE = EntitiesAPI OBJECT_MAP = { "applications": ApplicationsAPI, "command_lines": CommandLinesAPI, diff --git a/tests/api/v2/test_organization_info.py b/tests/api/v2/test_organization_info.py index 03dc243..5009bb3 100644 --- a/tests/api/v2/test_organization_info.py +++ b/tests/api/v2/test_organization_info.py @@ -3,15 +3,30 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ +import pytest + from laceworksdk.api.v2.organization_info import OrganizationInfoAPI +from tests.api.test_base_endpoint import BaseEndpoint # Tests -def test_organization_info_api_object_creation(api): - assert isinstance(api.organization_info, OrganizationInfoAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.organization_info + + +class TestDatasources(BaseEndpoint): + + OBJECT_ID_NAME = "name" + OBJECT_TYPE = OrganizationInfoAPI + def test_api_get(self, api_object): + response = api_object.get() + keys = set([ + "orgAccount", + "orgAccountUrl" + ]) -def test_organization_info_api_get(api): - response = api.organization_info.get() - assert len(response) > 0 + for item in response["data"]: + assert keys.issubset(item.keys()) diff --git a/tests/api/v2/test_policies.py b/tests/api/v2/test_policies.py index 0cda30d..aa37a0a 100644 --- a/tests/api/v2/test_policies.py +++ b/tests/api/v2/test_policies.py @@ -3,89 +3,59 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import pytest import random -import string -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.policies import PoliciesAPI +from tests.api.test_crud_endpoint import CrudEndpoint -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=6) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%S.000Z") - -POLICY_ID = None -RANDOM_TEXT = "".join(random.choices(string.ascii_uppercase, k=8)) # Tests +@pytest.fixture(scope="module") +def api_object(api): + return api.policies -def test_policies_api_object_creation(api): - assert isinstance(api.policies, PoliciesAPI) +@pytest.fixture(scope="module") +def api_object_create_body(random_text, query): + return { + "policy_type": "Violation", + "query_id": query["queryId"], + "enabled": True, + "title": random_text, + "description": f"{random_text} description", + "remediation": "Policy remediation", + "severity": "high", + "alert_enabled": True, + "alert_profile": "LW_CloudTrail_Alerts", + "evaluator_id": query["evaluatorId"] + } -def test_cloud_accounts_api_env_object_creation(api_env): - assert isinstance(api_env.policies, PoliciesAPI) +@pytest.fixture(scope="module") +def api_object_update_body(): + return { + "enabled": False + } -def test_policies_api_get(api): - response = api.policies.get() - assert "data" in response.keys() - -@pytest.mark.flaky(reruns=3) -def test_policies_api_create(api): +@pytest.fixture(scope="module") +def query(api): queries = api.queries.get() queries = list(filter(lambda elem: elem["owner"] == "Lacework", queries["data"])) + query = random.choice(queries) + return query - if len(queries) > 0: - query = random.choice(queries) - - response = api.policies.create( - policy_type="Violation", - query_id=query["queryId"], - enabled=True, - title=RANDOM_TEXT, - description=f"{RANDOM_TEXT} description", - remediation="Policy remediation", - severity="high", - alert_enabled=True, - alert_profile="LW_CloudTrail_Alerts", - evaluator_id=query["evaluatorId"] - ) - - global POLICY_ID - POLICY_ID = response["data"]["policyId"] - - assert "data" in response.keys() - - -def test_policies_api_get_by_id(api): - assert POLICY_ID is not None - if POLICY_ID: - response = api.policies.get_by_id(policy_id=POLICY_ID) - - assert "data" in response.keys() - - -def test_policies_api_update(api): - assert POLICY_ID is not None - if POLICY_ID: - response = api.policies.update( - policy_id=POLICY_ID, - enabled=False - ) - assert "data" in response.keys() - assert response["data"]["enabled"] is False +class TestPolicies(CrudEndpoint): + OBJECT_ID_NAME = "policyId" + OBJECT_TYPE = PoliciesAPI -def test_policies_api_delete(api): - assert POLICY_ID is not None - if POLICY_ID: - response = api.policies.delete(policy_id=POLICY_ID) + def test_api_get_by_id(self, api_object): + self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME) - assert response.status_code == 204 + def test_api_search(self): + pass diff --git a/tests/api/v2/test_queries.py b/tests/api/v2/test_queries.py index 7e446b4..f28da40 100644 --- a/tests/api/v2/test_queries.py +++ b/tests/api/v2/test_queries.py @@ -4,94 +4,74 @@ """ import random -import string -from datetime import datetime, timedelta, timezone +import pytest from laceworksdk.api.v2.queries import QueriesAPI +from tests.api.test_crud_endpoint import CrudEndpoint -# Build start/end times -current_time = datetime.now(timezone.utc) -start_time = current_time - timedelta(days=6) -start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z") -end_time = current_time.strftime("%Y-%m-%dT%H:%M:%S.000Z") - -RANDOM_TEXT = "".join(random.choices(string.ascii_uppercase, k=8)) # Tests - -def test_queries_api_object_creation(api): - assert isinstance(api.queries, QueriesAPI) - - -def test_cloud_accounts_api_env_object_creation(api_env): - assert isinstance(api_env.queries, QueriesAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.queries -def test_queries_api_get(api): - response = api.queries.get() - assert "data" in response.keys() - - -def test_queries_api_create(api): - response = api.queries.create( - evaluator_id="Cloudtrail", - query_id=RANDOM_TEXT, - query_text=f"""{RANDOM_TEXT} {{ +@pytest.fixture(scope="module") +def api_object_create_body(random_text): + return { + "evaluator_id": "Cloudtrail", + "query_id": random_text, + "query_text": f"""{random_text} {{ source {{CloudTrailRawEvents e}} - filter {{EVENT_SOURCE = 'iam.amazonaws.com' AND EVENT:userIdentity.name::String NOT LIKE '%{RANDOM_TEXT}'}} + filter {{EVENT_SOURCE = 'iam.amazonaws.com' AND EVENT:userIdentity.name::String NOT LIKE '%{random_text}'}} return distinct {{EVENT_NAME, EVENT}} - }}""" - ) - - assert "data" in response.keys() + }}""" + } -def test_queries_api_get_by_id(api): - response = api.queries.get_by_id(query_id=RANDOM_TEXT) - - assert "data" in response.keys() - - -def test_queries_api_update(api): - response = api.queries.update( - query_id=RANDOM_TEXT, - query_text=f"""{RANDOM_TEXT} {{ +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "query_text": f"""{random_text} {{ source {{CloudTrailRawEvents e}} - filter {{EVENT_SOURCE = 'iam.amazonaws.com' AND EVENT:userIdentity.name::String NOT LIKE '%{RANDOM_TEXT}_updated'}} + filter {{EVENT_SOURCE = 'iam.amazonaws.com' AND EVENT:userIdentity.name::String NOT LIKE '%{random_text}_updated'}} return distinct {{EVENT_NAME, EVENT}} - }}""" - ) - - assert "data" in response.keys() + }}""" + } -def test_queries_api_execute_by_id(api): - response = api.queries.execute_by_id( - query_id=RANDOM_TEXT, - arguments={ - "StartTimeRange": start_time, - "EndTimeRange": end_time, - } - ) +@pytest.fixture(scope="module") +def query(api): + queries = api.queries.get() + queries = list(filter(lambda elem: elem["owner"] == "Lacework", queries["data"])) + query = random.choice(queries) + return query - assert "data" in response.keys() +class TestQueries(CrudEndpoint): -def test_queries_api_validate(api): - response = api.queries.get() + OBJECT_ID_NAME = "queryId" + OBJECT_TYPE = QueriesAPI - if len(response) > 0: - query = random.choice(response["data"]) - - response = api.queries.validate(evaluator_id=query["evaluatorId"], - query_text=query["queryText"]) + def test_api_get_by_id(self, api_object): + self._get_object_classifier_test(api_object, "id", self.OBJECT_ID_NAME) + def test_queries_api_execute_by_id(self, api_object, query): + start_time, end_time = self._get_start_end_times() + response = api_object.execute_by_id( + query_id=query["queryId"], + arguments={ + "StartTimeRange": start_time, + "EndTimeRange": end_time, + } + ) assert "data" in response.keys() + def test_queries_api_validate(self, api_object, query): + response = api_object.validate(evaluator_id=query["evaluatorId"], query_text=query["queryText"]) + assert "data" in response.keys() -def test_queries_api_delete(api): - response = api.queries.delete(query_id=RANDOM_TEXT) - - assert response.status_code == 204 + def test_api_search(self): + pass diff --git a/tests/api/v2/test_report_rules.py b/tests/api/v2/test_report_rules.py index 2049260..2e74077 100644 --- a/tests/api/v2/test_report_rules.py +++ b/tests/api/v2/test_report_rules.py @@ -3,133 +3,52 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string - import pytest from laceworksdk.api.v2.report_rules import ReportRulesAPI - -REPORT_RULE_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_report_rules_api_object_creation(api): - assert isinstance(api.report_rules, ReportRulesAPI) - - -def test_report_rules_api_env_object_creation(api_env): - assert isinstance(api_env.report_rules, ReportRulesAPI) - - -def test_report_rules_api_get(api): - response = api.report_rules.get() - assert "data" in response.keys() - - -@pytest.mark.flaky_test -def test_report_rules_api_create(api): - - response = api.alert_channels.search( - query_data={ - "filters": [ - { - "expression": "eq", - "field": "type", - "value": "EmailUser" - } - ], - "returns": [ - "intgGuid" - ] - } - ) - alert_channel_guid = response["data"][0]["intgGuid"] +@pytest.fixture(scope="module") +def api_object(api): + return api.report_rules - response = api.resource_groups.search( - query_data={ - "filters": [ - { - "expression": "eq", - "field": "resourceType", - "value": "AWS" - } - ], - "returns": [ - "resourceGuid" - ] - } - ) - resource_group_guid = response["data"][0]["resourceGuid"] - response = api.report_rules.create( - type="Report", - filters={ - "name": f"Test Report Rule {RANDOM_TEXT}", - "description": f"Test Report Rule Description {RANDOM_TEXT}", +@pytest.fixture(scope="module") +def api_object_create_body(random_text, email_alert_channel_guid, aws_resource_group_guid): + return { + "type": "Report", + "filters": { + "name": f"Test Report Rule {random_text}", + "description": f"Test Report Rule Description {random_text}", "enabled": 1, - "resourceGroups": [resource_group_guid], + "resourceGroups": [aws_resource_group_guid], "severity": [1, 2, 3] }, - intg_guid_list=[alert_channel_guid], - report_notification_types={ + "intg_guid_list": [email_alert_channel_guid], + "report_notification_types": { "awsComplianceEvents": True, "awsCisS3": True } - ) - - assert "data" in response.keys() - - global REPORT_RULE_GUID - REPORT_RULE_GUID = response["data"]["mcGuid"] - + } -@pytest.mark.flaky_test -def test_report_rules_api_get_by_guid(api): - assert REPORT_RULE_GUID is not None - if REPORT_RULE_GUID: - response = api.report_rules.get_by_guid(guid=REPORT_RULE_GUID) - - assert "data" in response.keys() - assert response["data"]["mcGuid"] == REPORT_RULE_GUID - - -def test_report_rules_api_search(api): - response = api.report_rules.search(query_data={ - "filters": [ - { - "expression": "ilike", - "field": "filters.name", - "value": "test%" - } - ], - "returns": [ - "mcGuid" - ] - }) - assert "data" in response.keys() +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "filters": { + "name": f"Test Report Rule {random_text} (Updated)", + "enabled": False + } + } -@pytest.mark.flaky_test -def test_report_rules_api_update(api): - assert REPORT_RULE_GUID is not None - if REPORT_RULE_GUID: - response = api.report_rules.update( - REPORT_RULE_GUID, - filters={ - "name": f"Test Report Rule {RANDOM_TEXT} (Updated)", - "enabled": False - } - ) - assert "data" in response.keys() +class TestAlertRules(CrudEndpoint): + OBJECT_ID_NAME = "mcGuid" + OBJECT_TYPE = ReportRulesAPI -@pytest.mark.flaky_test -def test_report_rules_api_delete(api): - assert REPORT_RULE_GUID is not None - if REPORT_RULE_GUID: - response = api.report_rules.delete(REPORT_RULE_GUID) - assert response.status_code == 204 + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) diff --git a/tests/api/v2/test_resource_groups.py b/tests/api/v2/test_resource_groups.py index e334f2b..f5030fd 100644 --- a/tests/api/v2/test_resource_groups.py +++ b/tests/api/v2/test_resource_groups.py @@ -3,91 +3,49 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string +import pytest from laceworksdk.api.v2.resource_groups import ResourceGroupsAPI - -RESOURCE_GROUP_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_resource_groups_api_object_creation(api): - assert isinstance(api.resource_groups, ResourceGroupsAPI) - - -def test_resource_groups_api_env_object_creation(api_env): - assert isinstance(api_env.resource_groups, ResourceGroupsAPI) - +@pytest.fixture(scope="module") +def api_object(api): + return api.resource_groups -def test_resource_groups_api_get(api): - response = api.resource_groups.get() - assert "data" in response.keys() - -def test_resource_groups_api_create(api): - response = api.resource_groups.create( - name=f"AWS Test {RANDOM_TEXT}", - type="AWS", - enabled=True, - props={ - "description": f"Test Description {RANDOM_TEXT}", - "accountIds": [123456789] +@pytest.fixture(scope="module") +def api_object_create_body(random_text): + return { + "resource_name": f"AWS Test {random_text}", + "resource_type": "AWS", + "enabled": True, + "props": { + "description": f"Test Description {random_text}", + "accountIds": [123456789012] } - ) - - assert "data" in response.keys() - - global RESOURCE_GROUP_GUID - RESOURCE_GROUP_GUID = response["data"]["resourceGuid"] + } -def test_resource_groups_api_get_by_guid(api): - assert RESOURCE_GROUP_GUID is not None - if RESOURCE_GROUP_GUID: - response = api.resource_groups.get_by_guid(guid=RESOURCE_GROUP_GUID) - - assert "data" in response.keys() - assert response["data"]["resourceGuid"] == RESOURCE_GROUP_GUID - - -def test_resource_groups_api_search(api): - response = api.resource_groups.search(query_data={ - "filters": [ - { - "expression": "eq", - "field": "resourceType", - "value": "AWS" - } - ], - "returns": [ - "resourceGuid" - ] - }) - assert "data" in response.keys() - - -def test_resource_groups_api_update(api): - assert RESOURCE_GROUP_GUID is not None - if RESOURCE_GROUP_GUID: +@pytest.fixture(scope="module") +def api_object_update_body(random_text): + return { + "resource_name": f"AWS Test {random_text} (Updated)", + "enabled": 0, + "props": { + "description": f"Test Description {random_text} (Updated)", + "accountIds": [123456789012] + } + } - response = api.resource_groups.update( - RESOURCE_GROUP_GUID, - name=f"AWS Test {RANDOM_TEXT} (Updated)", - enabled=0, - props={ - "description": f"Test Description {RANDOM_TEXT} (Updated)", - "accountIds": [123456789] - } - ) - assert "data" in response.keys() +class TestAlertRules(CrudEndpoint): + OBJECT_ID_NAME = "resourceGuid" + OBJECT_TYPE = ResourceGroupsAPI + OBJECT_PARAM_EXCEPTIONS = ["props"] -def test_resource_groups_api_delete(api): - assert RESOURCE_GROUP_GUID is not None - if RESOURCE_GROUP_GUID: - response = api.resource_groups.delete(RESOURCE_GROUP_GUID) - assert response.status_code == 204 + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) diff --git a/tests/api/v2/test_schemas.py b/tests/api/v2/test_schemas.py index a767880..8e9221d 100644 --- a/tests/api/v2/test_schemas.py +++ b/tests/api/v2/test_schemas.py @@ -3,52 +3,55 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ +import pytest + from laceworksdk.api.v2.schemas import SchemasAPI +from tests.api.test_base_endpoint import BaseEndpoint # Tests -def test_schemas_api_object_creation(api): - assert isinstance(api.schemas, SchemasAPI) - +@pytest.fixture(scope="module") +def api_object(api): + return api.schemas -def test_schemas_api_env_object_creation(api_env): - assert isinstance(api_env.schemas, SchemasAPI) +class TestSchemas(BaseEndpoint): -def test_schemas_api_get(api): - response = api.schemas.get() - assert len(response) > 0 + OBJECT_ID_NAME = "name" + OBJECT_TYPE = SchemasAPI + def test_schemas_api_get(self, api_object): + response = api_object.get() + assert len(response) > 0 -def test_schemas_api_get_type_schema(api): - response = api.schemas.get() + def test_schemas_api_get_type_schema(self, api_object): + response = api_object.get() - for schema_type in response: - response = api.schemas.get(type=schema_type) + for schema_type in response: + response = api_object.get(type=schema_type) - if type(response) is dict: - if len(response) > 0: - if "oneOf" in response.keys(): - for schema in response["oneOf"]: - assert "properties" in schema.keys() + if type(response) is dict: + if len(response) > 0: + if "oneOf" in response.keys(): + for schema in response["oneOf"]: + assert "properties" in schema.keys() + else: + assert "properties" in response.keys() else: - assert "properties" in response.keys() - else: + assert True + elif type(response) is list: assert True - elif type(response) is list: - assert True - else: - assert False - + else: + assert False -def test_schemas_api_get_subtype_schema(api): - type = "AlertChannels" - response = api.schemas.get(type=type) + def test_schemas_api_get_subtype_schema(self, api_object): + type = "AlertChannels" + response = api_object.get(type=type) - for subtype_schema in response["oneOf"]: + for subtype_schema in response["oneOf"]: - subtype = subtype_schema["properties"]["type"]["enum"][0] + subtype = subtype_schema["properties"]["type"]["enum"][0] - response = api.schemas.get_by_subtype(type=type, subtype=subtype) - assert "properties" in response.keys() + response = api_object.get_by_subtype(type=type, subtype=subtype) + assert "properties" in response.keys() diff --git a/tests/api/v2/test_team_members.py b/tests/api/v2/test_team_members.py index 510bf28..02802d9 100644 --- a/tests/api/v2/test_team_members.py +++ b/tests/api/v2/test_team_members.py @@ -3,87 +3,44 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ -import random -import string +import pytest from laceworksdk.api.v2.team_members import TeamMembersAPI - -TEAM_MEMBER_GUID = None -RANDOM_TEXT = "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(8)) +from tests.api.test_crud_endpoint import CrudEndpoint # Tests -def test_team_members_api_object_creation(api): - assert isinstance(api.team_members, TeamMembersAPI) - - -def test_team_members_api_env_object_creation(api_env): - assert isinstance(api_env.team_members, TeamMembersAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.team_members -def test_team_members_api_get(api): - response = api.team_members.get() - assert "data" in response.keys() - - -def test_team_members_api_create(api): - response = api.team_members.create( - user_name=f"{RANDOM_TEXT}@lacework.net", - props={ +@pytest.fixture(scope="module") +def api_object_create_body(random_text): + return { + "user_name": f"{random_text.lower()}@lacework.net", + "props": { "firstName": "John", "lastName": "Doe", "company": "Lacework", "accountAdmin": True }, - user_enabled=True - ) - - assert "data" in response.keys() - - global TEAM_MEMBER_GUID - TEAM_MEMBER_GUID = response["data"]["userGuid"] - - -def test_team_members_api_get_by_guid(api): - assert TEAM_MEMBER_GUID is not None - if TEAM_MEMBER_GUID: - response = api.team_members.get_by_guid(guid=TEAM_MEMBER_GUID) - - assert "data" in response.keys() - assert response["data"]["userGuid"] == TEAM_MEMBER_GUID - + "user_enabled": True + } -def test_team_members_api_search(api): - response = api.team_members.search(query_data={ - "filters": [ - { - "expression": "eq", - "field": "userName", - "value": f"{RANDOM_TEXT}@lacework.net" - } - ], - "returns": [ - "userGuid" - ] - }) - assert "data" in response.keys() - assert len(response["data"]) == 1 +@pytest.fixture(scope="module") +def api_object_update_body(): + return { + "user_enabled": False + } -def test_team_members_api_update(api): - assert TEAM_MEMBER_GUID is not None - if TEAM_MEMBER_GUID: - response = api.team_members.update( - TEAM_MEMBER_GUID, - user_enabled=False - ) - assert "data" in response.keys() +class TestTeamMembers(CrudEndpoint): + OBJECT_ID_NAME = "userGuid" + OBJECT_TYPE = TeamMembersAPI -def test_team_members_api_delete(api): - assert TEAM_MEMBER_GUID is not None - if TEAM_MEMBER_GUID: - response = api.team_members.delete(TEAM_MEMBER_GUID) - assert response.status_code == 204 + def test_api_get_by_guid(self, api_object): + self._get_object_classifier_test(api_object, "guid", self.OBJECT_ID_NAME) diff --git a/tests/api/v2/test_user_profile.py b/tests/api/v2/test_user_profile.py index b548e6b..94deae5 100644 --- a/tests/api/v2/test_user_profile.py +++ b/tests/api/v2/test_user_profile.py @@ -3,19 +3,33 @@ Test suite for the community-developed Python SDK for interacting with Lacework APIs. """ +import pytest + from laceworksdk.api.v2.user_profile import UserProfileAPI +from tests.api.test_base_endpoint import BaseEndpoint # Tests -def test_user_profile_api_object_creation(api): - assert isinstance(api.user_profile, UserProfileAPI) +@pytest.fixture(scope="module") +def api_object(api): + return api.user_profile + +class TestUserProfile(BaseEndpoint): -def test_user_profile_api_env_object_creation(api_env): - assert isinstance(api_env.user_profile, UserProfileAPI) + OBJECT_TYPE = UserProfileAPI + def test_api_get(self, api_object): + response = api_object.get() + keys = set([ + "username", + "orgAccount", + "url", + "orgAdmin", + "orgUser", + "accounts" + ]) -def test_user_profile_api_get(api): - response = api.user_profile.get() - assert len(response) > 0 + for item in response["data"]: + assert keys.issubset(item.keys()) diff --git a/tests/api/v2/test_vulnerabilities.py b/tests/api/v2/test_vulnerabilities.py index c6520ab..6c5d259 100644 --- a/tests/api/v2/test_vulnerabilities.py +++ b/tests/api/v2/test_vulnerabilities.py @@ -22,7 +22,8 @@ def api_object(api): class TestVulnerabilitesEndpoint(SearchEndpoint): - BASE_OBJECT_TYPE = VulnerabilitiesAPI + + OBJECT_TYPE = VulnerabilitiesAPI OBJECT_MAP = { "containers": ContainerVulnerabilitiesAPI, "hosts": HostVulnerabilitiesAPI From 83880bde049204413cd9f8d1898885e4ef733d95 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Thu, 27 Jan 2022 22:39:58 -0500 Subject: [PATCH 28/30] tests: fixed dependency issue with tests --- .github/workflows/python-test-flaky.yml | 1 + .github/workflows/python-test.yml | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-test-flaky.yml b/.github/workflows/python-test-flaky.yml index fe8d089..66cc090 100644 --- a/.github/workflows/python-test-flaky.yml +++ b/.github/workflows/python-test-flaky.yml @@ -26,6 +26,7 @@ jobs: python -m pip install --upgrade pip python -m pip install flake8 flake8-quotes pytest pytest-rerunfailures if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi if [ -f jupyter/requirements.txt ]; then pip install -r jupyter/requirements.txt; fi - name: Run setup.py diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 895a2f5..86eba3e 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9"] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 @@ -25,8 +25,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 flake8-quotes pytest pytest-rerunfailures + python -m pip install flake8 flake8-quotes pytest pytest-lazy-fixture pytest-rerunfailures if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi if [ -f jupyter/requirements.txt ]; then pip install -r jupyter/requirements.txt; fi - name: Lint with flake8 From 157b036e9c0cd1f6d4d4f4b3475bcbeb5a63e24a Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Mon, 31 Jan 2022 10:08:22 -0500 Subject: [PATCH 29/30] fix: modified error logging for 'nextPage' parsing --- laceworksdk/http_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/laceworksdk/http_session.py b/laceworksdk/http_session.py index 712b536..1b63fec 100644 --- a/laceworksdk/http_session.py +++ b/laceworksdk/http_session.py @@ -298,7 +298,8 @@ def get_pages(self, uri, params=None, **kwargs): response_json = response.json() next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage") except json.JSONDecodeError as e: - logger.error(f"Failed to decode response from Lacework as JSON: {e}\nResponse text: {response.text}") + logger.error(f"Failed to decode response from Lacework as JSON.", exc_info=True) + logger.debug(f"Response text: {response.text}") next_page = None if next_page: From 1624ff8e7aabbb4cd7517f317fd074216608fa08 Mon Sep 17 00:00:00 2001 From: Alan Nix Date: Mon, 31 Jan 2022 10:08:56 -0500 Subject: [PATCH 30/30] refactor: changed LaceworksdkException to LaceworkSDKException --- laceworksdk/__init__.py | 2 +- laceworksdk/exceptions.py | 6 +++--- tests/test_laceworksdk.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/laceworksdk/__init__.py b/laceworksdk/__init__.py index a2d2e1d..388bd38 100644 --- a/laceworksdk/__init__.py +++ b/laceworksdk/__init__.py @@ -8,7 +8,7 @@ import logging from .api import LaceworkClient # noqa: F401 -from .exceptions import ApiError, LaceworksdkException # noqa: F401 +from .exceptions import ApiError, LaceworkSDKException # noqa: F401 # Initialize Package Logging logger = logging.getLogger(__name__) diff --git a/laceworksdk/exceptions.py b/laceworksdk/exceptions.py index f70116a..5ce97a1 100644 --- a/laceworksdk/exceptions.py +++ b/laceworksdk/exceptions.py @@ -10,14 +10,14 @@ logger = logging.getLogger(__name__) -class LaceworksdkException(Exception): +class LaceworkSDKException(Exception): """ Base class for all lacework package exceptions. """ pass -class ApiError(LaceworksdkException): +class ApiError(LaceworkSDKException): """ Errors returned in response to requests sent to the Lacework APIs. Several data attributes are available for inspection. @@ -72,7 +72,7 @@ def __repr__(self): ) -class MalformedResponse(LaceworksdkException): +class MalformedResponse(LaceworkSDKException): """Raised when a malformed response is received from Lacework.""" pass diff --git a/tests/test_laceworksdk.py b/tests/test_laceworksdk.py index dda09dd..1dbbb2b 100644 --- a/tests/test_laceworksdk.py +++ b/tests/test_laceworksdk.py @@ -17,4 +17,4 @@ def test_package_contents(self): # Lacework Exceptions assert hasattr(laceworksdk, "ApiError") - assert hasattr(laceworksdk, "LaceworksdkException") + assert hasattr(laceworksdk, "LaceworkSDKException")