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.
450 lines
19 KiB
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}
|