From 7f656cc2abae13dedd45fa677bb0043e8c0a3d3e Mon Sep 17 00:00:00 2001 From: Tyler Walch Date: Thu, 18 Nov 2021 14:07:45 -0500 Subject: [PATCH 01/22] calling the J1 sync results API --- jupiterone/client.py | 73 ++++++- jupiterone/constants.py | 18 ++ tests/test_query.py | 411 +++++++++++++++++++++++++++++++++++----- 3 files changed, 444 insertions(+), 58 deletions(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index a9be725..d814acf 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -7,6 +7,7 @@ import requests from retrying import retry +from warnings import warn from jupiterone.errors import ( JupiterOneClientError, @@ -22,7 +23,8 @@ DELETE_ENTITY, UPDATE_ENTITY, CREATE_RELATIONSHIP, - DELETE_RELATIONSHIP + DELETE_RELATIONSHIP, + CURSOR_QUERY_V1 ) def retry_on_429(exc): @@ -105,25 +107,48 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: raise JupiterOneApiError(content.get('errors')) return response.json() - elif response.status_code in [429, 500]: + elif response.status_code in [429, 503]: raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded') else: content = json.loads(response._content) raise JupiterOneApiError('{}:{}'.format(response.status_code, content.get('error'))) - def query_v1(self, query: str, **kwargs) -> Dict: - """ Performs a V1 graph query + def _cursor_query(self, query: str, cursor: str = None, include_deleted: bool = False) -> Dict: + """ Performs a V1 graph query using cursor pagination args: query (str): Query text - skip (int): Skip entity count - limit (int): Limit entity count + cursor (str): A pagination cursor for the initial query include_deleted (bool): Include recently deleted entities in query/search """ - skip: int = kwargs.pop('skip', J1QL_SKIP_COUNT) - limit: int = kwargs.pop('limit', J1QL_LIMIT_COUNT) - include_deleted: bool = kwargs.pop('include_deleted', False) + results: List = [] + while True: + variables = { + 'query': query, + 'includeDeleted': include_deleted + } + + if cursor is not None: + variables['cursor'] = cursor + + response = self._execute_query(query=CURSOR_QUERY_V1, variables=variables) + data = response['data']['queryV1'] + + if 'vertices' in data and 'edges' in data: + return data + + results.extend(data) + + if 'cursor' in response['data']['queryV1']: + print('cursor', response['data']['queryV1']['cursor'], len(data), len(results)) + cursor = response['data']['queryV1']['cursor'] + else: + break + + return {'data': results} + + def _limit_and_skip_query(self, query: str, skip: int = J1QL_SKIP_COUNT, limit: int = J1QL_LIMIT_COUNT, include_deleted: bool = False) -> Dict: results: List = [] page: int = 0 @@ -152,6 +177,36 @@ def query_v1(self, query: str, **kwargs) -> Dict: return {'data': results} + def query_v1(self, query: str, **kwargs) -> Dict: + """ Performs a V1 graph query + args: + query (str): Query text + skip (int): Skip entity count + limit (int): Limit entity count + cursor (str): A pagination cursor for the initial query + include_deleted (bool): Include recently deleted entities in query/search + """ + uses_limit_and_skip: bool = 'skip' in kwargs.keys() or 'limit' in kwargs.keys() + skip: int = kwargs.pop('skip', J1QL_SKIP_COUNT) + limit: int = kwargs.pop('limit', J1QL_LIMIT_COUNT) + include_deleted: bool = kwargs.pop('include_deleted', False) + cursor: str = kwargs.pop('cursor', None) + + if uses_limit_and_skip: + warn('limit and skip pagination is no longer a recommended method for pagination. To read more about using cursors checkout the JupiterOne documentation: https://support.jupiterone.io/hc/en-us/articles/360022722094#entityandrelationshipqueries', DeprecationWarning, stacklevel=2) + return self._limit_and_skip_query( + query=query, + skip=skip, + limit=limit, + include_deleted=include_deleted + ) + else: + return self._cursor_query( + query=query, + cursor=cursor, + include_deleted=include_deleted + ) + def create_entity(self, **kwargs) -> Dict: """ Creates an entity in graph. It will also update an existing entity. diff --git a/jupiterone/constants.py b/jupiterone/constants.py index 7b58dad..d8d350c 100644 --- a/jupiterone/constants.py +++ b/jupiterone/constants.py @@ -6,6 +6,24 @@ queryV1(query: $query, variables: $variables, dryRun: $dryRun, includeDeleted: $includeDeleted) { type data + url + } + } +""" + +CURSOR_QUERY_V1 = """ + query J1QL_v2($query: String!, $variables: JSON, $flags: QueryV1Flags, $includeDeleted: Boolean, $cursor: String) { + queryV1( + query: $query + variables: $variables + flags: $flags + includeDeleted: $includeDeleted + cursor: $cursor + ) { + type + data + cursor + __typename } } """ diff --git a/tests/test_query.py b/tests/test_query.py index 9228d7f..f9f5860 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -1,60 +1,146 @@ import json import pytest import responses +from collections import Counter from jupiterone.client import JupiterOneClient -from jupiterone.constants import QUERY_V1 +from jupiterone.constants import QUERY_V1, DEFERRED_RESULTS_COMPLETED +from jupiterone.errors import JupiterOneApiError -def request_callback(request): - headers = { - 'Content-Type': 'application/json' - } +API_ENDPOINT = 'https://api.us.jupiterone.io/graphql' +STATE_FILE_ENDPOINT = 'https://api.us.jupiterone.io/state' +RESULTS_ENDPOINT = 'https://api.us.jupiterone.io/results' - response = { - 'data': { - 'queryV1': { - 'type': 'list', - 'data': [ - { - 'id': '1', - 'entity': { - '_rawDataHashes': '1', - '_integrationDefinitionId': '1', - '_integrationName': '1', - '_beginOn': 1580482083079, - 'displayName': 'host1', - '_class': ['Host'], - '_scope': 'aws_instance', - '_version': 1, - '_integrationClass': 'CSP', - '_accountId': 'testAccount', - '_id': '1', - '_key': 'key1', - '_type': ['aws_instance'], - '_deleted': False, - '_integrationInstanceId': '1', - '_integrationType': 'aws', - '_source': 'integration-managed', - '_createdOn': 1578093840019 - }, - 'properties': { - 'id': 'host1', - 'active': True - } - } - ] +def build_deferred_query_response(response_code: int = 200, url: str = STATE_FILE_ENDPOINT): + def request_callback(request): + headers = { + 'Content-Type': 'application/json' + } + + response = { + 'data': { + 'queryV1': { + 'url': url + } } } - } - return (200, headers, json.dumps(response)) + return (response_code, headers, json.dumps(response)) + return request_callback +def build_state_response(response_code: int = 200, status: str = DEFERRED_RESULTS_COMPLETED, url: str = RESULTS_ENDPOINT): + def request_callback(request): + headers = { + 'Content-Type': 'application/json' + } + + response = { + 'status': status, + 'url': url + } + return (response_code, headers, json.dumps(response)) + return request_callback + +def build_deferred_query_results(response_code: int = 200, cursor: str = None, max_pages: int = 1): + pages = Counter(requests=0) + + def request_callback(request): + headers = { + 'Content-Type': 'application/json' + } + + response = { + 'data': [ + { + 'id': '1', + 'entity': { + '_rawDataHashes': '1', + '_integrationDefinitionId': '1', + '_integrationName': '1', + '_beginOn': 1580482083079, + 'displayName': 'host1', + '_class': ['Host'], + '_scope': 'aws_instance', + '_version': 1, + '_integrationClass': 'CSP', + '_accountId': 'testAccount', + '_id': '1', + '_key': 'key1', + '_type': ['aws_instance'], + '_deleted': False, + '_integrationInstanceId': '1', + '_integrationType': 'aws', + '_source': 'integration-managed', + '_createdOn': 1578093840019 + }, + 'properties': { + 'id': 'host1', + 'active': True + } + } + ] + } + + if cursor is not None and pages.get('requests') < max_pages: + response['cursor'] = cursor + + pages.update(requests=1) + + return (response_code, headers, json.dumps(response)) + + return request_callback + +def build_results(response_code: int = 200): + def request_callback(request): + headers = { + 'Content-Type': 'application/json' + } + + response = { + 'data': { + 'queryV1': { + 'type': 'list', + 'data': [ + { + 'id': '1', + 'entity': { + '_rawDataHashes': '1', + '_integrationDefinitionId': '1', + '_integrationName': '1', + '_beginOn': 1580482083079, + 'displayName': 'host1', + '_class': ['Host'], + '_scope': 'aws_instance', + '_version': 1, + '_integrationClass': 'CSP', + '_accountId': 'testAccount', + '_id': '1', + '_key': 'key1', + '_type': ['aws_instance'], + '_deleted': False, + '_integrationInstanceId': '1', + '_integrationType': 'aws', + '_source': 'integration-managed', + '_createdOn': 1578093840019 + }, + 'properties': { + 'id': 'host1', + 'active': True + } + } + ] + } + } + } + return (response_code, headers, json.dumps(response)) + return request_callback + @responses.activate def test_execute_query(): responses.add_callback( responses.POST, 'https://api.us.jupiterone.io/graphql', - callback=request_callback, + callback=build_results(), content_type='application/json', ) @@ -73,30 +159,66 @@ def test_execute_query(): assert 'queryV1' in response['data'] assert len(response['data']['queryV1']['data']) == 1 assert type(response['data']['queryV1']['data']) == list - assert response['data']['queryV1']['data'][0]['entity']['_id'] == '1' + assert response['data']['queryV1']['data'][0]['entity']['_id'] == '1' @responses.activate -def test_query_v1(): +def test_limit_skip_query_v1(): responses.add_callback( responses.POST, 'https://api.us.jupiterone.io/graphql', - callback=request_callback, + callback=build_results(), content_type='application/json', ) j1 = JupiterOneClient(account='testAccount', token='testToken') query = "find Host with _id='1'" - response = j1.query_v1(query) + response = j1.query_v1( + query=query, + limit=250, + skip=0 + ) assert type(response) == dict assert len(response['data']) == 1 assert type(response['data']) == list assert response['data'][0]['entity']['_id'] == '1' +@responses.activate +def test_cursor_query_v1(): + + responses.add_callback( + responses.POST, API_ENDPOINT, + callback=build_deferred_query_response(url=STATE_FILE_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, STATE_FILE_ENDPOINT, + callback=build_state_response(url=RESULTS_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, RESULTS_ENDPOINT, + callback=build_deferred_query_results(cursor='cursor_value'), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1'" + + response = j1.query_v1( + query=query, + ) + + assert type(response) == dict + assert len(response['data']) == 2 + assert type(response['data']) == list + assert response['data'][0]['entity']['_id'] == '1' @responses.activate -def test_tree_query_v1(): +def test_limit_skip_tree_query_v1(): def request_callback(request): headers = { @@ -131,12 +253,203 @@ def request_callback(request): j1 = JupiterOneClient(account='testAccount', token='testToken') query = "find Host with _id='1' return tree" - response = j1.query_v1(query) + response = j1.query_v1( + query=query, + limit=250, + skip=0 + ) - assert type(response) == dict + assert type(response) == dict assert 'edges' in response assert 'vertices' in response assert type(response['edges']) == list assert type(response['vertices']) == list assert response['vertices'][0]['id'] == '1' - \ No newline at end of file + +@responses.activate +def test_cursor_tree_query_v1(): + + def request_callback(request): + headers = { + 'Content-Type': 'application/json' + } + + response = { + 'data': { + 'vertices': [ + { + 'id': '1', + 'entity': {}, + 'properties': {} + } + ], + 'edges': [] + } + } + + return (200, headers, json.dumps(response)) + + responses.add_callback( + responses.POST, API_ENDPOINT, + callback=build_deferred_query_response(url=STATE_FILE_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, STATE_FILE_ENDPOINT, + callback=build_state_response(url=RESULTS_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, RESULTS_ENDPOINT, + callback=request_callback, + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1' return tree" + response = j1.query_v1( + query=query + ) + + assert type(response) == dict + assert 'edges' in response + assert 'vertices' in response + assert type(response['edges']) == list + assert type(response['vertices']) == list + assert response['vertices'][0]['id'] == '1' + +@responses.activate +def test_retry_on_limit_skip_query(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(response_code=429), + content_type='application/json', + ) + + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(response_code=503), + content_type='application/json', + ) + + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1'" + response = j1.query_v1( + query=query, + limit=250, + skip=0 + ) + + assert type(response) == dict + assert len(response['data']) == 1 + assert type(response['data']) == list + assert response['data'][0]['entity']['_id'] == '1' + +@responses.activate +def test_retry_on_cursor_query(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(response_code=429), + content_type='application/json', + ) + + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(response_code=503), + content_type='application/json', + ) + + responses.add_callback( + responses.POST, API_ENDPOINT, + callback=build_deferred_query_response(url=STATE_FILE_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, STATE_FILE_ENDPOINT, + callback=build_state_response(response_code=503, url=RESULTS_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, STATE_FILE_ENDPOINT, + callback=build_state_response(url=RESULTS_ENDPOINT), + content_type='application/json', + ) + + responses.add_callback( + responses.GET, RESULTS_ENDPOINT, + callback=build_deferred_query_results(), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1'" + response = j1.query_v1( + query=query + ) + + assert type(response) == dict + assert len(response['data']) == 1 + assert type(response['data']) == list + assert response['data'][0]['entity']['_id'] == '1' + +@responses.activate +def test_avoid_retry_on_limit_skip_query(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(response_code=404), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1'" + with pytest.raises(JupiterOneApiError): + j1.query_v1( + query=query, + limit=250, + skip=0 + ) + +@responses.activate +def test_avoid_retry_on_cursor_query(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(response_code=404), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1'" + with pytest.raises(JupiterOneApiError): + j1.query_v1( + query=query, + limit=250, + skip=0 + ) + +@responses.activate +def test_warn_limit_and_skip_deprecated(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='testToken') + query = "find Host with _id='1'" + + with pytest.warns(DeprecationWarning): + j1.query_v1( + query=query, + limit=250, + skip=0 + ) From af23ad9e5a614beb1a6c24f71e2728837e3cfd6f Mon Sep 17 00:00:00 2001 From: Tyler Walch Date: Thu, 18 Nov 2021 15:56:50 -0500 Subject: [PATCH 02/22] benchmarked to be faster: "FIND DataStore" 0.62s user 0.14s system 1% cpu 41.754 total 0.56s user 0.10s system 2% cpu 32.367 total "FIND (Service|DataStore)" 0.67s user 0.13s system 1% cpu 55.249 total 0.67s user 0.12s system 1% cpu 39.487 total --- jupiterone/client.py | 5 ++--- jupiterone/constants.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index d814acf..2c362bc 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -133,15 +133,14 @@ def _cursor_query(self, query: str, cursor: str = None, include_deleted: bool = variables['cursor'] = cursor response = self._execute_query(query=CURSOR_QUERY_V1, variables=variables) - data = response['data']['queryV1'] + data = response['data']['queryV1']['data'] if 'vertices' in data and 'edges' in data: return data results.extend(data) - if 'cursor' in response['data']['queryV1']: - print('cursor', response['data']['queryV1']['cursor'], len(data), len(results)) + if 'cursor' in response['data']['queryV1'] and response['data']['queryV1']['cursor'] is not None: cursor = response['data']['queryV1']['cursor'] else: break diff --git a/jupiterone/constants.py b/jupiterone/constants.py index d8d350c..1383e97 100644 --- a/jupiterone/constants.py +++ b/jupiterone/constants.py @@ -6,7 +6,6 @@ queryV1(query: $query, variables: $variables, dryRun: $dryRun, includeDeleted: $includeDeleted) { type data - url } } """ @@ -16,6 +15,7 @@ queryV1( query: $query variables: $variables + deferredResponse: DISABLED flags: $flags includeDeleted: $includeDeleted cursor: $cursor From b5e715e91a6cfc38c14badd3bb80a6898c040517 Mon Sep 17 00:00:00 2001 From: Tyler Walch Date: Thu, 18 Nov 2021 16:04:24 -0500 Subject: [PATCH 03/22] Fixing tests --- tests/test_query.py | 165 ++++++++------------------------------------ 1 file changed, 29 insertions(+), 136 deletions(-) diff --git a/tests/test_query.py b/tests/test_query.py index f9f5860..a524f98 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -4,93 +4,12 @@ from collections import Counter from jupiterone.client import JupiterOneClient -from jupiterone.constants import QUERY_V1, DEFERRED_RESULTS_COMPLETED +from jupiterone.constants import QUERY_V1 from jupiterone.errors import JupiterOneApiError -API_ENDPOINT = 'https://api.us.jupiterone.io/graphql' -STATE_FILE_ENDPOINT = 'https://api.us.jupiterone.io/state' -RESULTS_ENDPOINT = 'https://api.us.jupiterone.io/results' - -def build_deferred_query_response(response_code: int = 200, url: str = STATE_FILE_ENDPOINT): - def request_callback(request): - headers = { - 'Content-Type': 'application/json' - } - - response = { - 'data': { - 'queryV1': { - 'url': url - } - } - } - return (response_code, headers, json.dumps(response)) - return request_callback - - -def build_state_response(response_code: int = 200, status: str = DEFERRED_RESULTS_COMPLETED, url: str = RESULTS_ENDPOINT): - def request_callback(request): - headers = { - 'Content-Type': 'application/json' - } - - response = { - 'status': status, - 'url': url - } - return (response_code, headers, json.dumps(response)) - return request_callback - -def build_deferred_query_results(response_code: int = 200, cursor: str = None, max_pages: int = 1): +def build_results(response_code: int = 200, cursor: str = None, max_pages: int = 1): pages = Counter(requests=0) - def request_callback(request): - headers = { - 'Content-Type': 'application/json' - } - - response = { - 'data': [ - { - 'id': '1', - 'entity': { - '_rawDataHashes': '1', - '_integrationDefinitionId': '1', - '_integrationName': '1', - '_beginOn': 1580482083079, - 'displayName': 'host1', - '_class': ['Host'], - '_scope': 'aws_instance', - '_version': 1, - '_integrationClass': 'CSP', - '_accountId': 'testAccount', - '_id': '1', - '_key': 'key1', - '_type': ['aws_instance'], - '_deleted': False, - '_integrationInstanceId': '1', - '_integrationType': 'aws', - '_source': 'integration-managed', - '_createdOn': 1578093840019 - }, - 'properties': { - 'id': 'host1', - 'active': True - } - } - ] - } - - if cursor is not None and pages.get('requests') < max_pages: - response['cursor'] = cursor - - pages.update(requests=1) - - return (response_code, headers, json.dumps(response)) - - return request_callback - -def build_results(response_code: int = 200): def request_callback(request): headers = { 'Content-Type': 'application/json' @@ -132,7 +51,14 @@ def request_callback(request): } } } + + if cursor is not None and pages.get('requests') < max_pages: + response['data']['queryV1']['cursor'] = cursor + + pages.update(requests=1) + return (response_code, headers, json.dumps(response)) + return request_callback @responses.activate @@ -188,20 +114,14 @@ def test_limit_skip_query_v1(): def test_cursor_query_v1(): responses.add_callback( - responses.POST, API_ENDPOINT, - callback=build_deferred_query_response(url=STATE_FILE_ENDPOINT), - content_type='application/json', - ) - - responses.add_callback( - responses.GET, STATE_FILE_ENDPOINT, - callback=build_state_response(url=RESULTS_ENDPOINT), + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(cursor='cursor_value'), content_type='application/json', ) responses.add_callback( - responses.GET, RESULTS_ENDPOINT, - callback=build_deferred_query_results(cursor='cursor_value'), + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(), content_type='application/json', ) @@ -276,33 +196,26 @@ def request_callback(request): response = { 'data': { - 'vertices': [ - { - 'id': '1', - 'entity': {}, - 'properties': {} + 'queryV1': { + 'type': 'tree', + 'data': { + 'vertices': [ + { + 'id': '1', + 'entity': {}, + 'properties': {} + } + ], + 'edges': [] } - ], - 'edges': [] + } } } return (200, headers, json.dumps(response)) responses.add_callback( - responses.POST, API_ENDPOINT, - callback=build_deferred_query_response(url=STATE_FILE_ENDPOINT), - content_type='application/json', - ) - - responses.add_callback( - responses.GET, STATE_FILE_ENDPOINT, - callback=build_state_response(url=RESULTS_ENDPOINT), - content_type='application/json', - ) - - responses.add_callback( - responses.GET, RESULTS_ENDPOINT, + responses.POST, 'https://api.us.jupiterone.io/graphql', callback=request_callback, content_type='application/json', ) @@ -368,26 +281,8 @@ def test_retry_on_cursor_query(): ) responses.add_callback( - responses.POST, API_ENDPOINT, - callback=build_deferred_query_response(url=STATE_FILE_ENDPOINT), - content_type='application/json', - ) - - responses.add_callback( - responses.GET, STATE_FILE_ENDPOINT, - callback=build_state_response(response_code=503, url=RESULTS_ENDPOINT), - content_type='application/json', - ) - - responses.add_callback( - responses.GET, STATE_FILE_ENDPOINT, - callback=build_state_response(url=RESULTS_ENDPOINT), - content_type='application/json', - ) - - responses.add_callback( - responses.GET, RESULTS_ENDPOINT, - callback=build_deferred_query_results(), + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_results(), content_type='application/json', ) @@ -431,9 +326,7 @@ def test_avoid_retry_on_cursor_query(): query = "find Host with _id='1'" with pytest.raises(JupiterOneApiError): j1.query_v1( - query=query, - limit=250, - skip=0 + query=query ) @responses.activate From f2e1dcea379578b969948aeb11dff8ee82b67a1c Mon Sep 17 00:00:00 2001 From: Tyler Walch Date: Thu, 18 Nov 2021 16:11:33 -0500 Subject: [PATCH 04/22] Clean up --- tests/test_query.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_query.py b/tests/test_query.py index a524f98..9c7859e 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -222,9 +222,7 @@ def request_callback(request): j1 = JupiterOneClient(account='testAccount', token='testToken') query = "find Host with _id='1' return tree" - response = j1.query_v1( - query=query - ) + response = j1.query_v1(query) assert type(response) == dict assert 'edges' in response From b2b178f1d578b53472cb5b815aa10abdae4f2023 Mon Sep 17 00:00:00 2001 From: Tyler Walch Date: Thu, 18 Nov 2021 16:16:08 -0500 Subject: [PATCH 05/22] Bumping version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index eb477c7..17ba317 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ ] setup(name='jupiterone', - version='0.1.0', + version='0.2.0', description='A Python client for the JupiterOne API', license='MIT License', author='George Vauter', From b5165965f21ce8969232a59d5f0c58cbe92da3ab Mon Sep 17 00:00:00 2001 From: lmarkscp Date: Fri, 15 Jul 2022 14:21:36 -0400 Subject: [PATCH 06/22] Added handling of 401 and other responses that aren't JSON. Added tests for the new error handling. --- jupiterone/client.py | 11 +++++-- tests/test_query.py | 69 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index 2c362bc..c3a0786 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -107,12 +107,19 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: raise JupiterOneApiError(content.get('errors')) return response.json() + elif response.status_code == 401: + raise JupiterOneApiError('JupiterOne API query is unauthorized, check credentials.') + elif response.status_code in [429, 503]: raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded') else: - content = json.loads(response._content) - raise JupiterOneApiError('{}:{}'.format(response.status_code, content.get('error'))) + content = response._content + if isinstance(content, (bytes, bytearray)): + content = content.decode("utf-8") + if 'application/json' in response.headers.get('Content-Type', 'application/json') and '"error":' in content: + content = json.loads(content).get('error') + raise JupiterOneApiError('{}:{}'.format(response.status_code, content)) def _cursor_query(self, query: str, cursor: str = None, include_deleted: bool = False) -> Dict: """ Performs a V1 graph query using cursor pagination diff --git a/tests/test_query.py b/tests/test_query.py index 9c7859e..5172c17 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -61,6 +61,17 @@ def request_callback(request): return request_callback + +def build_error_results(response_code: int, response_content, response_type: str = 'application/json'): + def request_callback(request): + headers = { + 'Content-Type': response_type + } + return response_code, headers, response_content + + return request_callback + + @responses.activate def test_execute_query(): @@ -344,3 +355,61 @@ def test_warn_limit_and_skip_deprecated(): limit=250, skip=0 ) + + +@responses.activate +def test_unauthorized_query_v1(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_error_results(401, b'Unauthorized', 'text/plain'), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='bogusToken') + query = "find Host with _id='1' return tree" + + with pytest.raises(JupiterOneApiError) as exc_info: + j1.query_v1(query) + + assert type(exc_info.value) == JupiterOneApiError + assert exc_info.value.args[0] == 'JupiterOne API query is unauthorized, check credentials.' + + +@responses.activate +def test_unexpected_string_error_query_v1(): + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_error_results(500, 'String exception on server', 'text/plain'), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='bogusToken') + query = "find Host with _id='1' return tree" + + with pytest.raises(JupiterOneApiError) as exc_info: + j1.query_v1(query) + + assert type(exc_info.value) == JupiterOneApiError + assert exc_info.value.args[0] == '500:String exception on server' + + +@responses.activate +def test_unexpected_json_error_query_v1(): + error_json = { + 'error': 'Bad Gateway' + } + + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_error_results(502, json.dumps(error_json), ), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='bogusToken') + query = "find Host with _id='1' return tree" + + with pytest.raises(JupiterOneApiError) as exc_info: + j1.query_v1(query) + + assert type(exc_info.value) == JupiterOneApiError + assert exc_info.value.args[0] == '502:Bad Gateway' From 0d3a5aed0a8fccb1b1f6fe4490041872db04fcc9 Mon Sep 17 00:00:00 2001 From: lmarkscp Date: Mon, 18 Jul 2022 13:42:06 -0400 Subject: [PATCH 07/22] Cleaned up handling of error(s) in json --- jupiterone/client.py | 5 +++-- tests/test_query.py | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index c3a0786..f6287d1 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -117,8 +117,9 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: content = response._content if isinstance(content, (bytes, bytearray)): content = content.decode("utf-8") - if 'application/json' in response.headers.get('Content-Type', 'application/json') and '"error":' in content: - content = json.loads(content).get('error') + if 'application/json' in response.headers.get('Content-Type', 'text/plain'): + data = json.loads(content) + content = data.get('error', data.get('errors', content)) raise JupiterOneApiError('{}:{}'.format(response.status_code, content)) def _cursor_query(self, query: str, cursor: str = None, include_deleted: bool = False) -> Dict: diff --git a/tests/test_query.py b/tests/test_query.py index 5172c17..28dbc58 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -371,7 +371,6 @@ def test_unauthorized_query_v1(): with pytest.raises(JupiterOneApiError) as exc_info: j1.query_v1(query) - assert type(exc_info.value) == JupiterOneApiError assert exc_info.value.args[0] == 'JupiterOne API query is unauthorized, check credentials.' @@ -389,7 +388,6 @@ def test_unexpected_string_error_query_v1(): with pytest.raises(JupiterOneApiError) as exc_info: j1.query_v1(query) - assert type(exc_info.value) == JupiterOneApiError assert exc_info.value.args[0] == '500:String exception on server' @@ -411,5 +409,25 @@ def test_unexpected_json_error_query_v1(): with pytest.raises(JupiterOneApiError) as exc_info: j1.query_v1(query) - assert type(exc_info.value) == JupiterOneApiError assert exc_info.value.args[0] == '502:Bad Gateway' + + +@responses.activate +def test_unexpected_json_errors_query_v1(): + errors_json = { + 'errors': ['First error', 'Second error', 'Third error'] + } + + responses.add_callback( + responses.POST, 'https://api.us.jupiterone.io/graphql', + callback=build_error_results(500, json.dumps(errors_json), ), + content_type='application/json', + ) + + j1 = JupiterOneClient(account='testAccount', token='bogusToken') + query = "find Host with _id='1' return tree" + + with pytest.raises(JupiterOneApiError) as exc_info: + j1.query_v1(query) + + assert exc_info.value.args[0] == "500:['First error', 'Second error', 'Third error']" From f6a9d56fb402470f93fedcefc1dcac7a87f59f65 Mon Sep 17 00:00:00 2001 From: "sre-57-opslevel[bot]" <113727212+sre-57-opslevel[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:09:37 +0000 Subject: [PATCH 08/22] Upload OpsLevel YAML --- opslevel.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 opslevel.yml diff --git a/opslevel.yml b/opslevel.yml new file mode 100644 index 0000000..3a5e183 --- /dev/null +++ b/opslevel.yml @@ -0,0 +1,6 @@ +--- +version: 1 +repository: + owner: cloud_security_ops + tier: + tags: From 7b76a4609c9d89f93b3d8a6f5b6acfe66276a321 Mon Sep 17 00:00:00 2001 From: "sara.perez" Date: Tue, 22 Aug 2023 15:37:10 +0200 Subject: [PATCH 09/22] bumping version --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 17ba317..46c9dfb 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Auth0 +# Copyright (c) 2020-2023 Okta from setuptools import setup, find_packages @@ -8,12 +8,12 @@ ] setup(name='jupiterone', - version='0.2.0', + version='0.2.1', description='A Python client for the JupiterOne API', license='MIT License', author='George Vauter', - author_email='george.vauter@auth0.com', - maintainer='Auth0', + author_email='george.vauter@okta.com', + maintainer='Okta', url='https://github.com/auth0/jupiterone-python-sdk', install_requires=install_reqs, classifiers=[ From 79a1d9a4a76b838c768849cc0c9eb8d5debe322d Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Fri, 19 Apr 2024 19:55:11 -0600 Subject: [PATCH 10/22] Update setup.py --- setup.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 46c9dfb..c55479a 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020-2023 Okta +# Copyright (c) 2020-2025 JupiterOne from setuptools import setup, find_packages @@ -8,13 +8,13 @@ ] setup(name='jupiterone', - version='0.2.1', + version='0.3.0', description='A Python client for the JupiterOne API', license='MIT License', - author='George Vauter', - author_email='george.vauter@okta.com', - maintainer='Okta', - url='https://github.com/auth0/jupiterone-python-sdk', + author='JupiterOne', + author_email='solutions@jupiterone.com', + maintainer='JupiterOne', + url='https://github.com/JupiterOne/jupiterone-api-client-python', install_requires=install_reqs, classifiers=[ 'Development Status :: 4 - Beta', From 4f85ce60d2b2d754f35d981bf1265a091c84cfdf Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Fri, 19 Apr 2024 21:04:58 -0600 Subject: [PATCH 11/22] Create examples.py --- examples/examples.py | 91 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 examples/examples.py diff --git a/examples/examples.py b/examples/examples.py new file mode 100644 index 0000000..efcae0c --- /dev/null +++ b/examples/examples.py @@ -0,0 +1,91 @@ +from jupiterone.client import JupiterOneClient +import random +import time +import os + +account = os.environ.get("JUPITERONE_ACCOUNT") +token = os.environ.get("JUPITERONE_TOKEN") + +j1 = JupiterOneClient(account=account, token=token) + +# query_v1 +q = "FIND jupiterone_user" +query_r = j1.query_v1(q) +print(query_r) + +# create_entity +num1 = random.randrange(1,999,1) + +# create_entity +properties = { + 'displayName': 'test{}'.format(num1), + 'customProperty': 'customVal', + 'tag.Production': 'false', + 'owner': 'colin.blumer@jupiterone.com' + } + +create_r = j1.create_entity( + entity_key='jupiterone-api-client-python:{}'.format(num1), + entity_type='python_client_create_entity', + entity_class='Record', + properties=properties, + timestamp=int(time.time()) * 1000 # Optional, defaults to current datetime +) +print(create_r) + +properties = { + 'customProperty': 'customValUpdated' + } + +# update_entity +update_r = j1.update_entity( + entity_id='{}'.format(create_r['entity']['_id']), + properties=properties +) +print(update_r) + +# create_entity_2 +num2 = random.randrange(1,999,1) + +properties = { + 'displayName': 'test{}'.format(num2), + 'customProperty': 'customVal', + 'tag.Production': 'false', + 'owner': 'user.name@jupiterone.com' + } + +create_r_2 = j1.create_entity( + entity_key='jupiterone-api-client-python:{}'.format(num2), + entity_type='python_client_create_entity', + entity_class='Record', + properties=properties, + timestamp=int(time.time()) * 1000 # Optional, defaults to current datetime +) +print(create_r_2) + +# create_relationship +create_relationship_r = j1.create_relationship( + relationship_key='{}:{}'.format(create_r['entity']['_id'], create_r_2['entity']['_id']), + relationship_type='jupiterone-api-client-python:create_relationship', + relationship_class='HAS', + from_entity_id=create_r['entity']['_id'], + to_entity_id=create_r_2['entity']['_id'] +) +print(create_relationship_r) + +# delete_relationship +delete_relationship_r = j1.delete_relationship(relationship_id=create_relationship_r['relationship']['_id']) +print(delete_relationship_r) + +# delete_entity +delete_entity_r1 = j1.delete_entity(entity_id=create_r['entity']['_id']) +print(delete_entity_r1) + +delete_entity_r2 = j1.delete_entity(entity_id=create_r_2['entity']['_id']) +print(delete_entity_r2) + + +q = "FIND (Device | Person)" +cursor_query_r = j1._cursor_query(q) +print(cursor_query_r) +print(len(cursor_query_r['data'])) From d3a0e55aeea145dec052b9c5d35e99f423d54f3d Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Fri, 19 Apr 2024 21:10:44 -0600 Subject: [PATCH 12/22] Delete opslevel.yml --- opslevel.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 opslevel.yml diff --git a/opslevel.yml b/opslevel.yml deleted file mode 100644 index 3a5e183..0000000 --- a/opslevel.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -version: 1 -repository: - owner: cloud_security_ops - tier: - tags: From fa8ae1a1520894f5fae1f08720f25bd82c4da906 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Fri, 19 Apr 2024 21:22:02 -0600 Subject: [PATCH 13/22] Update client.py --- jupiterone/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index f6287d1..45d0579 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -91,7 +91,7 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: if variables: data.update(variables=variables) - response = requests.post(self.query_endpoint, headers=self.headers, json=data) + response = requests.post(self.query_endpoint, headers=self.headers, json=data, timeout=60) # It is still unclear if all responses will have a status # code of 200 or if 429 will eventually be used to From 485480ceb0422ad9b490a9447be831b770057274 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 22 Apr 2024 11:51:15 -0600 Subject: [PATCH 14/22] Update examples.py --- examples/examples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/examples.py b/examples/examples.py index efcae0c..402bdef 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -14,14 +14,14 @@ print(query_r) # create_entity -num1 = random.randrange(1,999,1) +num1 = random.randrange(1, 999, 1) # create_entity properties = { 'displayName': 'test{}'.format(num1), 'customProperty': 'customVal', 'tag.Production': 'false', - 'owner': 'colin.blumer@jupiterone.com' + 'owner': 'user.name@jupiterone.com' } create_r = j1.create_entity( @@ -45,7 +45,7 @@ print(update_r) # create_entity_2 -num2 = random.randrange(1,999,1) +num2 = random.randrange(1, 999, 1) properties = { 'displayName': 'test{}'.format(num2), From 0dc3965af29c169018ed01e89a306e81d6419011 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 22 Apr 2024 12:04:32 -0600 Subject: [PATCH 15/22] fix PEP8 checks --- jupiterone/client.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index 45d0579..466d328 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -27,6 +27,7 @@ CURSOR_QUERY_V1 ) + def retry_on_429(exc): """ Used to trigger retry on rate limit """ return isinstance(exc, JupiterOneApiRetryError) @@ -70,12 +71,12 @@ def account(self, value: str): @property def token(self): - """ Your JupiteOne access token """ + """ Your JupiterOne access token """ return self._token @token.setter def token(self, value: str): - """ Your JupiteOne access token """ + """ Your JupiterOne access token """ if not value: raise JupiterOneClientError('token is required') self._token = value @@ -95,7 +96,7 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: # It is still unclear if all responses will have a status # code of 200 or if 429 will eventually be used to - # indicate rate limitting. J1 devs are aware. + # indicate rate limits being hit. J1 devs are aware. if response.status_code == 200: if response._content: content = json.loads(response._content) @@ -155,7 +156,11 @@ def _cursor_query(self, query: str, cursor: str = None, include_deleted: bool = return {'data': results} - def _limit_and_skip_query(self, query: str, skip: int = J1QL_SKIP_COUNT, limit: int = J1QL_LIMIT_COUNT, include_deleted: bool = False) -> Dict: + def _limit_and_skip_query(self, + query: str, + skip: int = J1QL_SKIP_COUNT, + limit: int = J1QL_LIMIT_COUNT, + include_deleted: bool = False) -> Dict: results: List = [] page: int = 0 @@ -200,7 +205,10 @@ def query_v1(self, query: str, **kwargs) -> Dict: cursor: str = kwargs.pop('cursor', None) if uses_limit_and_skip: - warn('limit and skip pagination is no longer a recommended method for pagination. To read more about using cursors checkout the JupiterOne documentation: https://support.jupiterone.io/hc/en-us/articles/360022722094#entityandrelationshipqueries', DeprecationWarning, stacklevel=2) + warn('limit and skip pagination is no longer a recommended method for pagination. ' + 'To read more about using cursors checkout the JupiterOne documentation: ' + 'https://support.jupiterone.io/hc/en-us/articles/360022722094#entityandrelationshipqueries', + DeprecationWarning, stacklevel=2) return self._limit_and_skip_query( query=query, skip=skip, @@ -261,7 +269,7 @@ def update_entity(self, entity_id: str = None, properties: Dict = None) -> Dict: Update an existing entity. args: - entity_id (str): The _id of the entity to udate + entity_id (str): The _id of the entity to update properties (dict): Dictionary of key/value entity properties """ variables = { @@ -273,7 +281,7 @@ def update_entity(self, entity_id: str = None, properties: Dict = None) -> Dict: def create_relationship(self, **kwargs) -> Dict: """ - Create a relationship (edge) between two entities (veritces). + Create a relationship (edge) between two entities (vertices). args: relationship_key (str): Unique key for the relationship From 56365010802a4d29070b626ff4d8a33273b4682e Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 22 Apr 2024 14:26:51 -0600 Subject: [PATCH 16/22] more PEP8 fixes, updated README, bump to v1.0.0 --- README.md | 6 +++++- jupiterone/__init__.py | 2 +- jupiterone/errors.py | 4 +++- requirements.txt | 1 + setup.py | 8 ++++---- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cca35b9..8075f04 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/release/python-370/) -A Python library for the [JupiterOne API](https://support.jupiterone.io/hc/en-us/articles/360022722094-JupiterOne-Platform-API). +A Python library for the [JupiterOne API](https://docs.jupiterone.io/reference). ## Installation @@ -41,6 +41,10 @@ query_result = j1.query_v1(QUERY, include_deleted=True) # Tree query QUERY = 'FIND Host RETURN TREE' query_result = j1.query_v1(QUERY) + +# Using cursor graphQL variable to return full set of paginated results +QUERY = "FIND (Device | Person)" +cursor_query_r = j1._cursor_query(QUERY) ``` ##### Create an entity: diff --git a/jupiterone/__init__.py b/jupiterone/__init__.py index 50b1190..0d227bb 100644 --- a/jupiterone/__init__.py +++ b/jupiterone/__init__.py @@ -2,4 +2,4 @@ from .errors import ( JupiterOneClientError, JupiterOneApiError -) \ No newline at end of file +) diff --git a/jupiterone/errors.py b/jupiterone/errors.py index aaf1b3e..eccf017 100644 --- a/jupiterone/errors.py +++ b/jupiterone/errors.py @@ -2,8 +2,10 @@ class JupiterOneClientError(Exception): """ Raised when error creating client """ + class JupiterOneApiRetryError(Exception): """ Used to trigger retry on rate limit """ + class JupiterOneApiError(Exception): - """ Raised when API returns error response """ \ No newline at end of file + """ Raised when API returns error response """ diff --git a/requirements.txt b/requirements.txt index d5f6187..3c9c733 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ retrying requests +warnings diff --git a/setup.py b/setup.py index c55479a..9cb3f2a 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ # Copyright (c) 2020-2025 JupiterOne - from setuptools import setup, find_packages install_reqs = [ 'requests', - 'retrying' + 'retrying', + 'warnings' ] setup(name='jupiterone', - version='0.3.0', + version='1.0.0', description='A Python client for the JupiterOne API', license='MIT License', author='JupiterOne', @@ -28,4 +28,4 @@ 'Topic :: Security', ], packages=find_packages() -) + ) From 8e2b9238f7a1498d8ed2e892a252b75f6d7eba6b Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 22 Apr 2024 14:30:20 -0600 Subject: [PATCH 17/22] handle 500 response --- jupiterone/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index 466d328..9260db9 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -112,7 +112,10 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: raise JupiterOneApiError('JupiterOne API query is unauthorized, check credentials.') elif response.status_code in [429, 503]: - raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded') + raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded.') + + elif response.status_code in [500]: + raise JupiterOneApiRetryError('JupiterOne API internal server error.') else: content = response._content From 339daae84d39b552b2811cf03171f1ccf509df38 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 22 Apr 2024 15:08:00 -0600 Subject: [PATCH 18/22] Update client.py --- jupiterone/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index 9260db9..b9e9f4b 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -115,7 +115,7 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded.') elif response.status_code in [500]: - raise JupiterOneApiRetryError('JupiterOne API internal server error.') + raise JupiterOneApiError('JupiterOne API internal server error.') else: content = response._content From f3953b1053566e1dd9285d77f9c92ce365d79c85 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Tue, 30 Apr 2024 13:15:44 -0600 Subject: [PATCH 19/22] updated cursor docs link --- jupiterone/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index b9e9f4b..1aa1654 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -210,7 +210,7 @@ def query_v1(self, query: str, **kwargs) -> Dict: if uses_limit_and_skip: warn('limit and skip pagination is no longer a recommended method for pagination. ' 'To read more about using cursors checkout the JupiterOne documentation: ' - 'https://support.jupiterone.io/hc/en-us/articles/360022722094#entityandrelationshipqueries', + 'https://docs.jupiterone.io/features/admin/parameters#query-parameterlist', DeprecationWarning, stacklevel=2) return self._limit_and_skip_query( query=query, From c05addd6dd2b22128742e42a9e7f376c68a3d7e4 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Tue, 30 Apr 2024 13:19:35 -0600 Subject: [PATCH 20/22] Update LICENSE --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index d2618d4..d9da973 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Auth0 +Copyright (c) 2024 JupiterOne Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 91798121d164e84201ffb5699aa3171d45108133 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Tue, 30 Apr 2024 13:26:14 -0600 Subject: [PATCH 21/22] Delete .travis.yml --- .travis.yml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 19bcdce..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: python -python: - - 3.6 - - 3.7 - -install: - - pip install . - - pip install responses - - pip install tox-travis - -script: - - tox From a5866dc5a0101474fdca0134687c896e84aba6a7 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Tue, 30 Apr 2024 13:38:23 -0600 Subject: [PATCH 22/22] Update client.py --- jupiterone/client.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/jupiterone/client.py b/jupiterone/client.py index f91ea3d..542e626 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -111,16 +111,9 @@ def _execute_query(self, query: str, variables: Dict = None) -> Dict: elif response.status_code == 401: raise JupiterOneApiError('401: Unauthorized. Please supply a valid account id and API token.') - elif response.status_code in [429, 500]: + elif response.status_code in [429, 503]: raise JupiterOneApiRetryError('JupiterOne API rate limit exceeded') - else: - try: - content = json.loads(response._content) - raise JupiterOneApiError('{}: {}'.format(response.status_code, content.get('error') or 'Unknown Error')) - except ValueError as e: - raise JupiterOneApiError('{}: {}'.format(response.status_code, 'Unknown Error')); - elif response.status_code in [500]: raise JupiterOneApiError('JupiterOne API internal server error.')