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.
Splunk_Deploiement/apps/trackme/bin/trackme_rest_handler_config...

2505 lines
109 KiB

#!/usr/bin/env python
# coding=utf-8
__name__ = "trackme_rest_handler_configuration.py"
__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 os
import sys
import time
import datetime
import requests
import random
from collections import OrderedDict
import urllib.parse
# splunk home
splunkhome = os.environ["SPLUNK_HOME"]
# append current directory
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# import libs
import import_declare_test
# set logging
from trackme_libs_logging import setup_logger
logger = setup_logger(
"trackme.rest.configuration_admin", "trackme_rest_api_configuration_admin.log"
)
# Redirect global logging to use the same handler
import logging
logging.getLogger().handlers = logger.handlers
logging.getLogger().setLevel(logger.level)
# import test handler
import trackme_rest_handler
# import trackme libs
from trackme_libs import (
trackme_getloglevel,
trackme_create_report,
trackme_delete_report,
trackme_create_kvtransform,
trackme_delete_kvtransform,
trackme_create_macro,
trackme_delete_macro,
trackme_create_kvcollection,
trackme_delete_kvcollection,
)
# import the collections dict
from collections_data import vtenant_account_default, remote_account_default
# import Splunk libs
import splunklib.client as client
class TrackMeHandlerConfigurationAdmin_v2(trackme_rest_handler.RESTHandler):
def __init__(self, command_line, command_arg):
super(TrackMeHandlerConfigurationAdmin_v2, self).__init__(
command_line, command_arg, logger
)
def get_resource_group_desc_configuration(self, request_info, **kwargs):
response = {
"resource_group_name": "configuration/admin",
"resource_group_desc": "These endpoints provide various generic application level configuration capabilities (admin operations)",
}
return {"payload": response, "status": 200}
# Create a Kvstore transforms with privileges escalation
def post_create_kvtransform(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
transform_name = resp_dict["transform_name"]
transform_fields = resp_dict["transform_fields"]
collection_name = resp_dict["collection_name"]
transform_acl = resp_dict["transform_acl"]
owner = resp_dict["owner"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows creating a KVstore transforms knowledge object, it requires a POST with the following options:",
"resource_desc": "Create KVstore transforms",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/create_kvstore_transforms" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"transform_name\\": \\"<transform_name>\\", \\"transform_fields\\": \\"<transform_fields>\\", \\"collection_name\\": \\"<collection_name>\\", \\"owner\\": \\"<owner>\\", \\"transform_acl\\": \\"<transform_acl>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_create = trackme_create_kvtransform(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
transform_name,
transform_fields,
collection_name,
owner,
transform_acl,
)
return {"payload": action_create, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to create the transform definition, transform="{transform_name}", exception="{str(e)}"'
# Check if this is a 409 Conflict error (object already exists)
if "409 Conflict" in str(e) or "already exists" in str(e):
warning_msg = f'tenant_id="{tenant_id}", transform "{transform_name}" already exists, skipping creation'
logger.warning(warning_msg)
return {
"payload": {
"result": "warning",
"message": warning_msg,
"transform_name": transform_name,
"details": "The transform already exists and was not created",
},
"status": 202,
}
else:
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Delete a Kvstore transforms with privileges escalation
def post_delete_kvtransform(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
transform_name = resp_dict["transform_name"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows deleting a KVstore transforms knowledge object, it requires a POST with the following options:",
"resource_desc": "Delete KVstore transforms",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/delete_kvstore_transforms" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"transform_name\\": \\"<transform_name>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_delete = trackme_delete_kvtransform(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
transform_name,
)
return {"payload": action_delete, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to delete the transform definition, transform="{transform_name}", exception="{str(e)}"'
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Delete a Kvstore collection with privileges escalation
def post_delete_kvcollection(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
collection_name = resp_dict["collection_name"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows deleting a KVstore collection, it requires a POST with the following options:",
"resource_desc": "Delete KVstore collection",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/delete_kvstore_collection" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"collection_name\\": \\"<collection_name>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_delete = trackme_delete_kvcollection(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
collection_name,
)
return {"payload": action_delete, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to delete the KVstore collection, collection="{collection_name}", exception="{str(e)}"'
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Create a report with privileges escalation
def post_create_report(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
report_name = resp_dict["report_name"]
report_search = resp_dict["report_search"]
report_properties = resp_dict["report_properties"]
report_acl = resp_dict["report_acl"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows creating a TrackMe report knowledge object, it requires a POST with the following options:",
"resource_desc": "Create TrackMe report",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/create_report" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"report_name\\": \\"<report_name>\\", \\"report_search\\": \\"<report_search>\\", \\"report_properties\\": \\"<report_properties>\\", \\"report_acl\\": \\"<report_acl>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_create = trackme_create_report(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
report_name,
report_search,
report_properties,
report_acl,
)
return {"payload": action_create, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to create the report definition, report="{report_name}", exception="{str(e)}"'
# Check if this is a 409 Conflict error (object already exists)
if "409 Conflict" in str(e) or "already exists" in str(e):
warning_msg = f'tenant_id="{tenant_id}", report "{report_name}" already exists, skipping creation'
logger.warning(warning_msg)
return {
"payload": {
"result": "warning",
"message": warning_msg,
"report_name": report_name,
"details": "The report already exists and was not created",
},
"status": 202,
}
else:
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Delete a report with privileges escalation
def post_delete_report(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
report_name = resp_dict["report_name"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows deleting a TrackMe report knowledge object, it requires a POST with the following options:",
"resource_desc": "Create delete report",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/delete_report" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"report_name\\": \\"<report_name>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_delete = trackme_delete_report(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
report_name,
)
return {"payload": action_delete, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to delete the report definition, report="{report_name}", exception="{str(e)}"'
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Update a report with privileges escalation
def post_update_report(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
report_name = resp_dict["report_name"]
report_search = resp_dict["report_search"]
# Retrieving earliest and latest time from the request
earliest_time = resp_dict.get("earliest_time")
latest_time = resp_dict.get("latest_time")
# schedule_window from the request
schedule_window = resp_dict.get("schedule_window")
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows updating a TrackMe report knowledge object, it requires a POST with the following options:",
"resource_desc": "Update TrackMe report",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/update_report" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"report_name\\": \\"<report_name>\\", \\"report_search\\": \\"<report_search>\\"}"',
}
return {"payload": response, "status": 200}
# get service
service = client.connect(
owner="nobody",
app="trackme",
port=request_info.server_rest_port,
token=request_info.system_authtoken,
timeout=600,
)
# update the report
report_current = service.saved_searches[report_name]
try:
update_params = {"search": report_search}
if earliest_time is not None:
update_params["dispatch.earliest_time"] = earliest_time
if latest_time is not None:
update_params["dispatch.latest_time"] = latest_time
if schedule_window is not None:
update_params["schedule_window"] = schedule_window
action_update = report_current.update(**update_params)
return {
"payload": {
"action": "success",
"response": "The report was updated successfully",
"report_name": report_name,
},
"status": 200,
}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to update the report definition, report="{report_name}", exception="{str(e)}"'
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Create a macro with privileges escalation
def post_create_macro(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
macro_name = resp_dict["macro_name"]
macro_definition = resp_dict["macro_definition"]
macro_owner = resp_dict["macro_owner"]
macro_acl = resp_dict["macro_acl"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows creating a macro knowledge object, it requires a POST with the following options:",
"resource_desc": "Create macro",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/create_macro" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"macro_name\\": \\"<macro_name>\\", \\"macro_definition\\": \\"<macro_definition>\\", \\"macro_owner\\": \\"<macro_owner>\\", \\"macro_acl\\": \\"<macro_acl>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_create = trackme_create_macro(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
macro_name,
macro_definition,
macro_owner,
macro_acl,
)
return {"payload": action_create, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to create the macro definition, macro="{macro_name}", exception="{str(e)}"'
# Check if this is a 409 Conflict error (object already exists)
if "409 Conflict" in str(e) or "already exists" in str(e):
warning_msg = f'tenant_id="{tenant_id}", macro "{macro_name}" already exists, skipping creation'
logger.warning(warning_msg)
return {
"payload": {
"result": "warning",
"message": warning_msg,
"macro_name": macro_name,
"details": "The macro already exists and was not created",
},
"status": 202,
}
else:
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Update a macro with privileges escalation
def post_update_macro(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
macro_name = resp_dict["macro_name"]
macro_definition = resp_dict["macro_definition"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows updating a TrackMe macro knowledge object, it requires a POST with the following options:",
"resource_desc": "update TrackMe macro",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/update_macro" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"macro_name\\": \\"<macro_name>\\", \\"macro_definition\\": \\"<macro_definition>\\"}"',
}
return {"payload": response, "status": 200}
# get service
service = client.connect(
owner="nobody",
app="trackme",
port=request_info.server_rest_port,
token=request_info.system_authtoken,
timeout=600,
)
# update the macro
macro_current = service.confs["macros"][macro_name]
try:
action_update = macro_current.update(definition=macro_definition)
return {
"payload": {
"action": "success",
"response": "The macro was updated successfully",
"macro_name": macro_name,
},
"status": 200,
}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to update the macro definition, macro="{macro_name}", exception="{str(e)}"'
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Delete a macro with privileges escalation
def post_delete_macro(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
macro_name = resp_dict["macro_name"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows deleting a macro knowledge object, it requires a POST with the following options:",
"resource_desc": "Delete macro",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/delete_macro" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"macro_name\\": \\"<macro_name>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_delete = trackme_delete_macro(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
macro_name,
)
return {"payload": action_delete, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to delete the macro definition, macro="{macro_name}", exception="{str(e)}"'
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Create a KVstore collection with privileges escalation
def post_create_kvcollection(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
collection_name = resp_dict["collection_name"]
collection_acl = resp_dict["collection_acl"]
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows creating a Kvstore collection, it requires a POST with the following options:",
"resource_desc": "Create KVstore collection",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/create_kvcollection" body="{\\"tenant_id\\": \\"<tenant_id>\\", \\"collection_name\\": \\"<collection_name>\\", \\"collection_acl\\": \\"<collection_acl>\\"}"',
}
return {"payload": response, "status": 200}
# create the transform
try:
action_create = trackme_create_kvcollection(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
collection_name,
collection_acl,
)
return {"payload": action_create, "status": 200}
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to create the KVstore collection, collection="{collection_name}", exception="{str(e)}"'
# Check if this is a 409 Conflict error (object already exists)
if "409 Conflict" in str(e) or "already exists" in str(e):
warning_msg = f'tenant_id="{tenant_id}", KVstore collection "{collection_name}" already exists, skipping creation'
logger.warning(warning_msg)
return {
"payload": {
"result": "warning",
"message": warning_msg,
"collection_name": collection_name,
"details": "The KVstore collection already exists and was not created",
},
"status": 202,
}
else:
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Verify and update the Virtual Tenant account with privileges escalation
def post_maintain_vtenant_account(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
# updated_vtenant_data is optional
try:
updated_vtenant_data = resp_dict["updated_vtenant_data"]
# if not a dictionnary, attempt to load it
if not isinstance(updated_vtenant_data, dict):
try:
updated_vtenant_data = json.loads(updated_vtenant_data)
except Exception as e:
error_msg = f"failed to load updated_vtenant_data, exception={str(e)}"
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
except Exception as e:
updated_vtenant_data = None
# force_create_missing is optional, accept true/false case insensitive and turn it into a boolean
try:
force_create_missing = resp_dict["force_create_missing"]
if not isinstance(force_create_missing, bool):
force_create_missing = force_create_missing.lower()
if force_create_missing in ("true"):
force_create_missing = True
elif force_create_missing in ("false"):
force_create_missing = False
else:
return {
"payload": "force_create_missing must be a boolean, true or false",
"status": 500,
}
except Exception as e:
force_create_missing = False
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoints allows verifying and updating the Virtual Tenant account with privileges escalation, it requires a POST with the following options:",
"resource_desc": "Create KVstore collection",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/maintain_vtenant_account" body="{\\"tenant_id\\": \\"<tenant_id>\\"}"',
"options": [
{
"tenant_id": "The Virtual Tenant ID",
"updated_vtenant_data": "Optional, a dictionary with the updated Virtual Tenant data",
"force_create_missing": "Optional, if set to true, will force the creation of the Virtual Tenant account if it entirely missing, defaults to false.",
}
],
}
return {"payload": response, "status": 200}
#
# main
#
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_vtenants/{tenant_id}"
vtenant_data = {}
# vtenant_account_found boolean
vtenant_account_found = False
# update boolean - if the current of the Virtual Tenants is missing any key from the default config, we need to update it
update_is_required = False
try:
# Get current vtenant account configuration
response = requests.get(
url,
headers={"Authorization": f"Splunk {request_info.system_authtoken}"},
verify=False,
params={"output_mode": "json"},
timeout=600,
)
if response.status_code in (200, 201, 204):
logger.info(f"successfully retrieved vtenant configuration")
vtenant_data_json = response.json()
vtenant_data_current = vtenant_data_json["entry"][0]["content"]
# Set vtenant_account_found to True
vtenant_account_found = True
# Start with the current configuration and add missing keys from the default config
# We keep all keys from the current configuration
for key, value in vtenant_data_current.items():
if key in vtenant_account_default:
vtenant_data[key] = value
# before merging with the default config, check if any key is missing
for key in vtenant_account_default.keys():
if key not in vtenant_data:
update_is_required = True
# Merge with default config, only adding missing default keys
for key, value in vtenant_account_default.items():
if key not in vtenant_data:
vtenant_data[key] = value
# If updated_vtenant_data is provided, it takes precedence over the defaults and current config
if updated_vtenant_data:
vtenant_data.update(updated_vtenant_data)
# Finally, ensures that each key in vtenant_data exists in vtenant_account_default, otherwise drop it
vtenant_data = {
key: value
for key, value in vtenant_data.items()
if key in vtenant_account_default
}
logger.info(
f'vtenant_data="{json.dumps(vtenant_data, indent=2)}", update_is_required={update_is_required}'
)
else:
error_msg = f"failed to retrieve vtenant configuration, status_code={response.status_code}"
logger.error(error_msg)
except Exception as e:
error_msg = f"failed to retrieve vtenant configuration, exception={str(e)}"
logger.error(error_msg)
# init return_response
return_response = {}
#
# if Virtual Tenant account is not found
#
# not found and force_create_missing is set to true
if not vtenant_account_found and force_create_missing:
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_vtenants"
# load default vtenant config
data = dict(vtenant_account_default)
# add the name value
data["name"] = tenant_id
# Retrieve and set the tenant idx, if any failure, logs and use the global index
try:
response = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
data=data,
params={"output_mode": "json"},
verify=False,
timeout=600,
)
if response.status_code not in (200, 201, 204):
# set response
return_response["result"] = "failed"
return_response["message"] = (
f"failed to create vtenant account, status_code={response.status_code}, response={response.text}"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 500}
else:
# set response
return_response["result"] = "success"
return_response["message"] = (
f"vtenant account created successfully, status_code={response.status_code}"
)
return_response["vtenant_account"] = data
# log
logger.info(return_response.get("message"))
# return response
return {"payload": return_response, "status": 200}
except Exception as e:
# set response
return_response["result"] = "failed"
return_response["message"] = (
f"failed to create vtenant account, exception={str(e)}"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return response
return {"payload": return_response, "status": 500}
# not found and force_create_missing is not set to true
elif not vtenant_account_found and not force_create_missing:
# set response
return_response["result"] = "failed"
return_response["message"] = (
f"vtenant account not found and force_create_missing is not set to true"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 500}
#
# main: Virtual Tenant account is found, if update is required, proceed with updating, otherwise return the account
#
# no update required
if not update_is_required:
# set response
return_response["result"] = "success"
return_response["message"] = (
f"vtenant configuration checked successfully, no update required, status_code={response.status_code}"
)
return_response["vtenant_account"] = vtenant_data
# log
logger.info(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 200}
# update required
else:
try:
logger.info(
f'attempting to update vtenant configuration, vtenant_data="{json.dumps(vtenant_data, indent=2)}"'
)
response = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
data=vtenant_data,
verify=False,
timeout=600,
)
if response.status_code in (200, 201, 204):
# set response
return_response["result"] = "success"
return_response["message"] = (
f"vtenant configuration updated successfully, status_code={response.status_code}"
)
return_response["vtenant_account"] = vtenant_data
# log
logger.info(return_response.get("message"))
# return response
return {"payload": return_response, "status": 200}
else:
# set return_response
return_response["result"] = "failed"
return_response["message"] = (
f"failed to update vtenant configuration, status_code={response.status_code}, response={response.text}"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 500}
except Exception as e:
# set response
return_response["result"] = "failed"
return_response["message"] = (
f"failed to update vtenant configuration, exception={str(e)}"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return response
return {"payload": return_response, "status": 500}
# Retrieve the Virtual Tenant account configuration
def post_get_vtenant_account(self, request_info, **kwargs):
# Initialize response
return_response = {}
describe = False
try:
# Parse the request payload
resp_dict = json.loads(str(request_info.raw_args["payload"]))
describe = resp_dict.get("describe", False)
# Convert describe to boolean if it's a string
if isinstance(describe, str) and describe.lower() in ("true", "false"):
describe = describe.lower() == "true"
tenant_id = resp_dict.get("tenant_id")
if not tenant_id and not describe:
return {
"payload": "Missing tenant_id in the request payload.",
"status": 400,
}
except Exception as e:
if not describe:
error_msg = f"Invalid payload format: {str(e)}"
logger.error(error_msg)
return {"payload": error_msg, "status": 400}
# If describe is requested, return the endpoint usage details
if describe:
response = {
"describe": "This endpoint retrieves the Virtual Tenant account configuration, it requires a POST with the following options",
"resource_desc": "Retrieve Virtual Tenant account",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/get_vtenant_account" body="{\\"tenant_id\\": \\"<tenant_id>\\"}"',
"options": [
{
"tenant_id": "The Virtual Tenant ID",
"describe": "Optional, if set to true, returns the endpoint description and usage details.",
}
],
}
return {"payload": response, "status": 200}
# Build the URL for retrieving the Virtual Tenant account
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_vtenants/{tenant_id}"
try:
# Send a GET request to retrieve the account configuration
response = requests.get(
url,
headers={"Authorization": f"Splunk {request_info.system_authtoken}"},
verify=False,
params={"output_mode": "json"},
timeout=600,
)
if response.status_code in (200, 201, 204):
# Parse the response JSON
vtenant_data_json = response.json()
# Extract the account content
if "entry" in vtenant_data_json and len(vtenant_data_json["entry"]) > 0:
vtenant_data = vtenant_data_json["entry"][0]["content"]
# Set successful response
return_response["result"] = "success"
return_response["message"] = (
"Virtual Tenant account retrieved successfully."
)
return_response["vtenant_account"] = vtenant_data
logger.info(return_response["message"])
return {"payload": return_response, "status": 200}
else:
# Handle case where account content is not found
return_response["result"] = "failed"
return_response["message"] = (
"Virtual Tenant account content not found."
)
return_response["vtenant_account"] = None
logger.error(return_response["message"])
return {"payload": return_response, "status": 404}
else:
# Handle HTTP error responses
error_msg = f"Failed to retrieve Virtual Tenant account, status_code={response.status_code}, response={response.text}"
logger.error(error_msg)
return {"payload": error_msg, "status": response.status_code}
except Exception as e:
# Handle exceptions during the GET request
error_msg = (
f"Exception occurred while retrieving Virtual Tenant account: {str(e)}"
)
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
# Verify and update the Splunk Remote Accounts with privileges escalation
def post_maintain_remote_account(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
# accounts, if not a list, attempt to load it from csv
accounts = resp_dict.get("accounts", "*")
if not isinstance(accounts, list) and accounts != "*":
accounts = accounts.split(",")
# show_token is optional, accept true/false case insensitive and turn it into a boolean
try:
show_token = resp_dict["show_token"]
if isinstance(show_token, str):
show_token = show_token.lower()
if show_token in ("true"):
show_token = True
elif show_token in ("false"):
show_token = False
elif isinstance(show_token, bool):
pass
elif isinstance(show_token, int):
show_token = bool(show_token)
else:
return {
"payload": "show_token must be a boolean, true or false",
"status": 500,
}
except Exception as e:
show_token = False
# force_tokens_rotation is optional, accept true/false case insensitive and turn it into a boolean
try:
force_tokens_rotation = resp_dict["force_tokens_rotation"]
if isinstance(force_tokens_rotation, str):
force_tokens_rotation = force_tokens_rotation.lower()
if force_tokens_rotation in ("true"):
force_tokens_rotation = True
elif force_tokens_rotation in ("false"):
force_tokens_rotation = False
elif isinstance(force_tokens_rotation, bool):
pass
elif isinstance(force_tokens_rotation, int):
force_tokens_rotation = bool(force_tokens_rotation)
else:
return {
"payload": "force_tokens_rotation must be a boolean, true or false",
"status": 500,
}
except Exception as e:
force_tokens_rotation = False
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint maintains (verify and update parameters, peform bearer token rotation) Splunk Remote Accounts, it requires a POST with the following options:",
"resource_desc": "Verify, maintain and tokens rotation for Splunk Remote Accounts",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/maintain_remote_account" body="{\\"accounts\\": \\"<comma separated list of accounts, use * to target all existing accounts>\\"}"',
"options": [
{
"accounts": "comma separated list of accounts, use * to target all existing accounts",
"show_token": "Optional, if set to true, will show the bearer token value in the response, defaults to false.",
"force_tokens_rotation": "Optional, if set to true, will force the rotation of the bearer tokens, defaults to false.",
}
],
}
return {"payload": response, "status": 200}
#
# main
#
# Get splunkd port
splunkd_port = request_info.server_rest_port
# Get service
service = client.connect(
owner="nobody",
app="trackme",
port=splunkd_port,
token=request_info.session_key,
timeout=600,
)
# set loglevel
loglevel = trackme_getloglevel(
request_info.system_authtoken, request_info.server_rest_port
)
logger.setLevel(loglevel)
# Data collection
collection_name = "kv_trackme_remote_account_token_expiration"
collection = service.kvstore[collection_name]
# init return_response
return_response = {}
# init warnings counters
warnings_count = 0
warnings_list = []
# init errors counters
errors_count = 0
errors_list = []
# init actions_list
actions_list = []
def is_reachable(session, url, timeout):
try:
session.get(url, timeout=timeout, verify=False)
return True, None
except Exception as e:
return False, str(e)
def select_url(session, splunk_url, timeout=15):
splunk_urls = splunk_url.split(",")
unreachable_errors = []
reachable_urls = []
for url in splunk_urls:
reachable, error = is_reachable(session, url, timeout)
if reachable:
reachable_urls.append(url)
else:
unreachable_errors.append((url, error))
selected_url = random.choice(reachable_urls) if reachable_urls else False
return selected_url, unreachable_errors
def establish_remote_service(
account,
parsed_url,
bearer_token,
app_namespace,
timeout=600,
):
try:
service = client.connect(
host=parsed_url.hostname,
splunkToken=str(bearer_token),
owner="nobody",
app=app_namespace,
port=parsed_url.port,
autologin=True,
timeout=timeout,
)
remote_apps = [app.label for app in service.apps]
if remote_apps:
logger.info(
f'endpoint=maintain_remote_account, remote search connectivity check for account="{account}" with host="{parsed_url.hostname}" on port="{parsed_url.port}" was successful'
)
return service
except Exception as e:
error_msg = f'Remote search for account="{account}" has failed at connectivity check, host="{parsed_url.hostname}" on port="{parsed_url.port}" with exception="{str(e)}"'
raise Exception(error_msg)
def get_all_accounts():
"""
Update the configuration of any exising remote account, to ensure that the configuration is up to date.
:param reqinfo: dict containing Splunk session information (e.g., server URI, session key).
:param task_name: Name of the task for logger.purposes.
:param task_instance_id: ID of the task instance for logger.purposes.
:param tenant_id: ID of the vtenant.
:param default_account_values: manadatory dict of default values.
"""
# endpoint target
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_account"
# current_remote_accounts_dict
current_remote_accounts_dict = {}
# current_remote_accounts_list
current_remote_accounts_list = []
# first, get the list of remote accounts
try:
response = requests.get(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
params={
"output_mode": "json",
"count": -1,
},
timeout=600,
)
response.raise_for_status()
response_json = response.json()
# The list of remote accounts is stored as a list in entry
remote_accounts = response_json.get("entry", [])
# iterate through the remote accounts, adding them to the dict, name is the key, then we care about "content" which is a dict of our parameters
# for this account
for remote_account in remote_accounts:
remote_account_name = remote_account.get("name", None)
remote_account_content = remote_account.get("content", {})
# add to list
current_remote_accounts_list.append(remote_account_name)
if remote_account_name and remote_account_content:
# from remote_account_content, remove the following fields: bearer_token, disabled, eai:acl, eai:appName, eai:userName
remote_account_content.pop("bearer_token", None)
remote_account_content.pop("disabled", None)
remote_account_content.pop("eai:acl", None)
remote_account_content.pop("eai:appName", None)
remote_account_content.pop("eai:userName", None)
# add to the dict
current_remote_accounts_dict[remote_account_name] = (
remote_account_content
)
return current_remote_accounts_list, current_remote_accounts_dict
except Exception as e:
logger.error(
f"endpoint=maintain_remote_account, error while fetching remote account list: {str(e)}"
)
return False
def check_and_update_accounts(
accounts,
default_account_values,
warnings_count=0,
warnings_list=[],
errors_count=0,
errors_list=[],
actions_list=[],
):
# Second, iterate through our current_remote_accounts_dict, if any of the account is missing key/values from the default_account_values, we will update it
# running a POST request to the remote account endpoint
# in memory dict to store bearer tokens per account
current_accounts_secrets = {}
for remote_account_name in current_remote_accounts_dict:
current_account_config = current_remote_accounts_dict[
remote_account_name
]
if remote_account_name in accounts or accounts == "*":
# run a request against /services/trackme/v2/configuration/get_remote_account, body{'account': 'myaccount'} to retrieve the current bearer_token value (field token) and add to the content
try:
url = f"{request_info.server_rest_uri}/services/trackme/v2/configuration/get_remote_account"
data = {
"account": remote_account_name,
}
response_account_secret = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
data=json.dumps(data),
timeout=600,
)
response_account_secret.raise_for_status()
response_account_secret_json = response_account_secret.json()
except Exception as e:
error_msg = f"endpoint=maintain_remote_account, account={remote_account_name}, error while fetching remote account secret: {str(e)}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
account_must_be_updated = False
# retrieve and store the current bearer token
current_token = response_account_secret_json.get("token", None)
if not current_token:
error_msg = f"endpoint=maintain_remote_account, account={remote_account_name}, error while fetching remote account secret: token not found"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
account_must_be_updated = False
else:
current_account_config["bearer_token"] = current_token
current_accounts_secrets[remote_account_name] = current_token
# check if the account is missing any key/values from the default_account_values
account_must_be_updated = False
for key in default_account_values:
if key not in current_account_config:
account_must_be_updated = True
# update the current_account_config with the default value
current_account_config[key] = default_account_values[key]
# if the account must be updated, we will run a POST request to the remote account endpoint
if not account_must_be_updated:
info_msg = f"endpoint=maintain_remote_account, successfully verified options for account={remote_account_name}, no update required"
logger.info(info_msg)
actions_list.append(info_msg)
else:
info_msg = f"endpoint=maintain_remote_account, update is required, account={remote_account_name}, must be updated due to outdated or missing options"
logger.info(info_msg)
actions_list.append(info_msg)
# endpoint target
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_account/{remote_account_name}?output_mode=json"
try:
response = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
params=current_account_config,
timeout=600,
)
response.raise_for_status()
info_msg = f"endpoint=maintain_remote_account, successfully updated remote account configuration for missing default values, account={remote_account_name}, status={response.status_code}"
logger.info(info_msg)
actions_list.append(info_msg)
except Exception as e:
error_msg = f"endpoint=maintain_remote_account, failed to update remote account configuration for missing default values, account={remote_account_name}, exception={str(e)}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
# return
return (
current_remote_accounts_dict,
current_accounts_secrets,
warnings_count,
warnings_list,
errors_count,
errors_list,
actions_list,
)
def check_and_rotate_tokens(
accounts,
current_remote_accounts_list,
current_remote_accounts_dict,
current_accounts_secrets,
force=False,
warnings_count=0,
warnings_list=[],
errors_count=0,
errors_list=[],
actions_list=[],
):
accounts_to_be_processed = []
if accounts == "*":
for account in current_remote_accounts_list:
accounts_to_be_processed.append(account)
else:
accounts_to_be_processed = accounts
for account in accounts_to_be_processed:
# get the current remote account dict
current_remote_account_dict = current_remote_accounts_dict.get(account)
# Check if a metadata record exists for the account
try:
kvrecord = collection.data.query(
query=json.dumps({"account": account})
)[0]
key = kvrecord.get("_key")
except Exception as e:
key = None
# if no metadata record exists, create one
if not key:
new_record = {
"account": account,
"mtime": time.time(),
"last_message": "Bearer token rotation tracking initiated",
"last_result": "success",
}
# insert a new record
try:
collection.data.insert(json.dumps(new_record))
info_msg = f'endpoint=maintain_remote_account, created new metadata record for account="{account}"'
logger.info(info_msg)
actions_list.append(info_msg)
except Exception as e:
error_msg = f'endpoint=maintain_remote_account, failed to insert the maintenance record with exception="{str(e)}"'
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
# if a metadata record exists, check if the token needs to be renewed
else:
token_must_be_renewed = False
# first, get and check renewal preferences
token_rotation_enablement = bool(
current_remote_account_dict.get("token_rotation_enablement", 1)
)
token_rotation_frequency = int(
current_remote_account_dict.get("token_rotation_frequency", 7)
) # in days
# get the last token rotation timestamp
last_rotation_timestamp = kvrecord.get("mtime")
# calculate the time spent since the last rotation (seconds)
time_since_last_rotation = time.time() - last_rotation_timestamp
# check if the token must be renewed
if (
time_since_last_rotation > token_rotation_frequency * 86400
and token_rotation_enablement
):
token_must_be_renewed = True
if force:
token_must_be_renewed = True
else:
last_rotation_timestamp_human = datetime.datetime.fromtimestamp(
last_rotation_timestamp
).strftime("%Y-%m-%d %H:%M:%S")
if not token_rotation_enablement:
info_msg = f'endpoint=maintain_remote_account, account="{account}", token rotation is disabled, last_rotation_timestamp="{last_rotation_timestamp_human}", token_rotation_enablement="{token_rotation_enablement}"'
logger.info(info_msg)
actions_list.append(info_msg)
else:
if not token_must_be_renewed:
info_msg = f'endpoint=maintain_remote_account, account="{account}", token is not due for renewal, last_rotation_timestamp="{last_rotation_timestamp_human}", token_rotation_enablement="{token_rotation_enablement}", token_rotation_frequency="{token_rotation_frequency}"'
logger.info(info_msg)
actions_list.append(info_msg)
else:
info_msg = f'endpoint=maintain_remote_account, account="{account}", token must be renewed, last_rotation_timestamp="{last_rotation_timestamp_human}", token_rotation_enablement="{token_rotation_enablement}", token_rotation_frequency="{token_rotation_frequency}"'
logger.info(info_msg)
actions_list.append(info_msg)
# if the token must be renewed, proceed with the renewal
if token_must_be_renewed:
# Create a session within the generate function
session = requests.Session()
# splunk_url, app_namespace, timeout_connect_check
splunk_url = current_remote_account_dict.get("splunk_url")
timeout_connect_check = int(
current_remote_account_dict.get("timeout_connect_check", 15)
)
app_namespace = current_remote_account_dict.get("app_namespace")
# bearer_token
bearer_token = current_accounts_secrets[account]
# Call target selector and pass the session as an argument
selected_url, errors = select_url(
session, splunk_url, timeout_connect_check
)
# end of get configuration
# If none of the endpoints could be reached
if not selected_url:
error_msg = f"endpoint=maintain_remote_account, none of the endpoints provided in the account URLs could be reached successfully, verify your network connectivity! (timeout_connect_check={timeout_connect_check})"
error_msg += f"Errors: {' '.join([f'{url}: {error}' for url, error in errors])}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
else:
# Enforce https and remove trailing slash in the URL, if any
selected_url = f"https://{selected_url.replace('https://', '').rstrip('/')}"
# Use urlparse to extract relevant info from target
parsed_url = urllib.parse.urlparse(selected_url)
# Establish the remote service
info_msg = f'endpoint=maintain_remote_account, establishing connection to host="{parsed_url.hostname}" on port="{parsed_url.port}", selected_url="{selected_url}"'
logger.info(info_msg)
actions_list.append(info_msg)
try:
remoteservice = establish_remote_service(
account,
parsed_url,
bearer_token,
app_namespace,
timeout=timeout_connect_check,
)
except Exception as e:
remoteservice = None
errors_count += 1
errors_list.append(str(e))
# continue only if remoteservice is established
if remoteservice:
info_msg = f"endpoint=maintain_remote_account, successfully established remote service connection for account={account}"
logger.info(info_msg)
actions_list.append(info_msg)
#
# user context
#
user_context_username = None
user_context_capabilities = []
# Run a GET call against /services/authentication/current-context to discover our user context
try:
url = f"{selected_url}/services/authentication/current-context?output_mode=json"
response = requests.get(
url,
headers={
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
},
verify=False,
timeout=600,
)
response.raise_for_status()
response_json = response.json()
user_context_entry = response_json.get("entry", {})[
0
]
user_context_content = user_context_entry.get(
"content", {}
)
user_context_username = user_context_content.get(
"username", None
)
user_context_capabilities = (
user_context_content.get("capabilities", [])
)
info_msg = f'endpoint=maintain_remote_account, successfully retrieved current context for account="{account}", username="{user_context_username}"'
logger.info(info_msg)
actions_list.append(info_msg)
except Exception as e:
error_msg = f"endpoint=maintain_remote_account, failed to retrieve current context for account={account}, exception={str(e)}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
#
# Renew token
#
bearer_token_can_be_renewed = False
new_bearer_token_id = None
new_bearer_token = None
new_bearer_token_generated = False
# if user_context_username is not null, and we have either edit_tokens_all or edit_tokens_own, the token can be renewed
if user_context_username:
if (
"edit_tokens_all" in user_context_capabilities
or "edit_tokens_own"
in user_context_capabilities
):
bearer_token_can_be_renewed = True
if not bearer_token_can_be_renewed:
warnings_count += 1
warning_msg = f'endpoint=maintain_remote_account, account="{account}", token cannot be renewed, the following capabilites are required for automated token renewal: edit_tokens_all or edit_tokens_own, user_context_username="{user_context_username}", user_context_capabilities="{user_context_capabilities}"'
logger.warning(warning_msg)
warnings_list.append(warning_msg)
else:
# Generate a new token, run a POST request against /services/authorization/tokens, body audience=TrackMe
try:
url = f"{selected_url}/services/authorization/tokens?output_mode=json"
data = {
"name": user_context_username,
"audience": f"TrackMe bearer token auto-renewal operated at {time.strftime('%Y-%m-%d %H:%M:%S')}, account={account}",
}
response = requests.post(
url,
headers={
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
},
data=data,
verify=False,
timeout=600,
)
response.raise_for_status()
response_json = response.json()
bearer_token_context_entry = response_json.get(
"entry", {}
)[0]
bearer_token_context_content = (
bearer_token_context_entry.get(
"content", {}
)
)
new_bearer_token_id = (
bearer_token_context_content.get("id", None)
)
new_bearer_token = (
bearer_token_context_content.get(
"token", None
)
)
if new_bearer_token_id and new_bearer_token:
info_msg = f'endpoint=maintain_remote_account, successfully generated a token for account="{account}", new_bearer_token_id="{new_bearer_token_id}"'
logger.info(info_msg)
actions_list.append(info_msg)
new_bearer_token_generated = True
except Exception as e:
error_msg = f"endpoint=maintain_remote_account, failed to generate a new token for account={account}, exception={str(e)}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
#
# Test connection with new bearer token before updating account configuration
#
if new_bearer_token_generated:
# Test the new bearer token by establishing a new remote service connection
info_msg = f'endpoint=maintain_remote_account, testing new bearer token connection for account="{account}" with host="{parsed_url.hostname}" on port="{parsed_url.port}"'
logger.info(info_msg)
actions_list.append(info_msg)
try:
# Attempt to establish a new remote service using the new bearer token
new_remoteservice = establish_remote_service(
account,
parsed_url,
new_bearer_token,
app_namespace,
timeout=timeout_connect_check,
)
if new_remoteservice:
info_msg = f"endpoint=maintain_remote_account, successfully tested new bearer token connection for account={account}"
logger.info(info_msg)
actions_list.append(info_msg)
else:
raise Exception("Failed to establish remote service with new bearer token")
except Exception as e:
error_msg = f"endpoint=maintain_remote_account, new bearer token connection test failed for account={account}, exception={str(e)}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
# Stop here - don't update the account if the new token doesn't work
continue
#
# Update bearer token in the account configuration
#
remote_account_bearer_token_was_updated = False
previous_bearer_token_id = None
if new_bearer_token_generated:
#
# subtask: update the remote account with the new bearer token
#
# update the current_remote_accounts_dict with the new bearer token
current_remote_account_dict["bearer_token"] = (
new_bearer_token
)
# endpoint target
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_account/{account}?output_mode=json"
try:
response = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
params=current_remote_account_dict,
timeout=600,
)
response.raise_for_status()
remote_account_bearer_token_was_updated = True
info_msg = f"endpoint=maintain_remote_account, successfully updated remote account configuration in TrackMe with new bearer_token id={new_bearer_token_id}, account={account}, status={response.status_code}"
logger.info(info_msg)
actions_list.append(info_msg)
except Exception as e:
error_msg = f"endpoint=maintain_remote_account, failed to update remote account configuration in TrackMe with new bearer_token id={new_bearer_token_id}, account={account}, exception={str(e)}"
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
#
# subtask: Update the metadata KVstore collection
#
# get records
try:
kvrecord = collection.data.query(
query=json.dumps({"account": account})
)[0]
key = kvrecord.get("_key")
previous_bearer_token_id = kvrecord.get(
"remote_bearer_token_id"
)
except Exception as e:
key = None
# new record
if not key:
# Set the response record
new_record = {
"account": account,
"mtime": time.time(),
"last_message": f"Bearer token renewal operated at {time.strftime('%Y-%m-%d %H:%M:%S')}",
"remote_bearer_token_id": new_bearer_token_id,
}
# insert a new record
try:
collection.data.insert(
json.dumps(new_record)
)
return {
"payload": new_record,
"status": 200,
}
except Exception as e:
error_msg = f'endpoint=maintain_remote_account, failed to insert the metadata kvstore record with exception="{str(e)}"'
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
# existing record
else:
# update record
kvrecord["mtime"] = time.time()
kvrecord["last_message"] = (
f"Bearer token renewal operated at {time.strftime('%Y-%m-%d %H:%M:%S')}"
)
kvrecord["remote_bearer_token_id"] = (
new_bearer_token_id
)
try:
collection.data.update(
str(key), json.dumps(kvrecord)
)
except Exception as e:
error_msg = f'endpoint=maintain_remote_account, failed to update the metadata kvstore record with exception="{str(e)}"'
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
#
# subtask: Revoke the previous bearer token
#
if (
remote_account_bearer_token_was_updated
and previous_bearer_token_id
):
try:
url = f"{selected_url}/services/authorization/tokens/{user_context_username}?output_mode=json"
data = {
"id": previous_bearer_token_id,
}
response = requests.delete(
url,
headers={
"Authorization": f"Bearer {bearer_token}",
"Content-Type": "application/json",
},
data=data,
verify=False,
timeout=600,
)
response.raise_for_status()
info_msg = f'endpoint=maintain_remote_account, successfully revoked previous bearer token for account="{account}", previous_bearer_token_id="{previous_bearer_token_id}"'
logger.info(info_msg)
actions_list.append(info_msg)
except Exception as e:
error_msg = f'endpoint=maintain_remote_account, failed to revoke previous bearer token for account="{account}", previous_bearer_token_id="{previous_bearer_token_id}", exception="{str(e)}"'
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
#
# subtask: List all tokens for the user
#
try:
url = f"{selected_url}/services/authorization/tokens?output_mode=json"
response = requests.get(
url,
headers={
"Authorization": f"Bearer {new_bearer_token}",
"Content-Type": "application/json",
},
data={
"username": user_context_username,
"status": "enabled",
},
verify=False,
timeout=600,
)
response.raise_for_status()
response_json = response.json()
existing_tokens = response_json.get("entry", [])
existing_tokens_response_list = []
for existing_token_entry in existing_tokens:
existing_token_name = (
existing_token_entry.get("name", None)
)
existing_token_content = (
existing_token_entry.get("content", {})
)
existing_token_claims = (
existing_token_content.get("claims", {})
)
existing_token_lastused = (
existing_token_content.get(
"lastUsed", None
)
)
existing_token_status = (
existing_token_content.get(
"status", None
)
)
existing_tokens_response_list.append(
{
existing_token_name: {
"claims": existing_token_claims,
"lastUsed": existing_token_lastused,
"status": existing_token_status,
}
}
)
info_msg = f'endpoint=maintain_remote_account, successfully retrieved all tokens for remote_user="{user_context_username}", tokens="{json.dumps(existing_tokens_response_list, indent=2)}"'
logger.info(info_msg)
actions_list.append(info_msg)
except Exception as e:
error_msg = f'endpoint=maintain_remote_account, failed to retrieve all tokens for remote_user="{user_context_username}", exception="{str(e)}"'
logger.error(error_msg)
errors_count += 1
errors_list.append(error_msg)
# return
return (
warnings_count,
warnings_list,
errors_count,
errors_list,
actions_list,
)
#
# Process main
#
# get all remote accounts
try:
current_remote_accounts_list, current_remote_accounts_dict = (
get_all_accounts()
)
except Exception as e:
logger.error(
f"endpoint=maintain_remote_account, error while fetching remote accounts: {str(e)}"
)
return {"payload": "failed", "status": 500}
# check accounts
if accounts != "*":
for account in accounts:
if account not in current_remote_accounts_list:
# set response
return_response["result"] = "failed"
return_response["message"] = (
f"endpoint=maintain_remote_account, remote account {account} not found"
)
return_response["remote_account"] = None
# log
logger.error(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 500}
# check and update accounts configuration, as needed
try:
(
current_remote_accounts_dict,
current_accounts_secrets,
warnings_count,
warnings_list,
errors_count,
errors_list,
actions_list,
) = check_and_update_accounts(accounts, remote_account_default)
except Exception as e:
logger.error(
f"endpoint=maintain_remote_account, error while checking and updating remote accounts: {str(e)}"
)
return {"payload": "failed", "status": 500}
# check and renew tokens, as needed
try:
warnings_count, warnings_list, errors_count, errors_list, actions_list = (
check_and_rotate_tokens(
accounts,
current_remote_accounts_list,
current_remote_accounts_dict,
current_accounts_secrets,
force=force_tokens_rotation,
)
)
except Exception as e:
logger.error(
f"endpoint=maintain_remote_account, error while checking and renewing tokens: {str(e)}"
)
return {"payload": "failed", "status": 500}
# handle show_token, if not set to true, anonymize the token for each account in current_remote_accounts_dict
if not show_token:
for account in current_remote_accounts_dict:
current_remote_accounts_dict[account]["bearer_token"] = "********"
# return response
if errors_count > 0:
return_response["result"] = "failed"
return_response["message"] = (
f"endpoint=maintain_remote_account, failed to update remote accounts, error_counts={errors_count}"
)
return_response["remote_accounts"] = current_remote_accounts_dict
return_response["errors"] = errors_list
return_response["actions"] = actions_list
logger.error(return_response.get("message"))
return {"payload": return_response, "status": 500}
else:
if warnings_count > 0:
return_response["result"] = "warning"
else:
return_response["result"] = "success"
if accounts == "*":
return_response["accounts"] = current_remote_accounts_dict
return_response["actions"] = actions_list
if warnings_count > 0:
return_response["warnings"] = warnings_list
return {"payload": return_response, "status": 200}
else:
return_response["accounts"] = {}
for account in accounts:
return_response["accounts"][account] = (
current_remote_accounts_dict.get(account)
)
return_response["actions"] = actions_list
if warnings_count > 0:
return_response["warnings"] = warnings_list
return {"payload": return_response, "status": 200}
# Update splunk_url for a remote account
def post_update_remote_account_url(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
account = resp_dict.get("account")
splunk_url_value = resp_dict.get("splunk_url_value")
else:
# body is required in this endpoint
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint updates the splunk_url value for a remote account. It requires a POST call with the following options:",
"resource_desc": "Update the splunk_url value for a remote account",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/update_remote_account_url" body="{\\"account\\": \\"myaccount\\", \\"splunk_url_value\\": \\"https://splunk1:8089,https://splunk2:8089\\"}"',
"options": [
{
"account": "The account configuration identifier",
"splunk_url_value": "Required, comma separated list of URL values to be used for the account.",
}
],
}
return {"payload": response, "status": 200}
# Check required parameters
if not account:
return {
"payload": "account parameter is required",
"status": 500,
}
if not splunk_url_value:
return {
"payload": "splunk_url_value parameter is required",
"status": 500,
}
# set loglevel
loglevel = trackme_getloglevel(
request_info.system_authtoken, request_info.server_rest_port
)
logger.setLevel(loglevel)
# Retrieve the account configuration with bearer token
try:
url = f"{request_info.server_rest_uri}/services/trackme/v2/configuration/get_remote_account"
data = {
"account": account,
}
response_account = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
data=json.dumps(data),
timeout=600,
)
response_account.raise_for_status() # an exception is raised if the account is not found
account_config = response_account.json()
except Exception as e:
error_msg = f"endpoint=update_remote_account_url, account={account}, error while fetching account info: {str(e)}"
logger.error(error_msg)
return {
"payload": {"result": "failed", "message": error_msg},
"status": 500,
}
#
# Test connectivity with the new splunk_url
#
try:
url = f"{request_info.server_rest_uri}/services/trackme/v2/configuration/test_remote_connectivity"
data = {
"target_endpoints": splunk_url_value,
"bearer_token": account_config.get("token"),
"app_namespace": account_config.get("app_namespace", "search"),
"timeout_connect_check": int(
account_config.get("timeout_connect_check", 15)
),
"timeout_search_check": int(
account_config.get("timeout_search_check", 300)
),
}
response_test = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
data=json.dumps(data),
timeout=600,
)
response_test.raise_for_status()
test_result = response_test.json()
logger.info(
f"endpoint=update_remote_account_url, account={account}, new_splunk_url={splunk_url_value}, connection was successfully established, test_result={test_result}"
)
except Exception as e:
error_msg = f"endpoint=update_remote_account_url, connectivity test failed for account={account}, new_splunk_url={splunk_url_value}, exception={str(e)}"
logger.error(error_msg)
return {
"payload": {"result": "failed", "message": error_msg},
"status": 500,
}
#
# Update the account configuration
#
# update the account configuration
account_config["splunk_url"] = splunk_url_value
# remove account key/pair from account_config
account_config.pop("account", None)
# remove message and status from account_config
account_config.pop("message", None)
account_config.pop("status", None)
# turn rbac_roles from list to comma separated string
account_config["rbac_roles"] = ",".join(account_config["rbac_roles"])
# rename token as the expected bearer_token
account_config["bearer_token"] = account_config["token"]
account_config.pop("token", None)
# endpoint target
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_account/{account}?output_mode=json"
try:
response = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
verify=False,
params=account_config,
timeout=600,
)
response.raise_for_status()
info_msg = f"endpoint=update_remote_account_url, successfully updated remote account configuration in TrackMe, account={account}, status={response.status_code}"
logger.info(info_msg)
# remote the bearer token from the account configuration
account_config.pop("bearer_token", None)
return {
"payload": {
"result": "success",
"message": info_msg,
"account_config": account_config,
},
"status": 200,
}
except Exception as e:
error_msg = f"endpoint=update_remote_account_url, failed to update remote account configuration in TrackMe, account={account}, exception={str(e)}"
logger.error(error_msg)
return {
"payload": {
"result": "failed",
"message": error_msg,
"account_config": account_config,
},
"status": 500,
}
# Update specific parameters of a Virtual Tenant account with privileges escalation
def post_update_vtenant_account(self, request_info, **kwargs):
describe = False
# Retrieve from data
try:
resp_dict = json.loads(str(request_info.raw_args["payload"]))
except Exception as e:
resp_dict = None
if resp_dict is not None:
try:
describe = resp_dict["describe"]
if describe in ("true", "True"):
describe = True
except Exception as e:
describe = False
tenant_id = resp_dict["tenant_id"]
# parameters_to_update is required - a dictionary with the parameters to be updated
try:
parameters_to_update = resp_dict["parameters_to_update"]
# if not a dictionary, attempt to load it
if not isinstance(parameters_to_update, dict):
try:
parameters_to_update = json.loads(parameters_to_update)
except Exception as e:
error_msg = f"failed to load parameters_to_update, exception={str(e)}"
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
except Exception as e:
error_msg = "parameters_to_update is required and must be a dictionary"
logger.error(error_msg)
return {"payload": error_msg, "status": 500}
else:
describe = True
# if describe is requested, show the usage
if describe:
response = {
"describe": "This endpoint allows updating specific parameters of a Virtual Tenant account with privileges escalation, it requires a POST with the following options:",
"resource_desc": "Update Virtual Tenant account parameters",
"resource_spl_example": '| trackme mode=post url="/services/trackme/v2/configuration/admin/update_vtenant_account" body="{\'tenant_id\': \'<tenant_id>\', \'parameters_to_update\': {\'ui_default_timerange\': \'48h\', \'ui_min_object_width\': 400}}"',
"options": [
{
"tenant_id": "The Virtual Tenant ID",
"parameters_to_update": "Required, a dictionary with the parameters to be updated (only valid parameters from the default configuration are accepted)",
}
],
}
return {"payload": response, "status": 200}
#
# main
#
# Validate that all requested parameters are valid (exist in vtenant_account_default)
invalid_parameters = []
for param in parameters_to_update.keys():
if param not in vtenant_account_default:
invalid_parameters.append(param)
if invalid_parameters:
error_msg = f"Invalid parameters requested: {invalid_parameters}. Only parameters from the default configuration are allowed."
logger.error(error_msg)
return {"payload": error_msg, "status": 400}
url = f"{request_info.server_rest_uri}/servicesNS/nobody/trackme/trackme_vtenants/{tenant_id}"
vtenant_data = {}
# vtenant_account_found boolean
vtenant_account_found = False
try:
# Get current vtenant account configuration
response = requests.get(
url,
headers={"Authorization": f"Splunk {request_info.system_authtoken}"},
verify=False,
params={"output_mode": "json"},
timeout=600,
)
if response.status_code in (200, 201, 204):
logger.info(f"successfully retrieved vtenant configuration")
vtenant_data_json = response.json()
vtenant_data_current = vtenant_data_json["entry"][0]["content"]
# Set vtenant_account_found to True
vtenant_account_found = True
# Start with the current configuration
vtenant_data = dict(vtenant_data_current)
# Update only the requested parameters
vtenant_data.update(parameters_to_update)
logger.info(
f'vtenant_data updated="{json.dumps(vtenant_data, indent=2)}", parameters_to_update="{json.dumps(parameters_to_update, indent=2)}"'
)
else:
error_msg = f"failed to retrieve vtenant configuration, status_code={response.status_code}"
logger.error(error_msg)
except Exception as e:
error_msg = f"failed to retrieve vtenant configuration, exception={str(e)}"
logger.error(error_msg)
# init return_response
return_response = {}
#
# if Virtual Tenant account is not found
#
if not vtenant_account_found:
# set response
return_response["result"] = "failed"
return_response["message"] = f"vtenant account '{tenant_id}' not found"
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 404}
#
# main: Virtual Tenant account is found, proceed with updating
#
# in vtenant_data, remote the following fields: disabled, eai:acl, eai:appName, eai:userName
vtenant_data.pop("disabled", None)
vtenant_data.pop("eai:acl", None)
vtenant_data.pop("eai:appName", None)
vtenant_data.pop("eai:userName", None)
try:
logger.info(
f'attempting to update vtenant configuration, vtenant_data="{json.dumps(vtenant_data, indent=2)}"'
)
response = requests.post(
url,
headers={
"Authorization": f"Splunk {request_info.system_authtoken}",
"Content-Type": "application/json",
},
data=vtenant_data,
verify=False,
timeout=600,
)
if response.status_code in (200, 201, 204):
# set response
return_response["result"] = "success"
return_response["message"] = (
f"vtenant configuration updated successfully, status_code={response.status_code}"
)
return_response["vtenant_account"] = vtenant_data
return_response["updated_parameters"] = parameters_to_update
# log
logger.info(return_response.get("message"))
# return response
return {"payload": return_response, "status": 200}
else:
# set return_response
return_response["result"] = "failed"
return_response["message"] = (
f"failed to update vtenant configuration, status_code={response.status_code}, response={response.text}"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return return_response
return {"payload": return_response, "status": 500}
except Exception as e:
# set response
return_response["result"] = "failed"
return_response["message"] = (
f"failed to update vtenant configuration, exception={str(e)}"
)
return_response["vtenant_account"] = None
# log
logger.error(return_response.get("message"))
# return response
return {"payload": return_response, "status": 500}