Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7f656cc
calling the J1 sync results API
tywalch Nov 18, 2021
af23ad9
benchmarked to be faster:
tywalch Nov 18, 2021
b5e715e
Fixing tests
tywalch Nov 18, 2021
f2e1dce
Clean up
tywalch Nov 18, 2021
b2b178f
Bumping version
tywalch Nov 18, 2021
7000588
Merge pull request #11 from JupiterOne/sync-calls-with-cursor
gvauter Dec 6, 2021
b516596
Added handling of 401 and other responses that aren't JSON.
lmarkscp Jul 15, 2022
0d3a5ae
Cleaned up handling of error(s) in json
lmarkscp Jul 18, 2022
f6a9d56
Upload OpsLevel YAML
sre-57-opslevel[bot] Oct 17, 2022
1c63eaf
Merge pull request #14 from auth0/SRE-57-Upload-opslevel-yaml
gvauter Oct 24, 2022
49537d1
Merge pull request #13 from lmarkscp/improve-response-error-handling
gvauter Aug 4, 2023
7b76a46
bumping version
cleorun Aug 22, 2023
95b2f74
Merge pull request #16 from auth0/0.2.1
cleorun Aug 22, 2023
79a1d9a
Update setup.py
SeaBlooms Apr 20, 2024
4f85ce6
Create examples.py
SeaBlooms Apr 20, 2024
d3a0e55
Delete opslevel.yml
SeaBlooms Apr 20, 2024
fa8ae1a
Update client.py
SeaBlooms Apr 20, 2024
485480c
Update examples.py
SeaBlooms Apr 22, 2024
0dc3965
fix PEP8 checks
SeaBlooms Apr 22, 2024
5636501
more PEP8 fixes, updated README, bump to v1.0.0
SeaBlooms Apr 22, 2024
8e2b923
handle 500 response
SeaBlooms Apr 22, 2024
339daae
Update client.py
SeaBlooms Apr 22, 2024
f3953b1
updated cursor docs link
SeaBlooms Apr 30, 2024
c05addd
Update LICENSE
SeaBlooms Apr 30, 2024
9179812
Delete .travis.yml
SeaBlooms Apr 30, 2024
c4a35d1
Merge branch 'main' into resolveConflictsMigrateRepo
SeaBlooms Apr 30, 2024
a5866dc
Update client.py
SeaBlooms Apr 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down
91 changes: 91 additions & 0 deletions examples/examples.py
Original file line number Diff line number Diff line change
@@ -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': 'user.name@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']))
2 changes: 1 addition & 1 deletion jupiterone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from .errors import (
JupiterOneClientError,
JupiterOneApiError
)
)
110 changes: 88 additions & 22 deletions jupiterone/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import requests
from retrying import retry
from warnings import warn

from jupiterone.errors import (
JupiterOneClientError,
Expand All @@ -22,9 +23,11 @@
DELETE_ENTITY,
UPDATE_ENTITY,
CREATE_RELATIONSHIP,
DELETE_RELATIONSHIP
DELETE_RELATIONSHIP,
CURSOR_QUERY_V1
)


def retry_on_429(exc):
""" Used to trigger retry on rate limit """
return isinstance(exc, JupiterOneApiRetryError)
Expand Down Expand Up @@ -68,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
Expand All @@ -89,11 +92,11 @@ 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
# 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)
Expand All @@ -108,29 +111,59 @@ 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.')

def query_v1(self, query: str, **kwargs) -> Dict:
""" Performs a V1 graph query
else:
content = response._content
if isinstance(content, (bytes, bytearray)):
content = content.decode("utf-8")
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:
""" 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']['data']

if 'vertices' in data and 'edges' in data:
return data

results.extend(data)

if 'cursor' in response['data']['queryV1'] and response['data']['queryV1']['cursor'] is not None:
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

Expand Down Expand Up @@ -159,6 +192,39 @@ 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://docs.jupiterone.io/features/admin/parameters#query-parameterlist',
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.

Expand Down Expand Up @@ -206,7 +272,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 = {
Expand All @@ -218,7 +284,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
Expand Down
18 changes: 18 additions & 0 deletions jupiterone/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
}
"""

CURSOR_QUERY_V1 = """
query J1QL_v2($query: String!, $variables: JSON, $flags: QueryV1Flags, $includeDeleted: Boolean, $cursor: String) {
queryV1(
query: $query
variables: $variables
deferredResponse: DISABLED
flags: $flags
includeDeleted: $includeDeleted
cursor: $cursor
) {
type
data
cursor
__typename
}
}
"""

CREATE_ENTITY = """
mutation CreateEntity(
$entityKey: String!
Expand Down
4 changes: 3 additions & 1 deletion jupiterone/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
""" Raised when API returns error response """
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
retrying
requests
warnings
18 changes: 9 additions & 9 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# Copyright (c) 2020 Auth0

# Copyright (c) 2020-2025 JupiterOne
from setuptools import setup, find_packages

install_reqs = [
'requests',
'retrying'
'retrying',
'warnings'
]

setup(name='jupiterone',
version='0.1.0',
version='1.0.0',
description='A Python client for the JupiterOne API',
license='MIT License',
author='George Vauter',
author_email='george.vauter@auth0.com',
maintainer='Auth0',
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',
Expand All @@ -28,4 +28,4 @@
'Topic :: Security',
],
packages=find_packages()
)
)
Loading