laceworksdk.http_session

HttpSession class for package HTTP functions.

  1# -*- coding: utf-8 -*-
  2"""
  3HttpSession class for package HTTP functions.
  4"""
  5
  6import json
  7import logging
  8import requests
  9
 10from datetime import datetime, timezone
 11from requests.adapters import HTTPAdapter
 12from urllib3.util.retry import Retry
 13
 14from laceworksdk import version
 15from laceworksdk.config import (
 16    DEFAULT_BASE_DOMAIN,
 17    DEFAULT_ACCESS_TOKEN_EXPIRATION,
 18    DEFAULT_SUCCESS_RESPONSE_CODES,
 19    RATE_LIMIT_RESPONSE_CODE
 20)
 21from laceworksdk.exceptions import ApiError, MalformedResponse, RateLimitError
 22
 23logger = logging.getLogger(__name__)
 24
 25
 26class HttpSession:
 27    """
 28    Package HttpSession class.
 29    """
 30
 31    _access_token = None
 32    _access_token_expiry = None
 33
 34    def __init__(self, account, subaccount, api_key, api_secret, base_domain):
 35        """
 36        Initializes the HttpSession object.
 37
 38        :param account: a Lacework Account name
 39        :param subaccount: a Lacework Sub-account name
 40        :param api_key: a Lacework API Key
 41        :param api_secret: a Lacework API Secret
 42        :param base_domain: a Lacework Domain (defaults to "lacework.net")
 43
 44        :return HttpSession object.
 45        """
 46
 47        super().__init__()
 48
 49        # Create a requests session
 50        self._session = self._retry_session()
 51
 52        # Set the base parameters
 53        self._api_key = api_key
 54        self._api_secret = api_secret
 55        self._base_domain = base_domain or DEFAULT_BASE_DOMAIN
 56
 57        domain_string = f".{self._base_domain}"
 58        if account.endswith(domain_string):
 59            account = account[:-len(domain_string)]
 60
 61        self._base_url = f"https://{account}.{self._base_domain}"
 62        self._subaccount = subaccount
 63        self._org_level_access = False
 64
 65        # Get an access token
 66        self._check_access_token()
 67
 68    def _retry_session(self,
 69                       retries=3,
 70                       backoff_factor=0.3,
 71                       status_forcelist=(500, 502, 503, 504),
 72                       allowed_methods=None):
 73        """
 74        A method to set up automatic retries on HTTP requests that fail.
 75        """
 76
 77        # Create a new requests session
 78        session = requests.Session()
 79
 80        # Establish the retry criteria
 81        retry_strategy = Retry(
 82            total=retries,
 83            backoff_factor=backoff_factor,
 84            status_forcelist=status_forcelist,
 85            allowed_methods=allowed_methods,
 86            raise_on_status=False
 87        )
 88
 89        # Build the adapter with the retry criteria
 90        adapter = HTTPAdapter(max_retries=retry_strategy)
 91
 92        # Bind the adapter to HTTP/HTTPS calls
 93        session.mount("http://", adapter)
 94        session.mount("https://", adapter)
 95
 96        return session
 97
 98    def _check_access_token(self):
 99        """
100        A method to check the validity of the access token.
101        """
102
103        if self._access_token is None or self._access_token_expiry < datetime.now(timezone.utc):
104
105            response = self._get_access_token()
106
107            # Parse and restructure the returned date (necessary for Python 3.6)
108            expiry_date = response.json()["expiresAt"].replace("Z", "+0000")
109
110            # Update the access token and expiration
111            self._access_token_expiry = datetime.strptime(expiry_date, "%Y-%m-%dT%H:%M:%S.%f%z")
112            self._access_token = response.json()["token"]
113
114    def _check_response_code(self, response, expected_response_codes):
115        """
116        Check the requests.response.status_code to make sure it's one that we expected.
117        """
118        if response.status_code in expected_response_codes:
119            pass
120        elif response.status_code == RATE_LIMIT_RESPONSE_CODE:
121            raise RateLimitError(response)
122        else:
123            raise ApiError(response)
124
125    def _print_debug_response(self, response):
126        """
127        Print the debug logging, based on the returned content type.
128        """
129
130        logger.debug(response.headers)
131
132        # If it's supposed to be a JSON response, parse and log, otherwise, log the raw text
133        if "application/json" in response.headers.get("Content-Type", "").lower():
134            try:
135                if response.status_code != 204:
136                    logger.debug(json.dumps(response.json(), indent=2))
137                else:
138                    logger.debug("204 No Content Returned")
139            except ValueError:
140                logger.warning("Error parsing JSON response body")
141        else:
142            logger.debug(response.text)
143
144    def _get_access_token(self):
145        """
146        A method to fetch a new access token from Lacework.
147
148        :return requests response
149        """
150
151        logger.info("Creating Access Token in Lacework...")
152
153        uri = f"{self._base_url}/api/v2/access/tokens"
154
155        # Build the access token request headers
156        headers = {
157            "X-LW-UAKS": self._api_secret,
158            "Content-Type": "application/json",
159            "User-Agent": f"laceworksdk-python-client/{version}"
160        }
161
162        # Build the access token request data
163        data = {
164            "keyId": self._api_key,
165            "expiryTime": DEFAULT_ACCESS_TOKEN_EXPIRATION
166        }
167
168        response = None
169
170        try:
171            response = self._session.post(uri, json=data, headers=headers)
172
173            # Validate the response
174            self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES)
175
176            self._print_debug_response(response)
177
178        except Exception:
179            if response:
180                raise ApiError(response)
181
182            logger.error("Call to _get_access_token() returned no response.")
183            raise
184
185        return response
186
187    def _get_request_headers(self, org_access=False):
188        """
189        A method to build the HTTP request headers for Lacework.
190
191        :param org_access: boolean representing whether the request should be performed at the Organization level
192        """
193
194        # Build the request headers
195        headers = self._session.headers
196
197        headers["Authorization"] = f"Bearer {self._access_token}"
198        headers["Org-Access"] = "true" if self._org_level_access or org_access else "false"
199        headers["User-Agent"] = f"laceworksdk-python-client/{version}"
200
201        if self._subaccount:
202            headers["Account-Name"] = self._subaccount
203
204        logger.debug("Request headers: \n" + json.dumps(dict(headers), indent=2))
205
206        return headers
207
208    def _request(self, method, uri, **kwargs):
209        """
210        A method to abstract building requests to Lacework.
211
212        :param method: string representing the HTTP request method ("GET", "POST", ...)
213        :param uri: string representing the URI of the API endpoint
214        :param kwargs: passed on to the requests package
215
216        :return: response json
217
218        :raises: ApiError if anything but expected response code is returned
219        """
220
221        self._check_access_token()
222
223        # Strip the protocol/host if provided
224        domain_begin = uri.find(self._base_domain)
225        if domain_begin >= 0:
226            domain_end = domain_begin + len(self._base_domain)
227            uri = uri[domain_end:]
228
229        uri = f"{self._base_url}{uri}"
230
231        logger.info(f"{method} request to URI: {uri}")
232
233        # Check for 'org' - if True, make an organization-level API call
234        # TODO: Remove this on v1.0 release - this is done for back compat
235        org = kwargs.pop("org", None)
236        headers = self._get_request_headers(org_access=org)
237
238        # Check for 'data' or 'json'
239        data = kwargs.get("data", "")
240        json = kwargs.get("json", "")
241        if data or json:
242            logger.debug(f"{method} request data:\nData: {data}\nJSON: {json}")
243
244        # TODO: Remove this on v1.0 release - this is done for back compat
245        if data and not json:
246            kwargs["json"] = data
247            kwargs.pop("data")
248
249        # Make the HTTP request to the API endpoint
250        response = self._session.request(method, uri, headers=headers, **kwargs)
251
252        # Validate the response
253        self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES)
254
255        self._print_debug_response(response)
256
257        # Fix for when Lacework returns a 204 with no data on searches
258        if method != "DELETE" and response.status_code == 204:
259            try:
260                response.json()
261            except Exception:
262                response._content = b'{"data": []}'
263
264        return response
265
266    def get(self, uri, params=None, **kwargs):
267        """
268        A method to build a GET request to interact with Lacework.
269
270        :param uri: uri to send the HTTP GET request to
271        :param params: dict of parameters for the HTTP request
272        :param kwargs: passed on to the requests package
273
274        :return: response json
275
276        :raises: ApiError if anything but expected response code is returned
277        """
278
279        # Perform a GET request
280        response = self._request("GET", uri, params=params, **kwargs)
281
282        return response
283
284    def get_pages(self, uri, params=None, **kwargs):
285        """
286        A method to build a GET request that yields pages of data returned by Lacework.
287
288        :param uri: uri to send the initial HTTP GET request to
289        :param params: dict of parameters for the HTTP request
290        :param kwargs: passed on to the requests package
291
292        :return: a generator that yields pages of data
293
294        :raises: ApiError if anything but expected response code is returned
295        """
296
297        response = self.get(uri, params=params, **kwargs)
298
299        while True:
300            yield response
301
302            try:
303                response_json = response.json()
304                next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage")
305            except json.JSONDecodeError:
306                logger.error("Failed to decode response from Lacework as JSON.", exc_info=True)
307                logger.debug(f"Response text: {response.text}")
308                next_page = None
309
310            if next_page:
311                response = self.get(next_page, params=params, **kwargs)
312            else:
313                break
314
315    def get_data_items(self, uri, params=None, **kwargs):
316        """
317        A method to build a GET request that yields individual objects as returned by Lacework.
318
319        :param uri: uri to send the initial HTTP GET request to
320        :param params: dict of parameters for the HTTP request
321        :param kwargs: passed on to the requests package
322
323        :return: a generator that yields individual objects from pages of data
324
325        :raises: ApiError if anything but expected response code is returned
326        :raises: MalformedResponse if the returned response does not contain a
327                top-level dictionary with an "data" key.
328        """
329
330        # Get generator for pages of JSON data
331        pages = self.get_pages(uri, params=params, **kwargs)
332
333        for page in pages:
334            page = page.json()
335            assert isinstance(page, dict)
336
337            items = page.get("data")
338
339            if items is None:
340                error_message = f"'data' key not found in JSON data:\n{page}"
341                raise MalformedResponse(error_message)
342
343            for item in items:
344                yield item
345
346    def patch(self, uri, data=None, json=None, **kwargs):
347        """
348        A method to build a PATCH request to interact with Lacework.
349
350        :param uri: uri to send the HTTP POST request to
351        :param data: data to be sent in the body of the request
352        :param json: data to be sent in JSON format in the body of the request
353        :param kwargs: passed on to the requests package
354
355        :return: response json
356
357        :raises: ApiError if anything but expected response code is returned
358        """
359
360        # Perform a PATCH request
361        response = self._request("PATCH", uri, data=data, json=json, **kwargs)
362
363        return response
364
365    def post(self, uri, data=None, json=None, **kwargs):
366        """
367        A method to build a POST request to interact with Lacework.
368
369        :param uri: uri to send the HTTP POST request to
370        :param data: data to be sent in the body of the request
371        :param json: data to be sent in JSON format in the body of the request
372        :param kwargs: passed on to the requests package
373
374        :return: response json
375
376        :raises: ApiError if anything but expected response code is returned
377        """
378
379        # Perform a POST request
380        response = self._request("POST", uri, data=data, json=json, **kwargs)
381
382        return response
383
384    def put(self, uri, data=None, json=None, **kwargs):
385        """
386        A method to build a PUT request to interact with Lacework.
387
388        :param uri: uri to send the HTTP POST request to
389        :param data: data to be sent in the body of the request
390        :param json: data to be sent in JSON format in the body of the request
391        :param kwargs: passed on to the requests package
392
393        :return: response json
394
395        :raises: ApiError if anything but expected response code is returned
396        """
397
398        # Perform a PUT request
399        response = self._request("PUT", uri, data=data, json=json, **kwargs)
400
401        return response
402
403    def delete(self, uri, data=None, json=None, **kwargs):
404        """
405        A method to build a DELETE request to interact with Lacework.
406
407        :param uri: uri to send the http DELETE request to
408        :param data: data to be sent in the body of the request
409        :param json: data to be sent in JSON format in the body of the request
410        :param kwargs: passed on to the requests package
411
412        :response: reponse json
413
414        :raises: ApiError if anything but expected response code is returned
415        """
416
417        # Perform a DELETE request
418        response = self._request("DELETE", uri, data=data, json=json, **kwargs)
419
420        return response
class HttpSession:
 27class HttpSession:
 28    """
 29    Package HttpSession class.
 30    """
 31
 32    _access_token = None
 33    _access_token_expiry = None
 34
 35    def __init__(self, account, subaccount, api_key, api_secret, base_domain):
 36        """
 37        Initializes the HttpSession object.
 38
 39        :param account: a Lacework Account name
 40        :param subaccount: a Lacework Sub-account name
 41        :param api_key: a Lacework API Key
 42        :param api_secret: a Lacework API Secret
 43        :param base_domain: a Lacework Domain (defaults to "lacework.net")
 44
 45        :return HttpSession object.
 46        """
 47
 48        super().__init__()
 49
 50        # Create a requests session
 51        self._session = self._retry_session()
 52
 53        # Set the base parameters
 54        self._api_key = api_key
 55        self._api_secret = api_secret
 56        self._base_domain = base_domain or DEFAULT_BASE_DOMAIN
 57
 58        domain_string = f".{self._base_domain}"
 59        if account.endswith(domain_string):
 60            account = account[:-len(domain_string)]
 61
 62        self._base_url = f"https://{account}.{self._base_domain}"
 63        self._subaccount = subaccount
 64        self._org_level_access = False
 65
 66        # Get an access token
 67        self._check_access_token()
 68
 69    def _retry_session(self,
 70                       retries=3,
 71                       backoff_factor=0.3,
 72                       status_forcelist=(500, 502, 503, 504),
 73                       allowed_methods=None):
 74        """
 75        A method to set up automatic retries on HTTP requests that fail.
 76        """
 77
 78        # Create a new requests session
 79        session = requests.Session()
 80
 81        # Establish the retry criteria
 82        retry_strategy = Retry(
 83            total=retries,
 84            backoff_factor=backoff_factor,
 85            status_forcelist=status_forcelist,
 86            allowed_methods=allowed_methods,
 87            raise_on_status=False
 88        )
 89
 90        # Build the adapter with the retry criteria
 91        adapter = HTTPAdapter(max_retries=retry_strategy)
 92
 93        # Bind the adapter to HTTP/HTTPS calls
 94        session.mount("http://", adapter)
 95        session.mount("https://", adapter)
 96
 97        return session
 98
 99    def _check_access_token(self):
100        """
101        A method to check the validity of the access token.
102        """
103
104        if self._access_token is None or self._access_token_expiry < datetime.now(timezone.utc):
105
106            response = self._get_access_token()
107
108            # Parse and restructure the returned date (necessary for Python 3.6)
109            expiry_date = response.json()["expiresAt"].replace("Z", "+0000")
110
111            # Update the access token and expiration
112            self._access_token_expiry = datetime.strptime(expiry_date, "%Y-%m-%dT%H:%M:%S.%f%z")
113            self._access_token = response.json()["token"]
114
115    def _check_response_code(self, response, expected_response_codes):
116        """
117        Check the requests.response.status_code to make sure it's one that we expected.
118        """
119        if response.status_code in expected_response_codes:
120            pass
121        elif response.status_code == RATE_LIMIT_RESPONSE_CODE:
122            raise RateLimitError(response)
123        else:
124            raise ApiError(response)
125
126    def _print_debug_response(self, response):
127        """
128        Print the debug logging, based on the returned content type.
129        """
130
131        logger.debug(response.headers)
132
133        # If it's supposed to be a JSON response, parse and log, otherwise, log the raw text
134        if "application/json" in response.headers.get("Content-Type", "").lower():
135            try:
136                if response.status_code != 204:
137                    logger.debug(json.dumps(response.json(), indent=2))
138                else:
139                    logger.debug("204 No Content Returned")
140            except ValueError:
141                logger.warning("Error parsing JSON response body")
142        else:
143            logger.debug(response.text)
144
145    def _get_access_token(self):
146        """
147        A method to fetch a new access token from Lacework.
148
149        :return requests response
150        """
151
152        logger.info("Creating Access Token in Lacework...")
153
154        uri = f"{self._base_url}/api/v2/access/tokens"
155
156        # Build the access token request headers
157        headers = {
158            "X-LW-UAKS": self._api_secret,
159            "Content-Type": "application/json",
160            "User-Agent": f"laceworksdk-python-client/{version}"
161        }
162
163        # Build the access token request data
164        data = {
165            "keyId": self._api_key,
166            "expiryTime": DEFAULT_ACCESS_TOKEN_EXPIRATION
167        }
168
169        response = None
170
171        try:
172            response = self._session.post(uri, json=data, headers=headers)
173
174            # Validate the response
175            self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES)
176
177            self._print_debug_response(response)
178
179        except Exception:
180            if response:
181                raise ApiError(response)
182
183            logger.error("Call to _get_access_token() returned no response.")
184            raise
185
186        return response
187
188    def _get_request_headers(self, org_access=False):
189        """
190        A method to build the HTTP request headers for Lacework.
191
192        :param org_access: boolean representing whether the request should be performed at the Organization level
193        """
194
195        # Build the request headers
196        headers = self._session.headers
197
198        headers["Authorization"] = f"Bearer {self._access_token}"
199        headers["Org-Access"] = "true" if self._org_level_access or org_access else "false"
200        headers["User-Agent"] = f"laceworksdk-python-client/{version}"
201
202        if self._subaccount:
203            headers["Account-Name"] = self._subaccount
204
205        logger.debug("Request headers: \n" + json.dumps(dict(headers), indent=2))
206
207        return headers
208
209    def _request(self, method, uri, **kwargs):
210        """
211        A method to abstract building requests to Lacework.
212
213        :param method: string representing the HTTP request method ("GET", "POST", ...)
214        :param uri: string representing the URI of the API endpoint
215        :param kwargs: passed on to the requests package
216
217        :return: response json
218
219        :raises: ApiError if anything but expected response code is returned
220        """
221
222        self._check_access_token()
223
224        # Strip the protocol/host if provided
225        domain_begin = uri.find(self._base_domain)
226        if domain_begin >= 0:
227            domain_end = domain_begin + len(self._base_domain)
228            uri = uri[domain_end:]
229
230        uri = f"{self._base_url}{uri}"
231
232        logger.info(f"{method} request to URI: {uri}")
233
234        # Check for 'org' - if True, make an organization-level API call
235        # TODO: Remove this on v1.0 release - this is done for back compat
236        org = kwargs.pop("org", None)
237        headers = self._get_request_headers(org_access=org)
238
239        # Check for 'data' or 'json'
240        data = kwargs.get("data", "")
241        json = kwargs.get("json", "")
242        if data or json:
243            logger.debug(f"{method} request data:\nData: {data}\nJSON: {json}")
244
245        # TODO: Remove this on v1.0 release - this is done for back compat
246        if data and not json:
247            kwargs["json"] = data
248            kwargs.pop("data")
249
250        # Make the HTTP request to the API endpoint
251        response = self._session.request(method, uri, headers=headers, **kwargs)
252
253        # Validate the response
254        self._check_response_code(response, DEFAULT_SUCCESS_RESPONSE_CODES)
255
256        self._print_debug_response(response)
257
258        # Fix for when Lacework returns a 204 with no data on searches
259        if method != "DELETE" and response.status_code == 204:
260            try:
261                response.json()
262            except Exception:
263                response._content = b'{"data": []}'
264
265        return response
266
267    def get(self, uri, params=None, **kwargs):
268        """
269        A method to build a GET request to interact with Lacework.
270
271        :param uri: uri to send the HTTP GET request to
272        :param params: dict of parameters for the HTTP request
273        :param kwargs: passed on to the requests package
274
275        :return: response json
276
277        :raises: ApiError if anything but expected response code is returned
278        """
279
280        # Perform a GET request
281        response = self._request("GET", uri, params=params, **kwargs)
282
283        return response
284
285    def get_pages(self, uri, params=None, **kwargs):
286        """
287        A method to build a GET request that yields pages of data returned by Lacework.
288
289        :param uri: uri to send the initial HTTP GET request to
290        :param params: dict of parameters for the HTTP request
291        :param kwargs: passed on to the requests package
292
293        :return: a generator that yields pages of data
294
295        :raises: ApiError if anything but expected response code is returned
296        """
297
298        response = self.get(uri, params=params, **kwargs)
299
300        while True:
301            yield response
302
303            try:
304                response_json = response.json()
305                next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage")
306            except json.JSONDecodeError:
307                logger.error("Failed to decode response from Lacework as JSON.", exc_info=True)
308                logger.debug(f"Response text: {response.text}")
309                next_page = None
310
311            if next_page:
312                response = self.get(next_page, params=params, **kwargs)
313            else:
314                break
315
316    def get_data_items(self, uri, params=None, **kwargs):
317        """
318        A method to build a GET request that yields individual objects as returned by Lacework.
319
320        :param uri: uri to send the initial HTTP GET request to
321        :param params: dict of parameters for the HTTP request
322        :param kwargs: passed on to the requests package
323
324        :return: a generator that yields individual objects from pages of data
325
326        :raises: ApiError if anything but expected response code is returned
327        :raises: MalformedResponse if the returned response does not contain a
328                top-level dictionary with an "data" key.
329        """
330
331        # Get generator for pages of JSON data
332        pages = self.get_pages(uri, params=params, **kwargs)
333
334        for page in pages:
335            page = page.json()
336            assert isinstance(page, dict)
337
338            items = page.get("data")
339
340            if items is None:
341                error_message = f"'data' key not found in JSON data:\n{page}"
342                raise MalformedResponse(error_message)
343
344            for item in items:
345                yield item
346
347    def patch(self, uri, data=None, json=None, **kwargs):
348        """
349        A method to build a PATCH request to interact with Lacework.
350
351        :param uri: uri to send the HTTP POST request to
352        :param data: data to be sent in the body of the request
353        :param json: data to be sent in JSON format in the body of the request
354        :param kwargs: passed on to the requests package
355
356        :return: response json
357
358        :raises: ApiError if anything but expected response code is returned
359        """
360
361        # Perform a PATCH request
362        response = self._request("PATCH", uri, data=data, json=json, **kwargs)
363
364        return response
365
366    def post(self, uri, data=None, json=None, **kwargs):
367        """
368        A method to build a POST request to interact with Lacework.
369
370        :param uri: uri to send the HTTP POST request to
371        :param data: data to be sent in the body of the request
372        :param json: data to be sent in JSON format in the body of the request
373        :param kwargs: passed on to the requests package
374
375        :return: response json
376
377        :raises: ApiError if anything but expected response code is returned
378        """
379
380        # Perform a POST request
381        response = self._request("POST", uri, data=data, json=json, **kwargs)
382
383        return response
384
385    def put(self, uri, data=None, json=None, **kwargs):
386        """
387        A method to build a PUT request to interact with Lacework.
388
389        :param uri: uri to send the HTTP POST request to
390        :param data: data to be sent in the body of the request
391        :param json: data to be sent in JSON format in the body of the request
392        :param kwargs: passed on to the requests package
393
394        :return: response json
395
396        :raises: ApiError if anything but expected response code is returned
397        """
398
399        # Perform a PUT request
400        response = self._request("PUT", uri, data=data, json=json, **kwargs)
401
402        return response
403
404    def delete(self, uri, data=None, json=None, **kwargs):
405        """
406        A method to build a DELETE request to interact with Lacework.
407
408        :param uri: uri to send the http DELETE request to
409        :param data: data to be sent in the body of the request
410        :param json: data to be sent in JSON format in the body of the request
411        :param kwargs: passed on to the requests package
412
413        :response: reponse json
414
415        :raises: ApiError if anything but expected response code is returned
416        """
417
418        # Perform a DELETE request
419        response = self._request("DELETE", uri, data=data, json=json, **kwargs)
420
421        return response

Package HttpSession class.

HttpSession(account, subaccount, api_key, api_secret, base_domain)
35    def __init__(self, account, subaccount, api_key, api_secret, base_domain):
36        """
37        Initializes the HttpSession object.
38
39        :param account: a Lacework Account name
40        :param subaccount: a Lacework Sub-account name
41        :param api_key: a Lacework API Key
42        :param api_secret: a Lacework API Secret
43        :param base_domain: a Lacework Domain (defaults to "lacework.net")
44
45        :return HttpSession object.
46        """
47
48        super().__init__()
49
50        # Create a requests session
51        self._session = self._retry_session()
52
53        # Set the base parameters
54        self._api_key = api_key
55        self._api_secret = api_secret
56        self._base_domain = base_domain or DEFAULT_BASE_DOMAIN
57
58        domain_string = f".{self._base_domain}"
59        if account.endswith(domain_string):
60            account = account[:-len(domain_string)]
61
62        self._base_url = f"https://{account}.{self._base_domain}"
63        self._subaccount = subaccount
64        self._org_level_access = False
65
66        # Get an access token
67        self._check_access_token()

Initializes the HttpSession object.

Parameters
  • account: a Lacework Account name
  • subaccount: a Lacework Sub-account name
  • api_key: a Lacework API Key
  • api_secret: a Lacework API Secret
  • base_domain: a Lacework Domain (defaults to "lacework.net")

:return HttpSession object.

def get(self, uri, params=None, **kwargs):
267    def get(self, uri, params=None, **kwargs):
268        """
269        A method to build a GET request to interact with Lacework.
270
271        :param uri: uri to send the HTTP GET request to
272        :param params: dict of parameters for the HTTP request
273        :param kwargs: passed on to the requests package
274
275        :return: response json
276
277        :raises: ApiError if anything but expected response code is returned
278        """
279
280        # Perform a GET request
281        response = self._request("GET", uri, params=params, **kwargs)
282
283        return response

A method to build a GET request to interact with Lacework.

Parameters
  • uri: uri to send the HTTP GET request to
  • params: dict of parameters for the HTTP request
  • kwargs: passed on to the requests package
Returns

response json

Raises
  • ApiError if anything but expected response code is returned
def get_pages(self, uri, params=None, **kwargs):
285    def get_pages(self, uri, params=None, **kwargs):
286        """
287        A method to build a GET request that yields pages of data returned by Lacework.
288
289        :param uri: uri to send the initial HTTP GET request to
290        :param params: dict of parameters for the HTTP request
291        :param kwargs: passed on to the requests package
292
293        :return: a generator that yields pages of data
294
295        :raises: ApiError if anything but expected response code is returned
296        """
297
298        response = self.get(uri, params=params, **kwargs)
299
300        while True:
301            yield response
302
303            try:
304                response_json = response.json()
305                next_page = response_json.get("paging", {}).get("urls", {}).get("nextPage")
306            except json.JSONDecodeError:
307                logger.error("Failed to decode response from Lacework as JSON.", exc_info=True)
308                logger.debug(f"Response text: {response.text}")
309                next_page = None
310
311            if next_page:
312                response = self.get(next_page, params=params, **kwargs)
313            else:
314                break

A method to build a GET request that yields pages of data returned by Lacework.

Parameters
  • uri: uri to send the initial HTTP GET request to
  • params: dict of parameters for the HTTP request
  • kwargs: passed on to the requests package
Returns

a generator that yields pages of data

Raises
  • ApiError if anything but expected response code is returned
def get_data_items(self, uri, params=None, **kwargs):
316    def get_data_items(self, uri, params=None, **kwargs):
317        """
318        A method to build a GET request that yields individual objects as returned by Lacework.
319
320        :param uri: uri to send the initial HTTP GET request to
321        :param params: dict of parameters for the HTTP request
322        :param kwargs: passed on to the requests package
323
324        :return: a generator that yields individual objects from pages of data
325
326        :raises: ApiError if anything but expected response code is returned
327        :raises: MalformedResponse if the returned response does not contain a
328                top-level dictionary with an "data" key.
329        """
330
331        # Get generator for pages of JSON data
332        pages = self.get_pages(uri, params=params, **kwargs)
333
334        for page in pages:
335            page = page.json()
336            assert isinstance(page, dict)
337
338            items = page.get("data")
339
340            if items is None:
341                error_message = f"'data' key not found in JSON data:\n{page}"
342                raise MalformedResponse(error_message)
343
344            for item in items:
345                yield item

A method to build a GET request that yields individual objects as returned by Lacework.

Parameters
  • uri: uri to send the initial HTTP GET request to
  • params: dict of parameters for the HTTP request
  • kwargs: passed on to the requests package
Returns

a generator that yields individual objects from pages of data

Raises
  • ApiError if anything but expected response code is returned
  • MalformedResponse if the returned response does not contain a top-level dictionary with an "data" key.
def patch(self, uri, data=None, json=None, **kwargs):
347    def patch(self, uri, data=None, json=None, **kwargs):
348        """
349        A method to build a PATCH request to interact with Lacework.
350
351        :param uri: uri to send the HTTP POST request to
352        :param data: data to be sent in the body of the request
353        :param json: data to be sent in JSON format in the body of the request
354        :param kwargs: passed on to the requests package
355
356        :return: response json
357
358        :raises: ApiError if anything but expected response code is returned
359        """
360
361        # Perform a PATCH request
362        response = self._request("PATCH", uri, data=data, json=json, **kwargs)
363
364        return response

A method to build a PATCH request to interact with Lacework.

Parameters
  • uri: uri to send the HTTP POST request to
  • data: data to be sent in the body of the request
  • json: data to be sent in JSON format in the body of the request
  • kwargs: passed on to the requests package
Returns

response json

Raises
  • ApiError if anything but expected response code is returned
def post(self, uri, data=None, json=None, **kwargs):
366    def post(self, uri, data=None, json=None, **kwargs):
367        """
368        A method to build a POST request to interact with Lacework.
369
370        :param uri: uri to send the HTTP POST request to
371        :param data: data to be sent in the body of the request
372        :param json: data to be sent in JSON format in the body of the request
373        :param kwargs: passed on to the requests package
374
375        :return: response json
376
377        :raises: ApiError if anything but expected response code is returned
378        """
379
380        # Perform a POST request
381        response = self._request("POST", uri, data=data, json=json, **kwargs)
382
383        return response

A method to build a POST request to interact with Lacework.

Parameters
  • uri: uri to send the HTTP POST request to
  • data: data to be sent in the body of the request
  • json: data to be sent in JSON format in the body of the request
  • kwargs: passed on to the requests package
Returns

response json

Raises
  • ApiError if anything but expected response code is returned
def put(self, uri, data=None, json=None, **kwargs):
385    def put(self, uri, data=None, json=None, **kwargs):
386        """
387        A method to build a PUT request to interact with Lacework.
388
389        :param uri: uri to send the HTTP POST request to
390        :param data: data to be sent in the body of the request
391        :param json: data to be sent in JSON format in the body of the request
392        :param kwargs: passed on to the requests package
393
394        :return: response json
395
396        :raises: ApiError if anything but expected response code is returned
397        """
398
399        # Perform a PUT request
400        response = self._request("PUT", uri, data=data, json=json, **kwargs)
401
402        return response

A method to build a PUT request to interact with Lacework.

Parameters
  • uri: uri to send the HTTP POST request to
  • data: data to be sent in the body of the request
  • json: data to be sent in JSON format in the body of the request
  • kwargs: passed on to the requests package
Returns

response json

Raises
  • ApiError if anything but expected response code is returned
def delete(self, uri, data=None, json=None, **kwargs):
404    def delete(self, uri, data=None, json=None, **kwargs):
405        """
406        A method to build a DELETE request to interact with Lacework.
407
408        :param uri: uri to send the http DELETE request to
409        :param data: data to be sent in the body of the request
410        :param json: data to be sent in JSON format in the body of the request
411        :param kwargs: passed on to the requests package
412
413        :response: reponse json
414
415        :raises: ApiError if anything but expected response code is returned
416        """
417
418        # Perform a DELETE request
419        response = self._request("DELETE", uri, data=data, json=json, **kwargs)
420
421        return response

A method to build a DELETE request to interact with Lacework.

Parameters
  • uri: uri to send the http DELETE request to
  • data: data to be sent in the body of the request
  • json: data to be sent in JSON format in the body of the request
  • kwargs: passed on to the requests package

:response: reponse json

Raises
  • ApiError if anything but expected response code is returned