From 61f36be32b3a910b8f9096523b09c7902b6df4df Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 14 Apr 2025 21:19:52 -0600 Subject: [PATCH 1/4] add support for alert rule k:v labels and resource groups --- examples/examples.py | 102 ++++++++++++++++++++++++++++++++++++++++++- jupiterone/client.py | 14 +++++- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/examples/examples.py b/examples/examples.py index 1f065e8..50fe6cc 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -2,6 +2,7 @@ import random import time import os +import json account = os.environ.get("JUPITERONE_ACCOUNT") token = os.environ.get("JUPITERONE_TOKEN") @@ -332,12 +333,61 @@ ] } -create_alert_rule_r = j1.create_alert_rule(name="create_alert_rule-name", +create_jira_ticket_action_config = { + "integrationInstanceId" : "", + "type" : "CREATE_JIRA_TICKET", + "entityClass" : "Record", + "summary" : "Jira Task created via JupiterOne Alert Rule", + "issueType" : "Task", + "project" : "KEY", + "additionalFields" : { + "description" : { + "type" : "doc", + "version" : 1, + "content" : [ + { + "type" : "paragraph", + "content" : [ + { + "type" : "text", + "text" : "{{alertWebLink}}\n\n**Affected Items:**\n\n* {{queries.query0.data|mapProperty('displayName')|join('\n* ')}}" + } + ] + } + ] + }, + "customfield_1234": "text-value", + "customfield_5678": { + "value": "select-value" + }, + "labels" : [ + "label1","label2" + ], + } +} + +alert_rule_labels = [ + { + "labelName": "tagkey1", + "labelValue": "tagval" + }, + { + "labelName": "tagkey2", + "labelValue": "tagval" + } +] + +resource_group_id = "" + +create_alert_rule_r = j1.create_alert_rule(name="4-14-25-create_alert_rule-name3", description="create_alert_rule-description", tags=['tag1', 'tag2'], + labels=alert_rule_labels, polling_interval="DISABLED", severity="INFO", - j1ql="find jupiterone_user") + j1ql="find jupiterone_user", + action_configs=create_jira_ticket_action_config, + resource_group_id=resource_group_id) print("create_alert_rule()") print(create_alert_rule_r) @@ -380,6 +430,41 @@ } ] +alert_rule_config_jira = [ + { + "integrationInstanceId" : "", + "type" : "CREATE_JIRA_TICKET", + "entityClass" : "Record", + "summary" : "Jira Task created via JupiterOne Alert Rule", + "issueType" : "Task", + "project" : "KEY", + "additionalFields" : { + "description" : { + "type" : "doc", + "version" : 1, + "content" : [ + { + "type" : "paragraph", + "content" : [ + { + "type" : "text", + "text" : "{{alertWebLink}}\n\n**Affected Items:**\n\n* {{queries.query0.data|mapProperty('displayName')|join('\n* ')}}" + } + ] + } + ] + }, + "customfield_1234": "text-value", + "customfield_5678": { + "value": "select-value" + }, + "labels" : [ + "label1","label2" + ], + } + } +] + alert_rule_config_multiple = [ { "type": "WEBHOOK", @@ -404,6 +489,19 @@ } ] +alert_rule_labels = [ + { + "labelName": "tagkey1", + "labelValue": "tagval" + }, + { + "labelName": "tagkey2", + "labelValue": "tagval" + } +] + +resource_group_id = "" + # polling_interval can be DISABLED, THIRTY_MINUTES, ONE_HOUR, FOUR_HOURS, EIGHT_HOURS, TWELVE_HOURS, ONE_DAY, or ONE_WEEK # tag_op can be OVERWRITE or APPEND # severity can be INFO, LOW, MEDIUM, HIGH, or CRITICAL diff --git a/jupiterone/client.py b/jupiterone/client.py index bb4039f..13a0c62 100644 --- a/jupiterone/client.py +++ b/jupiterone/client.py @@ -887,10 +887,12 @@ def create_alert_rule( name: str = None, description: str = None, tags: List[str] = None, + labels: List[dict] = None, polling_interval: str = None, severity: str = None, j1ql: str = None, action_configs: Dict = None, + resource_group_id: str = None, ): """Create Alert Rule Configuration in J1 account""" @@ -931,15 +933,15 @@ def create_alert_rule( }, "specVersion": 1, "tags": tags, + "labels": labels, "templates": {}, + "resourceGroupId": resource_group_id, } } 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"] @@ -962,8 +964,10 @@ def update_alert_rule( severity: str = None, tags: List[str] = None, tag_op: str = None, + labels: List[dict] = None, action_configs: List[dict] = None, action_configs_op: str = None, + resource_group_id: str = None, ): """Update Alert Rule Configuration in J1 account""" # fetch existing alert rule @@ -1020,6 +1024,10 @@ def update_alert_rule( else: tags_config = alert_rule_config["tags"] + # update labels list if provided + if labels is not None: + label_config = labels + # update action_configs list if provided if action_configs is not None: @@ -1054,6 +1062,8 @@ def update_alert_rule( "operations": operations, "pollingInterval": interval_config, "tags": tags_config, + "labels": label_config, + "resourceGroupId": resource_group_id, } } From 1e314f3a89190adb18ae2b2eb99db6809b9fb53a Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 14 Apr 2025 21:19:59 -0600 Subject: [PATCH 2/4] Create J1QLdeferredResponse.py --- examples/J1QLdeferredResponse.py | 297 +++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 examples/J1QLdeferredResponse.py diff --git a/examples/J1QLdeferredResponse.py b/examples/J1QLdeferredResponse.py new file mode 100644 index 0000000..7ca7b86 --- /dev/null +++ b/examples/J1QLdeferredResponse.py @@ -0,0 +1,297 @@ +import os +import time +import json +import requests +from requests.adapters import HTTPAdapter, Retry + + +def const(): + + # JupiterOne GraphQL API: + url = "https://graphql.us.jupiterone.io" + + # JupiterOne API creds + acct = os.environ.get("JUPITERONE_ACCOUNT") + token = os.environ.get("JUPITERONE_TOKEN") + + # J1QL query to be executed + query = "FIND *" + + return [acct, token, url, query] + + +def get_data(url, query, variables): + + headers = { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + const()[1], + 'Jupiterone-Account': const()[0] + } + + payload = { + "query": query, + "variables": variables + } + + try: + s = requests.Session() + retries = Retry(total=10, backoff_factor=1, status_forcelist=[502, 503, 504]) + s.mount('https://', HTTPAdapter(max_retries=retries)) + response = s.post(url, headers=headers, json=payload) + + if response.status_code == 200: + data = response.json() + return data + else: + raise Exception("Failed to fetch data. HTTP status code: " + str(response.status_code)) + except Exception as e: + raise Exception("POST request to JupiterOne GraphQL API failed. Exception: " + str(e)) + + +def fetch_first_result(url, query, variables): + + deferred_response = get_data(url, query, variables) + results_url = deferred_response['data']['queryV1'].get('url') + results_data = requests.get(results_url).json() + return results_data + + +def build_payload(q): + query = ''' + query J1QL($query: String!, $variables: JSON, $cursor: String, $deferredResponse: DeferredResponseOption + $flags: QueryV1Flags) { + queryV1(query: $query, variables: $variables, cursor: $cursor, deferredResponse: $deferredResponse, + flags: $flags) { + type + url + cursor + } + } + ''' + + variables = { + "query": q, + "deferredResponse": "FORCE", + "cursor": "" + } + + return query, variables + + +# def poll_deferred_response_url(url): +# +# r = requests.get(url, verify=True).json() +# max_retry = 10 +# retry_count = 0 +# retry_delay = 2 +# +# while r['status'] == "IN_PROGRESS" and retry_count < max_retry: +# r = requests.get(url, verify=True).json() +# retry_count += 1 +# time.sleep(retry_delay) +# retry_delay *= 2 +# +# if r['status'] == "COMPLETED": +# response_url = r['url'] +# return response_url +# +# else: +# formatted_json = json.dumps(r, indent=4) +# # print(formatted_json) +# +# +# def fetch_cursor_results(results): +# +# full_results = [] +# full_results.extend(results["data"]) +# +# try: +# +# while results["cursor"]: +# +# variables = build_payload(const()[3])[1] +# +# variables["cursor"] = results["cursor"] +# print(results["cursor"]) +# +# url = fetch_results_url(const()[2], +# build_payload(const()[3])[0], +# variables) +# +# print(url) +# +# # results_download_url = poll_deferred_response_url(url) +# +# r = requests.get(results_download_url, verify=True).json() +# print(r) +# # +# # full_results.extend(r["data"]) +# # +# # return full_results +# +# except Exception as e: +# err = e +# +# return err + + +def fetch_all_results(api_endpoint, query, variables=None, headers=None): + """ + Fetch all URLs from a paginated GraphQL API endpoint. + + Args: + api_endpoint (str): The GraphQL API URL to request data from. + query (str): The GraphQL query string. + variables (dict, optional): Variables for the GraphQL query. + headers (dict, optional): Headers for the request. + + Returns: + list: A list of URLs retrieved from the API. + """ + + all_results_data = [] + cursor = None # Initial cursor + first_request = True + + while True: + + # Update variables with the current cursor if available + if variables is None: + variables = {} + if cursor: + variables['cursor'] = cursor + + payload = { + "query": query, + "variables": variables + } + + try: + s = requests.Session() + retries = Retry(total=10, backoff_factor=1, status_forcelist=[502, 503, 504]) + s.mount('https://', HTTPAdapter(max_retries=retries)) + response = s.post(api_endpoint, headers=headers, json=payload) + + if response.status_code == 200: + data = response.json() + # return data + # print(data) + + # Extract URLs from the response (modify according to API response structure) + url = data.get('data', {}).get('queryV1', []).get('url', "") + + # print(url) + # print(first_request) + + if first_request and url: + # Execute a GET request to the first URL and return the contents + first_url_response = requests.get(url) + first_url_response_data = first_url_response.json()['data'] + first_url_response_url = first_url_response.json()['url'] + first_url_response_cursor = first_url_response.json()['cursor'] + # print(first_url_response_data) + # print(first_url_response_cursor) + # print(first_url_response_url) + + all_results_data.append(first_url_response_data) + + while first_url_response_cursor: + url_response = requests.get(first_url_response_url).json() + print(url_response) + cursor = url_response.get('cursor') + + all_results_data.append(url_response) + + if not cursor: + break + + + return all_results_data + + first_request = False # Mark first request as done + + # Check for next cursor + cursor = data.get('data', {}).get('cursor') + if not cursor: + break # Stop if there is no next cursor + + else: + raise Exception("Failed to fetch data. HTTP status code: " + str(response.status_code)) + except Exception as e: + raise Exception("POST request to JupiterOne GraphQL API failed. Exception: " + str(e)) + + return all_results_data + + +if __name__ == "__main__": + + deferred_response_first_result = fetch_first_result(const()[2], + build_payload(const()[3])[0], + build_payload(const()[3])[1]) + # print(deferred_response_first_result) + + all_results_data = [] + + response_status = deferred_response_first_result['status'] + response_url = deferred_response_first_result['url'] + response_data = deferred_response_first_result['data'] + response_cursor = deferred_response_first_result['cursor'] + + all_results_data.extend(response_data) + + cursor = response_cursor + + while cursor: + variables = build_payload(const()[3])[1]['cursor'] = cursor + # print(variables) + r = get_data(url=response_url, query=build_payload(const()[3])[0], variables=const()[3]) + print(r) + r_data = r['data'] + r_cursor = r['cursor'] + + all_results_data.extend(r_data) + + cursor = r_cursor + + if not cursor: + break + + + + # endpoint = const()[2] + # query = build_payload("find *")[0] + # variables = build_payload(const()[3])[1] + # # print(variables) + # headers = { + # 'Content-Type': 'application/json', + # 'Authorization': 'Bearer ' + const()[1], + # 'Jupiterone-Account': const()[0] + # } + # + # r = fetch_all_urls(endpoint, query, variables, headers) + + # print(fetch_all_urls(endpoint, query, variables, headers)) + + + + # deferredResponseURL = fetch_results_url(const()[2], + # build_payload(const()[3])[0], + # build_payload(const()[3])[1]) + # # print(deferredResponseURL) + # + # results_download_url = poll_deferred_response_url(deferredResponseURL) + # + # r = requests.get(results_download_url, verify=True).json() + # print(len(r["data"])) + # + # out = fetch_cursor_results(r) + # print(len(out)) + + + + + + # + # # # write full results data to local .json file + # # with open("J1VulnExtr.json", "w", encoding="utf-8") as outfile: + # # json.dump(full_results, outfile) From 903bceac2a9018b8788069712cfbfa6ac4a9c092 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 14 Apr 2025 21:21:13 -0600 Subject: [PATCH 3/4] Revert "Create J1QLdeferredResponse.py" This reverts commit 1e314f3a89190adb18ae2b2eb99db6809b9fb53a. --- examples/J1QLdeferredResponse.py | 297 ------------------------------- 1 file changed, 297 deletions(-) delete mode 100644 examples/J1QLdeferredResponse.py diff --git a/examples/J1QLdeferredResponse.py b/examples/J1QLdeferredResponse.py deleted file mode 100644 index 7ca7b86..0000000 --- a/examples/J1QLdeferredResponse.py +++ /dev/null @@ -1,297 +0,0 @@ -import os -import time -import json -import requests -from requests.adapters import HTTPAdapter, Retry - - -def const(): - - # JupiterOne GraphQL API: - url = "https://graphql.us.jupiterone.io" - - # JupiterOne API creds - acct = os.environ.get("JUPITERONE_ACCOUNT") - token = os.environ.get("JUPITERONE_TOKEN") - - # J1QL query to be executed - query = "FIND *" - - return [acct, token, url, query] - - -def get_data(url, query, variables): - - headers = { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + const()[1], - 'Jupiterone-Account': const()[0] - } - - payload = { - "query": query, - "variables": variables - } - - try: - s = requests.Session() - retries = Retry(total=10, backoff_factor=1, status_forcelist=[502, 503, 504]) - s.mount('https://', HTTPAdapter(max_retries=retries)) - response = s.post(url, headers=headers, json=payload) - - if response.status_code == 200: - data = response.json() - return data - else: - raise Exception("Failed to fetch data. HTTP status code: " + str(response.status_code)) - except Exception as e: - raise Exception("POST request to JupiterOne GraphQL API failed. Exception: " + str(e)) - - -def fetch_first_result(url, query, variables): - - deferred_response = get_data(url, query, variables) - results_url = deferred_response['data']['queryV1'].get('url') - results_data = requests.get(results_url).json() - return results_data - - -def build_payload(q): - query = ''' - query J1QL($query: String!, $variables: JSON, $cursor: String, $deferredResponse: DeferredResponseOption - $flags: QueryV1Flags) { - queryV1(query: $query, variables: $variables, cursor: $cursor, deferredResponse: $deferredResponse, - flags: $flags) { - type - url - cursor - } - } - ''' - - variables = { - "query": q, - "deferredResponse": "FORCE", - "cursor": "" - } - - return query, variables - - -# def poll_deferred_response_url(url): -# -# r = requests.get(url, verify=True).json() -# max_retry = 10 -# retry_count = 0 -# retry_delay = 2 -# -# while r['status'] == "IN_PROGRESS" and retry_count < max_retry: -# r = requests.get(url, verify=True).json() -# retry_count += 1 -# time.sleep(retry_delay) -# retry_delay *= 2 -# -# if r['status'] == "COMPLETED": -# response_url = r['url'] -# return response_url -# -# else: -# formatted_json = json.dumps(r, indent=4) -# # print(formatted_json) -# -# -# def fetch_cursor_results(results): -# -# full_results = [] -# full_results.extend(results["data"]) -# -# try: -# -# while results["cursor"]: -# -# variables = build_payload(const()[3])[1] -# -# variables["cursor"] = results["cursor"] -# print(results["cursor"]) -# -# url = fetch_results_url(const()[2], -# build_payload(const()[3])[0], -# variables) -# -# print(url) -# -# # results_download_url = poll_deferred_response_url(url) -# -# r = requests.get(results_download_url, verify=True).json() -# print(r) -# # -# # full_results.extend(r["data"]) -# # -# # return full_results -# -# except Exception as e: -# err = e -# -# return err - - -def fetch_all_results(api_endpoint, query, variables=None, headers=None): - """ - Fetch all URLs from a paginated GraphQL API endpoint. - - Args: - api_endpoint (str): The GraphQL API URL to request data from. - query (str): The GraphQL query string. - variables (dict, optional): Variables for the GraphQL query. - headers (dict, optional): Headers for the request. - - Returns: - list: A list of URLs retrieved from the API. - """ - - all_results_data = [] - cursor = None # Initial cursor - first_request = True - - while True: - - # Update variables with the current cursor if available - if variables is None: - variables = {} - if cursor: - variables['cursor'] = cursor - - payload = { - "query": query, - "variables": variables - } - - try: - s = requests.Session() - retries = Retry(total=10, backoff_factor=1, status_forcelist=[502, 503, 504]) - s.mount('https://', HTTPAdapter(max_retries=retries)) - response = s.post(api_endpoint, headers=headers, json=payload) - - if response.status_code == 200: - data = response.json() - # return data - # print(data) - - # Extract URLs from the response (modify according to API response structure) - url = data.get('data', {}).get('queryV1', []).get('url', "") - - # print(url) - # print(first_request) - - if first_request and url: - # Execute a GET request to the first URL and return the contents - first_url_response = requests.get(url) - first_url_response_data = first_url_response.json()['data'] - first_url_response_url = first_url_response.json()['url'] - first_url_response_cursor = first_url_response.json()['cursor'] - # print(first_url_response_data) - # print(first_url_response_cursor) - # print(first_url_response_url) - - all_results_data.append(first_url_response_data) - - while first_url_response_cursor: - url_response = requests.get(first_url_response_url).json() - print(url_response) - cursor = url_response.get('cursor') - - all_results_data.append(url_response) - - if not cursor: - break - - - return all_results_data - - first_request = False # Mark first request as done - - # Check for next cursor - cursor = data.get('data', {}).get('cursor') - if not cursor: - break # Stop if there is no next cursor - - else: - raise Exception("Failed to fetch data. HTTP status code: " + str(response.status_code)) - except Exception as e: - raise Exception("POST request to JupiterOne GraphQL API failed. Exception: " + str(e)) - - return all_results_data - - -if __name__ == "__main__": - - deferred_response_first_result = fetch_first_result(const()[2], - build_payload(const()[3])[0], - build_payload(const()[3])[1]) - # print(deferred_response_first_result) - - all_results_data = [] - - response_status = deferred_response_first_result['status'] - response_url = deferred_response_first_result['url'] - response_data = deferred_response_first_result['data'] - response_cursor = deferred_response_first_result['cursor'] - - all_results_data.extend(response_data) - - cursor = response_cursor - - while cursor: - variables = build_payload(const()[3])[1]['cursor'] = cursor - # print(variables) - r = get_data(url=response_url, query=build_payload(const()[3])[0], variables=const()[3]) - print(r) - r_data = r['data'] - r_cursor = r['cursor'] - - all_results_data.extend(r_data) - - cursor = r_cursor - - if not cursor: - break - - - - # endpoint = const()[2] - # query = build_payload("find *")[0] - # variables = build_payload(const()[3])[1] - # # print(variables) - # headers = { - # 'Content-Type': 'application/json', - # 'Authorization': 'Bearer ' + const()[1], - # 'Jupiterone-Account': const()[0] - # } - # - # r = fetch_all_urls(endpoint, query, variables, headers) - - # print(fetch_all_urls(endpoint, query, variables, headers)) - - - - # deferredResponseURL = fetch_results_url(const()[2], - # build_payload(const()[3])[0], - # build_payload(const()[3])[1]) - # # print(deferredResponseURL) - # - # results_download_url = poll_deferred_response_url(deferredResponseURL) - # - # r = requests.get(results_download_url, verify=True).json() - # print(len(r["data"])) - # - # out = fetch_cursor_results(r) - # print(len(out)) - - - - - - # - # # # write full results data to local .json file - # # with open("J1VulnExtr.json", "w", encoding="utf-8") as outfile: - # # json.dump(full_results, outfile) From 9e9780abb5cec0edcb73d37ac8632d70ae6040c8 Mon Sep 17 00:00:00 2001 From: SeaBlooms Date: Mon, 14 Apr 2025 21:28:47 -0600 Subject: [PATCH 4/4] Update examples.py --- examples/examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/examples.py b/examples/examples.py index 50fe6cc..31cc7fa 100644 --- a/examples/examples.py +++ b/examples/examples.py @@ -379,7 +379,7 @@ resource_group_id = "" -create_alert_rule_r = j1.create_alert_rule(name="4-14-25-create_alert_rule-name3", +create_alert_rule_r = j1.create_alert_rule(name="create_alert_rule-name", description="create_alert_rule-description", tags=['tag1', 'tag2'], labels=alert_rule_labels,