diff --git a/README.md b/README.md index b0daa05..385de23 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ j1.update_entity( #### Delete an entity: ```python -j1.delete_entity(entit_id='') +j1.delete_entity(entity_id='') ``` ##### Create a relationship @@ -300,14 +300,96 @@ j1.evaluate_smartclass(smartclass_id='') j1.get_smartclass_details(smartclass_id='') ``` +##### Generate J1QL from Natural Language Prompt + +```python +j1.generate_j1ql(natural_language_prompt='') +``` + ##### List Alert Rules ```python -j1.list_configured_alert_rules() +j1.list_alert_rules() ``` -##### Generate J1QL from Natural Language Prompt +##### Get Alert Rule Details ```python -j1.generate_j1ql(natural_language_prompt='') +j1.get_alert_rule_details(rule_id='') +``` + +##### Create Alert Rule + +```python +# polling_interval can be DISABLED, THIRTY_MINUTES, ONE_HOUR, FOUR_HOURS, EIGHT_HOURS, TWELVE_HOURS, ONE_DAY, or ONE_WEEK +# severity can be INFO, LOW, MEDIUM, HIGH, or CRITICAL + +j1.create_alert_rule(name="create_alert_rule-name", + description="create_alert_rule-description", + tags=['tag1', 'tag2'], + polling_interval="DISABLED", + severity="INFO", + j1ql="find jupiterone_user") +``` + +##### Create Alert Rule with Action Config + +```python + +webhook_action_config = { + "type": "WEBHOOK", + "endpoint": "https://webhook.domain.here/endpoint", + "headers": { + "Authorization": "Bearer ", + }, + "method": "POST", + "body": { + "queryData": "{{queries.query0.data}}" + } +} + +j1.create_alert_rule(name="create_alert_rule-name", + description="create_alert_rule-description", + tags=['tag1', 'tag2'], + polling_interval="DISABLED", + severity="INFO", + j1ql="find jupiterone_user", + action_configs=webhook_action_config) + +``` + +##### Delete Alert Rule + +```python + +j1.delete_alert_rule(rule_id='") +print("fetch_entity_raw_data()") +print(json.dumps(fetch_entity_raw_data_r, indent=1)) + # create_integration_instance create_integration_instance_r = j1.create_integration_instance(instance_name="pythonclient-customintegration", instance_description="dev-testing") @@ -118,11 +123,19 @@ integration_instance_id = "" # start_sync_job -start_sync_job_r = j1.start_sync_job(instance_id=integration_instance_id) +# sync_mode can be "DIFF", "CREATE_OR_UPDATE", or "PATCH" +start_sync_job_r = j1.start_sync_job(instance_id=integration_instance_id, + sync_mode='CREATE_OR_UPDATE', + source='integration-external') print("start_sync_job()") print(start_sync_job_r) # upload_entities_batch_json +rand_val_range = [x / 10.0 for x in range(0, 100)] +rand_val = random.choice(rand_val_range) + +epoch_now = round(time.time() * 1000) + entity_payload = [ { "_key": "1", @@ -131,20 +144,18 @@ "displayName": "pythonclient1", "propertyName": "value", "relationshipProperty": "source", + "value": rand_val, + "bulkUploadedOn": epoch_now }, { "_key": "2", "_type": "pythonclient", "_class": "API", "displayName": "pythonclient2", - "propertyName": "value" - }, - { - "_key": "3", - "_type": "pythonclient", - "_class": "API", - "displayName": "pythonclient3", - "propertyName": "value" + "propertyName": "value", + "relationshipProperty": "source", + "value": rand_val, + "bulkUploadedOn": epoch_now } ] @@ -188,22 +199,21 @@ "_type": "pythonclient", "_class": "API", "displayName": "pythonclient4", - "propertyName": "value", - "relationshipProperty": "source", + "enrichProp": "value1" }, { "_key": "5", "_type": "pythonclient", "_class": "API", "displayName": "pythonclient5", - "propertyName": "value" + "enrichProp": "value2" }, { "_key": "6", "_type": "pythonclient", "_class": "API", "displayName": "pythonclient6", - "propertyName": "value" + "enrichProp": "value3" } ], "relationships": [ @@ -278,12 +288,74 @@ print("get_smartclass_details()") print(get_smartclass_details_r) -# list_configured_alert_rules -list_configured_alert_rules_r = j1.list_configured_alert_rules() -print("list_configured_alert_rules()") -print(list_configured_alert_rules_r) - # generate_j1ql generate_j1ql_r = j1.generate_j1ql(natural_language_prompt="show me all Users containing 'jupiterone' in their email address") print("generate_j1ql()") print(generate_j1ql_r) + +# list_alert_rules +list_alert_rules_r = j1.list_alert_rules() +print("list_configured_alert_rules()") +print(list_alert_rules_r) +print(len(list_alert_rules_r)) + +# get_alert_rule_details +get_alert_rule_details_r = j1.get_alert_rule_details(rule_id="") +print("get_alert_rule_details()") +print(get_alert_rule_details_r) + +# create_alert_rule +# polling_interval can be DISABLED, THIRTY_MINUTES, ONE_HOUR, FOUR_HOURS, EIGHT_HOURS, TWELVE_HOURS, ONE_DAY, and ONE_WEEK +webhook_token = "" + +webhook_action_config = { + "type": "WEBHOOK", + "endpoint": "https://webhook.domain.here/endpoint", + "headers": { + "Authorization": "Bearer {}".format(webhook_token), + }, + "method": "POST", + "body": { + "queryData": "{{queries.query0.data}}" + } +} + +tag_entities_action_config = { + "type": "TAG_ENTITIES", + "entities": "{{queries.query0.data}}", + "tags": [ + { + "name": "tagKey", + "value": "tagValue" + } + ] +} + +create_alert_rule_r = j1.create_alert_rule(name="create_alert_rule-name", + description="create_alert_rule-description", + tags=['tag1', 'tag2'], + polling_interval="DISABLED", + severity="INFO", + j1ql="find jupiterone_user") +print("create_alert_rule()") +print(create_alert_rule_r) + +# delete_alert_rule +delete_alert_rule_r = j1.delete_alert_rule(rule_id="") +print("delete_alert_rule()") +print(delete_alert_rule_r) + +# update_alert_rule +update_alert_rule_r = j1.update_alert_rule(rule_id="", + j1ql="find jupiterone_user as i return i._key", + polling_interval="ONE_WEEK", + tags=['new_tag1', 'new_tag2']) +print("update_alert_rule()") +print(json.dumps(update_alert_rule_r, indent=1)) + +# evaluate_alert_rule +evaluate_alert_rule_r = j1.evaluate_alert_rule(rule_id="") +print("evaluate_alert_rule()") +print(json.dumps(evaluate_alert_rule_r, indent=1)) + + diff --git a/jupiterone/client.py b/jupiterone/client.py index 3d39cda..8757a26 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -33,6 +33,7 @@ INTEGRATION_JOB_VALUES, INTEGRATION_INSTANCE_EVENT_VALUES, ALL_PROPERTIES, + GET_ENTITY_RAW_DATA, CREATE_SMARTCLASS, CREATE_SMARTCLASS_QUERY, EVALUATE_SMARTCLASS, @@ -40,7 +41,9 @@ J1QL_FROM_NATURAL_LANGUAGE, LIST_RULE_INSTANCES, CREATE_RULE_INSTANCE, - DELETE_RULE_INSTANCE + DELETE_RULE_INSTANCE, + UPDATE_RULE_INSTANCE, + EVALUATE_RULE_INSTANCE ) @@ -48,7 +51,6 @@ def retry_on_429(exc): """Used to trigger retry on rate limit""" return isinstance(exc, JupiterOneApiRetryError) - class JupiterOneClient: """Python client class for the JupiterOne GraphQL API""" @@ -499,6 +501,19 @@ def fetch_all_entity_tags(self): return return_list + def fetch_entity_raw_data(self, entity_id: str = None): + """Fetch the contents of raw data for a given entity in a J1 Account. + + """ + variables = { + "entityId": entity_id, + "source": "integration-managed" + } + + response = self._execute_query(query=GET_ENTITY_RAW_DATA, variables=variables) + + return response + def start_sync_job(self, instance_id: str = None, sync_mode: str = None, source: str = None,): """Start a synchronization job. @@ -712,12 +727,82 @@ def generate_j1ql(self, natural_language_prompt: str = None): return response['data']['j1qlFromNaturalLanguage'] def list_alert_rules(self): - """List defined Alert Rules configured in J1 account + """List all defined Alert Rules configured in J1 account + + """ + results = [] + + data = { + "query": LIST_RULE_INSTANCES, + "flags": { + "variableResultSize": True + } + } + + r = requests.post(url=self.graphql_url, headers=self.headers, json=data, verify=True).json() + results.extend(r['data']['listRuleInstances']['questionInstances']) + + while r['data']['listRuleInstances']['pageInfo']['hasNextPage'] == True: + + cursor = r['data']['listRuleInstances']['pageInfo']['endCursor'] + + # cursor query until last page fetched + data = { + "query": LIST_RULE_INSTANCES, + "variables": { + "cursor": cursor + }, + "flags":{ + "variableResultSize": True + } + } + + r = requests.post(url=self.graphql_url, headers=self.headers, json=data, verify=True).json() + results.extend(r['data']['listRuleInstances']['questionInstances']) + + return results + + def get_alert_rule_details(self, rule_id: str = None): + """Get details of a single defined Alert Rule configured in J1 account """ - response = self._execute_query(LIST_RULE_INSTANCES) + results = [] + + data = { + "query": LIST_RULE_INSTANCES, + "flags": { + "variableResultSize": True + } + } + + r = requests.post(url=self.graphql_url, headers=self.headers, json=data, verify=True).json() + results.extend(r['data']['listRuleInstances']['questionInstances']) + + while r['data']['listRuleInstances']['pageInfo']['hasNextPage'] == True: + + cursor = r['data']['listRuleInstances']['pageInfo']['endCursor'] + + # cursor query until last page fetched + data = { + "query": LIST_RULE_INSTANCES, + "variables": { + "cursor": cursor + }, + "flags":{ + "variableResultSize": True + } + } + + r = requests.post(url=self.graphql_url, headers=self.headers, json=data, verify=True).json() + results.extend(r['data']['listRuleInstances']['questionInstances']) + + # pick result out of list of results by 'id' key + item = next((item for item in results if item['id'] == rule_id), None) - return response['data']['listRuleInstances'] + if item: + return item + else: + return 'Alert Rule not found for provided ID in configured J1 Account' def create_alert_rule(self, name: str = None, description: str = None, tags: List[str] = None, polling_interval: str = None, severity: str = None, j1ql: str = None, action_configs: Dict = None): """Create Alert Rule Configuration in J1 account @@ -779,8 +864,6 @@ def create_alert_rule(self, name: str = None, description: str = None, tags: Lis if action_configs: variables['instance']['operations'][0]['actions'].append(action_configs) - print(variables) - response = self._execute_query(CREATE_RULE_INSTANCE, variables=variables) return response['data']['createInlineQuestionRuleInstance'] @@ -795,4 +878,79 @@ def delete_alert_rule(self, rule_id: str = None): response = self._execute_query(DELETE_RULE_INSTANCE, variables=variables) - return response['data']['deleteRuleInstance'] \ No newline at end of file + return response['data']['deleteRuleInstance'] + + def update_alert_rule(self, rule_id: str = None, j1ql: str = None, polling_interval: str = None, tags: List[str] = None, tag_op: str = None): + """Update Alert Rule Configuration in J1 account + + """ + # fetch existing alert rule + alert_rule_config = self.get_alert_rule_details(rule_id) + + # increment rule config version + rule_version = alert_rule_config['version'] + 1 + + # fetch current operations config + operations = alert_rule_config['operations'] + del operations[0]['__typename'] + + # update J1QL query if provided + if j1ql is not None: + question_config = alert_rule_config['question'] + # remove problematic fields + del question_config['__typename'] + del question_config['queries'][0]['__typename'] + + # update query string + question_config['queries'][0]['query'] = j1ql + else: + question_config = alert_rule_config['question'] + # remove problematic fields + del question_config['__typename'] + del question_config['queries'][0]['__typename'] + + # update polling_interval if provided + if polling_interval is not None: + interval_config = polling_interval + else: + interval_config = alert_rule_config['pollingInterval'] + + # update tags list if provided + if tags is not None: + + if tag_op == "OVERWRITE": + tags_config = tags + elif tag_op == "APPEND": + tags_config = alert_rule_config['tags'] + tags + else: + tags_config = alert_rule_config['tags'] + else: + tags_config = alert_rule_config['tags'] + + variables = { + "instance": { + "id": rule_id, + "version": rule_version, + "specVersion": alert_rule_config['specVersion'], + "name": alert_rule_config['name'], + "question": question_config, + "operations": operations, + "pollingInterval": interval_config, + "tags": tags_config + } + } + + response = self._execute_query(UPDATE_RULE_INSTANCE, variables=variables) + + return response['data']['updateInlineQuestionRuleInstance'] + + def evaluate_alert_rule(self, rule_id: str = None): + """Run an Evaluation for a defined Alert Rule configured in J1 account + + """ + variables = { + "id": rule_id + } + + response = self._execute_query(EVALUATE_RULE_INSTANCE, variables=variables) + return response diff --git a/jupiterone/constants.py b/jupiterone/constants.py index 71fda66..110419a 100644 --- a/jupiterone/constants.py +++ b/jupiterone/constants.py @@ -473,4 +473,72 @@ __typename } } +""" + +UPDATE_RULE_INSTANCE = """ + mutation updateQuestionRuleInstance($instance: UpdateInlineQuestionRuleInstanceInput!) { + updateInlineQuestionRuleInstance(instance: $instance) { + ...RuleInstanceFields + __typename + } + } + + fragment RuleInstanceFields on QuestionRuleInstance { + id + accountId + name + description + version + lastEvaluationStartOn + lastEvaluationEndOn + evaluationStep + specVersion + notifyOnFailure + triggerActionsOnNewEntitiesOnly + ignorePreviousResults + pollingInterval + templates + outputs + labels { + labelName + labelValue + __typename + } + question { + queries { + query + name + includeDeleted + __typename + } + __typename + } + questionId + latest + deleted + type + operations { + when + actions + __typename + } + latestAlertId + latestAlertIsActive + state { + actions + __typename + } + tags + remediationSteps + __typename + } +""" + +EVALUATE_RULE_INSTANCE = """ + mutation evaluateRuleInstance($id: ID!) { + evaluateRuleInstance(id: $id) { + id + __typename + } + } """ \ No newline at end of file