You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
812 lines
32 KiB
812 lines
32 KiB
#!/usr/bin/env python
|
|
# coding=utf-8
|
|
|
|
__author__ = "TrackMe Limited"
|
|
__copyright__ = "Copyright 2022-2026, TrackMe Limited, U.K."
|
|
__credits__ = "TrackMe Limited, U.K."
|
|
__license__ = "TrackMe Limited, all rights reserved"
|
|
__version__ = "0.1.0"
|
|
__maintainer__ = "TrackMe Limited, U.K."
|
|
__email__ = "support@trackme-solutions.com"
|
|
__status__ = "PRODUCTION"
|
|
|
|
# Built-in libraries
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import time
|
|
from ast import literal_eval
|
|
|
|
# Third-party libraries
|
|
import requests
|
|
import urllib3
|
|
|
|
# Logging handlers
|
|
from logging.handlers import RotatingFileHandler
|
|
|
|
# Disable insecure request warnings for urllib3
|
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
|
|
# splunk home
|
|
splunkhome = os.environ["SPLUNK_HOME"]
|
|
|
|
# set logging
|
|
filehandler = RotatingFileHandler(
|
|
"%s/var/log/splunk/trackme_splk_soar.log" % splunkhome,
|
|
mode="a",
|
|
maxBytes=10000000,
|
|
backupCount=1,
|
|
)
|
|
formatter = logging.Formatter(
|
|
"%(asctime)s %(levelname)s %(filename)s %(funcName)s %(lineno)d %(message)s"
|
|
)
|
|
logging.Formatter.converter = time.gmtime
|
|
filehandler.setFormatter(formatter)
|
|
log = logging.getLogger() # root logger - Good to get it only once.
|
|
for hdlr in log.handlers[:]: # remove the existing file handlers
|
|
if isinstance(hdlr, logging.FileHandler):
|
|
log.removeHandler(hdlr)
|
|
log.addHandler(filehandler) # set the new handler
|
|
# set the log level to INFO, DEBUG as the default is ERROR
|
|
log.setLevel(logging.INFO)
|
|
|
|
# append current directory
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
# import libs
|
|
import import_declare_test
|
|
|
|
# Import Splunk libs
|
|
from splunklib.searchcommands import (
|
|
dispatch,
|
|
GeneratingCommand,
|
|
Configuration,
|
|
Option,
|
|
validators,
|
|
)
|
|
|
|
# Import trackme libs
|
|
from trackme_libs import trackme_reqinfo
|
|
|
|
|
|
@Configuration(distributed=False)
|
|
class TrackMeRestHandler(GeneratingCommand):
|
|
soar_server = Option(
|
|
doc="""
|
|
**Syntax:** **The soar_server=****
|
|
**Description:** Mandatory, SOAR server account as configured in the Splunk App for SOAR""",
|
|
require=False,
|
|
default="*",
|
|
validate=validators.Match("url", r"^.*"),
|
|
)
|
|
|
|
action = Option(
|
|
doc="""
|
|
**Syntax:** **The action to be requested=****
|
|
**Description:** A pre-built set of actions or a single action, valid options: soar_get, soar_post, soar_test_apps, soar_automation_broker_manage""",
|
|
require=False,
|
|
default="soar_get",
|
|
validate=validators.Match(
|
|
"action",
|
|
r"^(?:soar_get|soar_post|soar_test_apps|soar_health_status|soar_health_memory|soar_health_load|soar_automation_broker_manage|soar_playbook_status)$",
|
|
),
|
|
)
|
|
|
|
action_data = Option(
|
|
doc="""
|
|
**Syntax:** **The data body for the actions to be requested=****
|
|
**Description:** Some actions may accept options, this should be a JSON formated object""",
|
|
require=False,
|
|
default=None,
|
|
validate=validators.Match("action_data", r"^\{.*\}$"),
|
|
)
|
|
|
|
action_params = Option(
|
|
doc="""
|
|
**Syntax:** **Optional extra params for the actions to be requested=****
|
|
**Description:** In some cases, you way want to add extra params to the query, this should be a JSON formated object""",
|
|
require=False,
|
|
default=None,
|
|
validate=validators.Match("action_params", r"^\{.*\}$"),
|
|
)
|
|
|
|
def generate(self, **kwargs):
|
|
# Start performance counter
|
|
start = time.time()
|
|
|
|
# Get request info and set logging level
|
|
reqinfo = trackme_reqinfo(
|
|
self._metadata.searchinfo.session_key, self._metadata.searchinfo.splunkd_uri
|
|
)
|
|
log.setLevel(reqinfo["logging_level"])
|
|
|
|
# Get the session key
|
|
session_key = self._metadata.searchinfo.session_key
|
|
|
|
# Build header and target
|
|
header = f"Splunk {session_key}"
|
|
root_url = f"{reqinfo['server_rest_uri']}/services/trackme/v2/splk_soar"
|
|
|
|
# Set HTTP headers
|
|
headers = {"Authorization": header}
|
|
headers["Content-Type"] = "application/json"
|
|
|
|
# load action_data, if any
|
|
if self.action_data:
|
|
if not isinstance(self.action_data, dict):
|
|
try:
|
|
# Try parsing as standard JSON (with double quotes)
|
|
action_data = json.loads(self.action_data)
|
|
except ValueError:
|
|
# If it fails, try parsing with ast.literal_eval (supports single quotes)
|
|
action_data = literal_eval(self.action_data)
|
|
else:
|
|
action_data = None
|
|
|
|
# load action_params, if any
|
|
if self.action_params:
|
|
if not isinstance(self.action_params, dict):
|
|
try:
|
|
# Try parsing as standard JSON (with double quotes)
|
|
action_params = json.loads(self.action_params)
|
|
except ValueError:
|
|
# If it fails, try parsing with ast.literal_eval (supports single quotes)
|
|
action_params = literal_eval(self.action_params)
|
|
else:
|
|
action_params = None
|
|
|
|
# Set session and proceed
|
|
with requests.Session() as session:
|
|
#
|
|
# Active SOAR test apps connectivity
|
|
#
|
|
|
|
if self.action == "soar_test_apps":
|
|
target_url = f"{root_url}/admin/soar_test_assets"
|
|
|
|
if not action_data:
|
|
active_check = "True"
|
|
assets_allow_list = "None"
|
|
assets_block_list = "None"
|
|
else:
|
|
# active check, if defined, otherwise defaults to True
|
|
active_check = action_data.get("active_check", "True")
|
|
|
|
# get assets_allow_list, if any
|
|
assets_allow_list = action_data.get("assets_allow_list", "None")
|
|
|
|
# get assets_block_list, if any
|
|
assets_block_list = action_data.get("assets_block_list", "None")
|
|
|
|
data = {
|
|
"soar_server": self.soar_server,
|
|
"active_check": active_check,
|
|
"assets_allow_list": assets_allow_list,
|
|
"assets_block_list": assets_block_list,
|
|
}
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
for el in response_soar: # loop and parse
|
|
yield {
|
|
"_time": el.get("mtime_epoch"),
|
|
"_raw": el,
|
|
"id": el.get("id"),
|
|
"message": el.get("message"),
|
|
"time": el.get("mtime_human"),
|
|
"name": el.get("name"),
|
|
"status": el.get("status"),
|
|
"type": el.get("type"),
|
|
}
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# SOAR health services status
|
|
#
|
|
|
|
elif self.action == "soar_health_status":
|
|
target_url = f"{root_url}/soar_get_endpoint"
|
|
|
|
data = {"soar_server": self.soar_server, "endpoint": "health"}
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
# extract and render health services status
|
|
health_status = response_soar.get("status")
|
|
|
|
for el in health_status:
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {el: health_status.get(el)},
|
|
"service": el,
|
|
"status": health_status.get(el),
|
|
}
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# SOAR health memory usage
|
|
#
|
|
|
|
elif self.action == "soar_health_memory":
|
|
target_url = f"{root_url}/soar_get_endpoint"
|
|
|
|
data = {"soar_server": self.soar_server, "endpoint": "health"}
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
# extract and render health services status
|
|
health_memory = response_soar.get("memory_data")
|
|
|
|
# write our own
|
|
mem_free = None
|
|
mem_used = None
|
|
mem_cached = None
|
|
|
|
for el in health_memory: # this is a list
|
|
if el.get("label") == "Free":
|
|
mem_free = int(el.get("value"))
|
|
if el.get("label") == "Used":
|
|
mem_used = int(el.get("value"))
|
|
if el.get("label") == "Cached":
|
|
mem_cached = int(el.get("value"))
|
|
|
|
# we can calculate things!
|
|
mem_total = mem_used + mem_free
|
|
mem_used_pct = round(mem_used / mem_total * 100, 2)
|
|
mem_cached_pct = round(mem_cached / mem_total * 100, 2)
|
|
|
|
mem_summary = {
|
|
"mem_free": mem_free,
|
|
"mem_used": mem_used,
|
|
"mem_cached": mem_cached,
|
|
"mem_used_pct": mem_used_pct,
|
|
"mem_cached_pct": mem_cached_pct,
|
|
}
|
|
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": mem_summary,
|
|
"mem_free": mem_free,
|
|
"mem_used": mem_used,
|
|
"mem_cached": mem_cached,
|
|
"mem_used_pct": mem_used_pct,
|
|
"mem_cached_pct": mem_cached_pct,
|
|
}
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# SOAR health cpu load
|
|
#
|
|
|
|
elif self.action == "soar_health_load":
|
|
target_url = f"{root_url}/soar_get_endpoint"
|
|
|
|
data = {"soar_server": self.soar_server, "endpoint": "health"}
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
# extract and render health services status
|
|
health_load = response_soar.get("load_data")
|
|
load_summary = {}
|
|
count = 0
|
|
|
|
for (
|
|
el
|
|
) in (
|
|
health_load
|
|
): # this is a list, load come in order from 1 to 15min
|
|
count += 1
|
|
if count == 1:
|
|
load_summary["load_1min"] = el.get("load")
|
|
if count == 2:
|
|
load_summary["load_5min"] = el.get("load")
|
|
if count == 3:
|
|
load_summary["load_15min"] = el.get("load")
|
|
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": load_summary,
|
|
"load_1min": load_summary.get("load_1min"),
|
|
"load_5min": load_summary.get("load_5min"),
|
|
"load_15min": load_summary.get("load_15min"),
|
|
}
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# get query against a SOAR endpoint
|
|
#
|
|
|
|
elif self.action == "soar_get":
|
|
target_url = f"{root_url}/soar_get_endpoint"
|
|
|
|
data = {
|
|
"soar_server": self.soar_server,
|
|
"endpoint": action_data.get("endpoint"),
|
|
}
|
|
|
|
# if extra params are provided, add them to the data
|
|
if action_params:
|
|
data["params"] = action_params
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
if isinstance(response_soar, dict):
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": response_soar,
|
|
}
|
|
|
|
elif isinstance(response_soar, list):
|
|
if len(response_soar) > 0: # if not an empty list
|
|
for el in response_soar: # this is a list
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": el,
|
|
}
|
|
else:
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"response": "REST API call was successful, but an empty response was received.",
|
|
"response.status_code": response.status_code,
|
|
"response.text": response.text,
|
|
}, # return index as key and element as value
|
|
}
|
|
|
|
else: # something else
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": response_soar,
|
|
}
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# post query against a SOAR endpoint
|
|
#
|
|
|
|
elif self.action == "soar_post":
|
|
target_url = f"{root_url}/admin/soar_post_endpoint"
|
|
|
|
data = {
|
|
"soar_server": self.soar_server,
|
|
"endpoint": action_data.get("endpoint"),
|
|
"data": action_data.get("data"),
|
|
}
|
|
|
|
# if extra params are provided, add them to the data
|
|
if action_params:
|
|
data["params"] = action_params
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
if isinstance(response_soar, list): # if the response is a list
|
|
if len(response_soar) > 0: # if response if not empty list
|
|
for idx, el in enumerate(
|
|
response_soar
|
|
): # enumerate to keep track of index
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
idx: el
|
|
}, # return index as key and element as value
|
|
}
|
|
else:
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"response": "REST API call was successful, but an empty response was received, this can be expected if there were no operations to be performed.",
|
|
"response.status_code": response.status_code,
|
|
"response.text": response.text,
|
|
}, # return index as key and element as value
|
|
}
|
|
|
|
elif isinstance(
|
|
response_soar, dict
|
|
): # if the response is a dictionary
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": response_soar,
|
|
}
|
|
else: # if the response is neither a list nor a dictionary
|
|
logging.error(
|
|
f"Unexpected response_soar type: {type(response_soar)}"
|
|
)
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# Automation Broker advanced management
|
|
#
|
|
|
|
elif self.action == "soar_automation_broker_manage":
|
|
target_url = f"{root_url}/admin/soar_automation_broker_manage"
|
|
|
|
data = {
|
|
"soar_server": self.soar_server,
|
|
}
|
|
|
|
# optional
|
|
if action_data:
|
|
mode = action_data.get("mode", None)
|
|
if mode:
|
|
data["mode"] = mode
|
|
|
|
#
|
|
# pool definition
|
|
#
|
|
|
|
automation_brokers_pool_members = action_data.get(
|
|
"automation_brokers_pool_members", None
|
|
)
|
|
if automation_brokers_pool_members:
|
|
data["automation_brokers_pool_members"] = (
|
|
automation_brokers_pool_members
|
|
)
|
|
|
|
#
|
|
# active1/active2, these options are deprecated and left for compatibility purposes
|
|
#
|
|
|
|
automation_active1_broker_name = action_data.get(
|
|
"automation_active1_broker_name", None
|
|
)
|
|
if automation_active1_broker_name:
|
|
data["automation_active1_broker_name"] = (
|
|
automation_active1_broker_name
|
|
)
|
|
|
|
automation_active2_broker_name = action_data.get(
|
|
"automation_active2_broker_name", None
|
|
)
|
|
if automation_active2_broker_name:
|
|
data["automation_active2_broker_name"] = (
|
|
automation_active2_broker_name
|
|
)
|
|
|
|
assets_update_forbidden_fields = action_data.get(
|
|
"assets_update_forbidden_fields", None
|
|
)
|
|
if assets_update_forbidden_fields:
|
|
data["assets_update_forbidden_fields"] = (
|
|
assets_update_forbidden_fields
|
|
)
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code == 200: # we stricly expect a 200 HTTP code
|
|
response_json = response.json() # our response is always json
|
|
response_soar = response_json.get(
|
|
"response"
|
|
) # get response inside response
|
|
logging.debug(
|
|
f'response_soar="{json.dumps(response_soar)}"'
|
|
) # debug
|
|
|
|
if isinstance(response_soar, list): # if the response is a list
|
|
if len(response_soar) > 0: # if response if not empty list
|
|
for el in response_soar:
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": el,
|
|
}
|
|
else:
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"response": "REST API call was successful, but an empty response was received, this can be expected if there were no operations to be performed.",
|
|
"response.status_code": response.status_code,
|
|
"response.text": response.text,
|
|
}, # return index as key and element as value
|
|
}
|
|
|
|
elif isinstance(
|
|
response_soar, dict
|
|
): # if the response is a dictionary
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": response_soar,
|
|
}
|
|
else: # if the response is neither a list nor a dictionary
|
|
logging.error(
|
|
f"Unexpected response_soar type: {type(response_soar)}"
|
|
)
|
|
|
|
else:
|
|
error_msg = f'request has failed, response.status_code="{response.status_code}", response.text="{response.text}"'
|
|
logging.error(error_msg)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
|
|
#
|
|
# Playbook status
|
|
#
|
|
|
|
elif self.action == "soar_playbook_status":
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
target_url = f"{root_url}/soar_get_endpoint"
|
|
|
|
# Set default page_size
|
|
page_size = 1000
|
|
# Override page_size if specified in action_params
|
|
if action_params and "page_size" in action_params:
|
|
try:
|
|
page_size = int(action_params.get("page_size"))
|
|
logging.debug(f"Using custom page_size: {page_size}")
|
|
except (ValueError, TypeError) as e:
|
|
logging.error(
|
|
f"Invalid page_size value in action_params: {str(e)}"
|
|
)
|
|
# Keep default page_size if invalid value provided
|
|
|
|
# Set default max_age_sec
|
|
max_age_sec = 300
|
|
# Override max_age_sec if specified in action_params
|
|
if action_params and "max_age_sec" in action_params:
|
|
try:
|
|
max_age_sec = int(action_params.get("max_age_sec"))
|
|
logging.debug(f"Using custom max_age_sec: {max_age_sec}")
|
|
except (ValueError, TypeError) as e:
|
|
logging.error(
|
|
f"Invalid max_age_sec value in action_params: {str(e)}"
|
|
)
|
|
# Keep default max_age_sec if invalid value provided
|
|
# define cutoff time
|
|
cutoff_time = datetime.now(timezone.utc) - timedelta(
|
|
seconds=max_age_sec
|
|
)
|
|
filter_update_time_gt = (
|
|
f"\"{cutoff_time.strftime('%Y-%m-%dT%H:%M:%SZ')}\""
|
|
)
|
|
logging.info(
|
|
f"Applying max_age_sec filter: update_time > {filter_update_time_gt}"
|
|
)
|
|
|
|
# init page
|
|
page = 0
|
|
|
|
# Start with known statuses
|
|
known_statuses = [
|
|
"success",
|
|
"failed",
|
|
"running",
|
|
"pending",
|
|
"cancelled",
|
|
"waiting",
|
|
]
|
|
status_summary = {status: 0 for status in known_statuses}
|
|
|
|
# process loop
|
|
while True:
|
|
params = {"page_size": page_size, "page": page}
|
|
if filter_update_time_gt:
|
|
params["_filter_update_time__gt"] = filter_update_time_gt
|
|
|
|
data = {
|
|
"soar_server": self.soar_server,
|
|
"endpoint": "playbook_run",
|
|
"params": params,
|
|
}
|
|
|
|
response = session.post(
|
|
target_url, headers=headers, verify=False, data=json.dumps(data)
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
logging.error(
|
|
f"Playbook run fetch failed on page {page}: {response.status_code}, {response.text}"
|
|
)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
},
|
|
"action": "failed",
|
|
"response": response.text,
|
|
"http_status_code": response.status_code,
|
|
}
|
|
return
|
|
|
|
response_json = response.json()
|
|
response_data = response_json.get("response", [])
|
|
|
|
if not isinstance(response_data, list):
|
|
logging.error(
|
|
f"Unexpected response format: {type(response_data)}"
|
|
)
|
|
yield {
|
|
"_time": time.time(),
|
|
"_raw": {
|
|
"action": "failed",
|
|
"response": response_json,
|
|
"error": "Unexpected response structure (not a list)",
|
|
},
|
|
}
|
|
return
|
|
|
|
if not response_data:
|
|
break # No more data
|
|
|
|
for pb in response_data:
|
|
status = pb.get("status", "unknown")
|
|
if status not in status_summary:
|
|
status_summary[status] = 0 # Dynamically track unknowns
|
|
status_summary[status] += 1
|
|
|
|
if len(response_data) < page_size:
|
|
break
|
|
page += 1
|
|
|
|
yield {"_time": time.time(), "_raw": status_summary, **status_summary}
|
|
|
|
# Log the run time
|
|
logging.info(
|
|
f"trackmesoar has terminated, run_time={round(time.time() - start, 3)}"
|
|
)
|
|
|
|
|
|
dispatch(TrackMeRestHandler, sys.argv, sys.stdin, sys.stdout, __name__)
|