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_ack_po...

450 lines
19 KiB

#!/usr/bin/env python
# coding=utf-8
__name__ = "trackme_rest_handler_ack.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
# 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.ack_power", "trackme_rest_api_ack_power.log")
# Redirect global logging to use the same handler
import logging
logging.getLogger().handlers = logger.handlers
logging.getLogger().setLevel(logger.level)
# import rest handler
import trackme_rest_handler
# import trackme libs
from trackme_libs import (
trackme_getloglevel,
trackme_audit_event,
)
from trackme_libs_ack import (
get_all_ack_records_from_kvcollection,
convert_epoch_to_datetime,
)
# import Splunk libs
import splunklib.client as client
class TrackMeHandlerAckWriteOps_v2(trackme_rest_handler.RESTHandler):
def __init__(self, command_line, command_arg):
super(TrackMeHandlerAckWriteOps_v2, self).__init__(command_line, command_arg, logger)
def get_resource_group_desc_ack(self, request_info, **kwargs):
response = {
"resource_group_name": "ack",
"resource_group_desc": "Acknowledgments allow silencing an entity alert for a given period of time automatically (write operations)",
}
return {"payload": response, "status": 200}
def post_ack_manage(self, request_info, **kwargs):
describe = False
tenant_id = None
# 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
if not describe:
# tenant_id is required
tenant_id = resp_dict.get("tenant_id", None)
if tenant_id is None:
error_msg = f'tenant_id="{tenant_id}", tenant_id is required'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
# the action, if not specified, show will be the default
action = resp_dict.get("action", "show")
if action not in ("show", "enable", "disable"):
# log error and return
error_msg = f'tenant_id="{tenant_id}", action="{action}", action is incorrect, valid options are show | enable | disable'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
# object_list
object_list = resp_dict.get("object_list", None)
object_value_list = []
# for action = show, if not set, will be defined to *
# for action = enable/disable, if not set, will return an error
if object_list is None:
if action == "show":
object_list = "*"
else:
error_msg = f'tenant_id="{tenant_id}", action="{action}", object_list is required'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
else:
# turn as a list
object_value_list = object_list.split(",")
# object_category
object_category_value = resp_dict["object_category"]
# ack_period
ack_period = resp_dict.get("ack_period", 86400)
try:
ack_period = int(ack_period)
except Exception as e:
# log error format and return error
error_msg = f'tenant_id="{tenant_id}", ack_period="{ack_period}", ack_period period is incorrect, an integer is expected, exception="{str(e)}"'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
# ack_type is optional, if not set, will be defined to unsticky
ack_type = resp_dict.get("ack_type", "unsticky")
if not ack_type in ("sticky", "unsticky"):
# log error format and return error
error_msg = f'tenant_id="{tenant_id}", ack_type="{ack_type}", ack_type is incorrect, valid options are sticky | unsticky'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
# ack_comment
ack_comment = resp_dict.get("ack_comment", None)
# anomaly_reason is optional, if not set, will be defined to N/A
anomaly_reason = resp_dict.get("anomaly_reason", "N/A")
# ack_source is optional, if not set, will be defined to user_ack
ack_source = resp_dict.get("ack_source", "user_ack")
if not ack_source in ("auto_ack", "user_ack"):
# log error format and return error
error_msg = f'tenant_id="{tenant_id}", ack_source="{ack_source}", ack_source is incorrect, valid options are auto_ack | user_ack'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
else:
# body is required in this endpoint, if not submitted describe the usage
describe = True
if describe:
response = {
"describe": "This endpoint will enable/disable an acknowledgment for one or more entities, it requires a POST call with the following information:",
"resource_desc": "Show/Enable/Disable/Update acknowledgement for a comma separated list of entities",
"resource_spl_example": '| trackme url="/services/trackme/v2/ack/write/ack_manage" mode="post" body="{\'tenant_id\': \'mytenant\', '
+ "'action': 'enable', 'object_category': 'splk-dsm', 'object_list': 'netscreen:netscreen:firewall', 'ack_period': 86400, 'ack_comment': 'Under review'}\"",
"options": [
{
"tenant_id": "The tenant identifier",
"action": "The action to be performed, valid options are: enable | disable | show.",
"object_category": "the object category (splk-dsm, splk-dhm, splk-mhm, splk-flx, splk-wlk, splk-fqm)",
"object_list": "List of entities, in a comma separated format. If action=show and not set, will be defined to * to retrieve all Ack records, mandatory for action=enable/disable",
"ack_period": "Required if action=enable, the period for the acknowledgment in seconds",
"ack_type": "The type of Ack, valid options are sticky | unsticky, defaults to unsticky if not specified. Unsticky Ack are purged automatically when the entity goes back to a green state, while sticky Ack are purged only when the expiration is reached.",
"ack_comment": "Relevant if action=enable but optional, the acknowlegment comment to be added to the records",
"ack_source": "OPTIONAL: the source of the ack, if unset will be defined to: user_ack. Valid options are: auto_ack, user_ack",
"anomaly_reason": "OPTIONAL: the reason for the anomaly, if unset will be defined to: N/A",
"update_comment": "OPTIONAL: a comment for the update, comments are added to the audit record, if unset will be defined to: API update",
}
],
}
return {"payload": response, "status": 200}
# Update comment is optional and used for audit changes
try:
update_comment = resp_dict["update_comment"]
except Exception as e:
update_comment = "API update"
# ack_comment
if ack_comment is None:
ack_comment = update_comment
# counters
processed_count = 0
succcess_count = 0
failures_count = 0
# records summary
records_summary = []
# 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)
collection_name = f"kv_trackme_common_alerts_ack_tenant_{tenant_id}"
collection = service.kvstore[collection_name]
# Component mapping
component_mapping = {
"splk-dsm": "dsm",
"splk-dhm": "dhm",
"splk-mhm": "mhm",
"splk-flx": "flx",
"splk-fqm": "fqm",
"splk-wlk": "wlk",
}
# get the whole collection
try:
(
collection_records_list,
collection_records_keys,
collection_records_objects,
collection_records_objects_dict,
collection_records_keys_dict,
) = get_all_ack_records_from_kvcollection(
collection_name, collection, object_category_value
)
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", failed to retrieve KVstore collection records using function get_all_records_from_kvcollection, exception="{str(e)}"'
logger.error(error_msg)
return {
"payload": {"action": "failure", "result": error_msg},
"status": 500,
}
# if action is show and object_list is *, return all records
if action == "show" and object_list == "*":
return {
"payload": {
"process_count": len(collection_records_list),
"records": collection_records_list,
},
"status": 200,
}
else:
# action show
if action == "show":
for object_value in object_value_list:
if object_value in collection_records_objects:
# increment counter
processed_count += 1
succcess_count += 1
failures_count += 0
records_summary.append(
collection_records_objects_dict[object_value]
)
else:
# increment counter
processed_count += 1
succcess_count += 0
failures_count += 1
result = {
"object": object_value,
"action": "show",
"result": "failure",
"exception": f'tenant_id="{tenant_id}", the entity="{object_value}" could not be found in this tenant',
}
records_summary.append(result)
# action enable
elif action == "enable" or action == "disable":
if action == "enable":
ack_state = "active"
ack_expiration = time.time() + ack_period
else:
ack_state = "inactive"
ack_expiration = 0
ack_type = "N/A"
for object_value in object_value_list:
ack_record = {
"object": object_value,
"object_category": object_category_value,
"anomaly_reason": anomaly_reason,
"ack_source": ack_source,
"ack_expiration": ack_expiration,
"ack_state": ack_state,
"ack_mtime": time.time(),
"ack_type": ack_type,
"ack_comment": ack_comment,
}
# only for enable, on a per object and if anomaly_reason is not set
if action == "enable":
# if action is enable, and anomaly_reason is not set, attempt to connect to the data KV and retrieve the actual anomaly_reason
if anomaly_reason == "N/A":
try:
collection_data_name = f"kv_trackme_{component_mapping.get(object_category_value, None)}_tenant_{tenant_id}"
collection_data = service.kvstore[collection_data_name]
data_kvrecord = collection_data.data.query(
query=json.dumps({"object": object_value})
)[0]
ack_record["anomaly_reason"] = data_kvrecord.get(
"anomaly_reason", "N/A"
)
except Exception as e:
error_msg = f'tenant_id="{tenant_id}", while attempting to retrieve the anomaly_reason in the data KVstore {collection_data_name} an exception was encountered, exception="{str(e)}"'
logger.error(error_msg)
try:
if object_value in collection_records_objects:
# Update the record
collection.data.update(
collection_records_objects_dict[object_value]["_key"],
json.dumps(ack_record),
)
else:
collection.data.insert(json.dumps(ack_record))
# increment counter
processed_count += 1
succcess_count += 1
failures_count += 0
result = {
"object": object_value,
"action": action,
"result": "success",
"ack_record": ack_record,
}
records_summary.append(result)
# set audit message depending on the action (enable / disable)
if action == "enable":
audit_msg = "The Ack was enabled successfully"
elif action == "disable":
audit_msg = "The Ack was disabled successfully"
# audit
trackme_audit_event(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
request_info.user,
"success",
f"{action} ack",
str(object_value),
str(object_category_value),
ack_record,
audit_msg,
str(update_comment),
)
except Exception as e:
# increment counter
processed_count += 1
succcess_count += 0
failures_count += 1
result = {
"object": object_value,
"action": "enable",
"result": "failure",
"exception": f'tenant_id="{tenant_id}", the entity="{object_value}" could not be updated, exception="{str(e)}"',
}
records_summary.append(result)
# set audit message depending on the action (enable / disable)
if action == "enable":
audit_msg = (
f"The Ack could not be enabled, exception={str(e)}"
)
elif action == "disable":
audit_msg = (
f"The Ack could not be disabled, exception={str(e)}"
)
# audit
trackme_audit_event(
request_info.system_authtoken,
request_info.server_rest_uri,
tenant_id,
request_info.user,
"failure",
f"{action} ack",
str(object_value),
str(object_category_value),
ack_record,
audit_msg,
str(update_comment),
)
# render HTTP status and summary
req_summary = {
"process_count": processed_count,
"success_count": succcess_count,
"failures_count": failures_count,
"records": records_summary,
}
if processed_count > 0 and processed_count == succcess_count:
http_status = 200
else:
http_status = 500
return {"payload": req_summary, "status": http_status}