Merge remote-tracking branch 'origin/TOML' into TOML

This commit is contained in:
darius 2024-05-28 10:03:20 +02:00
commit 8559c9a297
4 changed files with 164 additions and 351 deletions

View File

@ -1,66 +0,0 @@
---
# Start of wazuh notifier configuration yaml.
# This is the yaml config file for wazuh-active-response (for both the Python and Go version)
targets: "slack, ntfy, discord" # Platforms in this string with comma seperated values are triggered.
full_message: "" # Platforms in this string will enable sending the full event information.
full_alert: "" # Platforms in this string will enable sending the full event information.
# Exclude rule events that are enabled in the ossec.conf active response definition.
# These settings provide an easier way to disable events from firing the notifiers.
excluded_rules: "99999, 00000" # Enter as a string with comma seperated values
excluded_agents: "99999" # Enter as a string with comma seperated values
# Priority mapping from 0-15 (Wazuh threat levels) to 1-5 (in notifications) and their respective colors (Discord)
# https://documentation.wazuh.com/current/user-manual/ruleset/rules-classification.html
# Enter threat_map as lists of integers, mention_threshold as integer and color as Hex integer
priority_map:
- threat_map: [ 15,14,13,12 ]
mention_threshold: 1
color: 0xec3e40 # Red, SEVERE
- threat_map: [ 11,10,9 ]
mention_threshold: 1
color: 0xff9b2b # Orange, HIGH
- threat_map: [ 8,7,6 ]
mention_threshold: 5
color: 0xf5d800 # Yellow, ELEVATED
- threat_map: [ 5,4 ]
mention_threshold: 20
color: 0x377fc7 # Blue, GUARDED
- threat_map: [ 3,2,1,0 ]
mention_threshold: 20
color: 0x01a465 # Green, LOW
# The next 2 settings are used to add information to the messages.
sender: "Wazuh (IDS)"
click: "https://documentation.wazuh.com/"
###########################################################################################
# From here on the settings are ONLY used by the Python version of wazuh-active-response. #
###########################################################################################
# Below settings provide for a window that enable/disables events from firing the notifiers.
excluded_days: "" # Enter as a string with comma seperated values. Be aware of your regional settings.
excluded_hours: [ "23:59", "00:00" ] # Enter as a tuple of string values. Be aware of your regional settings.
# Following parameter defines the markdown characters to emphasise the parameter names in the notification messages
markdown_emphasis:
slack: "*"
ntfy: "**"
discord: "**"
# The next settings are used for testing. Test mode will add the example event in wazuh-notify-test-event.json instead of the
# message received through wazuh. This enables testing for particular events when the test event is customized.
test_mode: False
# Enabling this parameter provides more logging to the wazuh-notifier log.
extended_logging: 2
# Enabling this parameter provides extended logging to the console.
extended_print: 0
# End of wazuh notifier configuration yaml
...

View File

@ -20,7 +20,7 @@ click = "https://documentation.wazuh.com/"
# Priority mapping from 0-15 (Wazuh threat levels) to 1-5 (in notifications) and their respective colors (Discord) # Priority mapping from 0-15 (Wazuh threat levels) to 1-5 (in notifications) and their respective colors (Discord)
# https://documentation.wazuh.com/current/user-manual/ruleset/rules-classification.html # https://documentation.wazuh.com/current/user-manual/ruleset/rules-classification.html
# Enter threat_map as lists of integers, mention_threshold as integer and color as Hex integer # Enter threat_map as lists of integers, mention/notify_threshold as integer and color as Hex integer
[[priority_map]] [[priority_map]]
threat_map = [15, 14, 13, 12] threat_map = [15, 14, 13, 12]
mention_threshold = 1 mention_threshold = 1

View File

@ -5,7 +5,7 @@
# License (version 2) as published by the FSF - Free Software # License (version 2) as published by the FSF - Free Software
# Foundation. # Foundation.
# #
# Rudi Klein, april 2024 # Rudi Klein, May 2024
import requests import requests
@ -14,168 +14,117 @@ from wazuh_notify_module import *
def main(): def main():
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
# Load the YAML config. # Load the TOML config.
config: dict = get_config() config: dict = get_config()
logger(0, config, me, him, "############ Processing event ###############################") logger(0, config, me, him, "############ Processing event ###############################")
logger(2, config, me, him, "Loading yaml configuration")
# Get the arguments used with running the script. # Get the arguments used with running the script.
arguments = get_arguments() arguments = get_arguments()
# Check if we are in test mode (test_mode setting in config yaml). If so, load test event instead of live event. # Check for test mode. Use test data if true
if config.get('python', 'test_mode'): data = check_test_mode(config)
logger(1, config, me, him, "Running in test mode: using test message wazuh-notify-test-event.json")
# Load the test event data.
home_path, _, _ = set_environment()
with (open(home_path + '/etc/wazuh-notify-test-event.json') as event_file):
data: dict = json.loads(event_file.read())
else:
# We are running live. Load the data from the Wazuh process.
logger(2, config, me, him, "Running in live mode: using live message")
data = load_message()
# Extract the 'alert' section of the (JSON) event # Extract the 'alert' section of the (JSON) event
alert = data["parameters"]["alert"] alert = data["parameters"]["alert"]
logger(2, config, me, him, "Extracting data from the event") logger(2, config, me, him, "Extracting data from the event")
# Check the config for any exclusion rules # Check the config for any exclusion rules and abort when excluded.
exclusions_check(config, alert)
fire_notification = exclusions_check(config, alert)
logger(1, config, me, him, "Checking if we are outside of the exclusion rules: " + str(fire_notification))
if not fire_notification:
# The event was excluded by the exclusion rules in the configuration.
logger(1, config, me, him, "Event excluded, no notification sent. Exiting")
exit()
else:
# The event was not excluded by the exclusion rules in the configuration. Keep processing.
logger(2, config, me, him, "Event NOT excluded, notification will be sent")
# Get the mapping from event threat level to priority, color and mention_flag. # Get the mapping from event threat level to priority, color and mention_flag.
priority, color, mention = threat_mapping(config, alert['rule']['level'], alert['rule']['firedtimes']) priority, color, mention = threat_mapping(config, alert['rule']['level'], alert['rule']['firedtimes'])
logger(2, config, me, him, "Threat mapping done: " +
"prio:" + str(priority) + " color:" + str(color) + " mention:" + mention)
# If the target argument was used with the script, we'll use that instead of the configuration parameter. # If the target argument was used with the script, we'll use that instead of the configuration parameter.
config["targets"] = arguments['targets'] if arguments['targets'] != "" else config["targets"] config["targets"] = arguments['targets'] if arguments['targets'] != "" else config["targets"]
# Prepare the messaging platform specific request and execute # Prepare the messaging platform specific notification and execute if configured.
if "discord" in config["targets"]: if "discord" in config["targets"]:
# Show me some ID! Stop resisting!
caller = "discord" caller = "discord"
me = frame(0).f_code.co_name
him = frame(1).f_code.co_name
# Load the url/webhook from the configuration. # Load the url/webhook from the configuration.
discord_url, _, _ = get_env() discord_url, _, _ = get_env()
discord_url = arguments['url'] if arguments['url'] else discord_url discord_url = arguments['url'] if arguments['url'] else discord_url
# Build the basic notification message content. # Build the basic message content.
message_body: str = construct_message_body(caller, config, arguments, alert)
notification: str = construct_basic_message(config, arguments, caller, alert) # Common preparation of the notification.
logger(2, config, me, him, caller + " basic message constructed") notification_body, click, sender = prepare_payload(caller, config, arguments, message_body, alert, priority)
# Build the payload(s) for the POST request. # Build the payload(s) for the POST request.
_, _, payload_json = build_discord_notification(caller, config, notification_body, color, mention, sender)
_, _, payload_json = build_notification(caller, # Build the notification to be sent.
config, build_discord_notification(caller, config, notification_body, color, mention, sender)
arguments,
notification,
alert,
priority,
color,
mention
)
# POST the notification through requests. # POST the notification through requests.
discord_result = requests.post(discord_url, json=payload_json)
result = requests.post(discord_url, json=payload_json) logger(1, config, me, him, caller + " notification constructed and sent: " + str(discord_result))
logger(1, config, me, him, caller + " notification constructed and HTTPS request done: " + str(result))
if "ntfy" in config["targets"]: if "ntfy" in config["targets"]:
# Show me some ID! Stop resisting!
caller = "ntfy" caller = "ntfy"
me = frame(0).f_code.co_name
him = frame(1).f_code.co_name
# Load the url/webhook from the configuration. # Load the url/webhook from the configuration.
_, ntfy_url, _ = get_env() _, ntfy_url, _ = get_env()
ntfy_url = arguments['url'] if arguments['url'] else ntfy_url
# Build the basic notification message content. # Build the basic message content.
message_body: str = construct_message_body(caller, config, arguments, alert)
notification: str = construct_basic_message(config, arguments, caller, alert) # Common preparation of the notification.
notification_body, click, sender = prepare_payload(caller, config, arguments, message_body, alert, priority)
logger(2, config, me, him, caller + " basic message constructed")
# Build the payload(s) for the POST request. # Build the payload(s) for the POST request.
payload_headers, payload_data, _ = build_notification(caller, payload_headers, payload_data, _ = build_ntfy_notification(caller, config, notification_body, color, mention,
config, sender)
arguments,
notification, # Build the notification to be sent.
alert, build_ntfy_notification(caller, config, notification_body, priority, click, sender)
priority,
color,
mention
)
# POST the notification through requests. # POST the notification through requests.
ntfy_result = requests.post(ntfy_url, data=payload_data, headers=payload_headers)
logger(1, config, me, him, caller + " notification constructed and sent: " + str(ntfy_result))
result = requests.post(ntfy_url, data=payload_data, headers=payload_headers) # if "slack" in config["targets"]:
logger(1, config, me, him, caller + " notification constructed and request done: " + str(result)) # # Show me some ID! Stop resisting!
# caller = "slack"
if "slack" in config["targets"]: # me = frame(0).f_code.co_name
caller = "slack" # him = frame(1).f_code.co_name
#
# Load the url/webhook from the configuration. # # Load the url/webhook from the configuration.
# _, _, slack_url = get_env()
_, _, slack_url = get_env() # slack_url = arguments['url'] if arguments['url'] else slack_url
#
# Build the basic notification message content. # # Build the basic message content.
# message_body: str = construct_message_body(caller, config, arguments, data)
notification: str = construct_basic_message(config, arguments, caller, alert) #
# # Common preparation of the notification.
logger(2, config, me, him, caller + " basic message constructed") # notification_body, click, sender = prepare_payload(caller, config, arguments, message_body, alert,
# priority)
# Build the payload(s) for the POST request. # # Build the payload(s) for the POST request.
# _, _, payload_json = build_slack_notification(caller, config, notification_body, priority, color, mention,
_, _, payload_json = build_notification(caller, # click, sender)
config, #
arguments, # # Build the notification to be sent.
notification, # build_slack_notification(caller, config, notification_body, priority, click, sender)
alert, #
priority, # result = requests.post(slack_url, headers={'Content-Type': 'application/json'}, json=payload_json)
color, #
mention # logger(1, config, me, him, caller + " notification constructed and sent: " + str(result))
)
# POST the notification through requests.
result = requests.post(slack_url, headers={'Content-Type': 'application/json'}, json=payload_json)
logger(1, config, me, him, caller + " notification constructed and request done: " + str(result))
logger(0, config, me, him, "############ Event processed ################################") logger(0, config, me, him, "############ Event processed ################################")
exit(0) exit(0)
if __name__ == "__main__": if "__main__" == __name__:
main() main()

View File

@ -26,14 +26,11 @@ def set_environment() -> tuple:
# Set paths for use in this module # Set paths for use in this module
wazuh_path, log_path, config_path = set_environment() wazuh_path, log_path, config_path = set_environment()
# Set structured timestamps for notifications. # Set structured timestamps for notifications.
def set_time_format(): def set_time_format():
now_message = time.strftime('%A, %d %b %Y %H:%M:%S') now_message = time.strftime('%A, %d %b %Y %H:%M:%S')
now_logging = time.strftime('%Y-%m-%d %H:%M:%S') now_logging = time.strftime('%Y-%m-%d %H:%M:%S')
now_time = time.strftime('%H:%M') now_time = time.strftime('%H:%M')
@ -43,7 +40,6 @@ def set_time_format():
# Logger: print to console and/or log to file # Logger: print to console and/or log to file
def logger(level, config, me, him, message): def logger(level, config, me, him, message):
_, now_logging, _, _ = set_time_format() _, now_logging, _, _ = set_time_format()
@ -51,45 +47,33 @@ def logger(level, config, me, him, message):
logger_log_path = '{0}/logs/wazuh-notify.log'.format(logger_wazuh_path) logger_log_path = '{0}/logs/wazuh-notify.log'.format(logger_wazuh_path)
# When logging from main(), the destination function is called "<module>". For cosmetic reasons rename to "main". # When logging from main(), the destination function is called "<module>". For cosmetic reasons rename to "main".
him = 'main' if him == '<module>' else him him = 'main' if him == '<module>' else him
log_line = f'{now_logging} | {level} | {me: <26} | {him: <13} | {message}'
log_line = f'{now_logging} | {level} | {me: <23} | {him: <15} | {message}'
# Compare the extended_print log level in the configuration to the log level of the message. # Compare the extended_print log level in the configuration to the log level of the message.
if config.get('python').get('extended_print', 0) >= level: if config.get('python').get('extended_print', 0) >= level:
print(log_line) print(log_line)
try: try:
# Compare the extended_logging level in the configuration to the log level of the message. # Compare the extended_logging level in the configuration to the log level of the message.
if config.get('python').get('extended_logging', 0) >= level: if config.get('python').get('extended_logging', 0) >= level:
with open(logger_log_path, mode="a") as log_file: with open(logger_log_path, mode="a") as log_file:
log_file.write(log_line + "\n") log_file.write(log_line + "\n")
except (FileNotFoundError, PermissionError, OSError): except (FileNotFoundError, PermissionError, OSError):
# Special message to console when logging to file fails and console logging might not be set. # Special message to console when logging to file fails and console logging might not be set.
log_line = f'{now_logging} | {level} | {me: <26} | {him: <13} | error opening log file: {logger_log_path}'
log_line = f'{now_logging} | {level} | {me: <23} | {him: <15} | error opening log file: {logger_log_path}'
print(log_line) print(log_line)
# Get the content of the .env file (url's and/or webhooks). # Get the content of the .env file (url's and/or webhooks).
def get_env(): def get_env():
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
# Write the configuration to a dictionary. # Write the configuration to a dictionary.
config: dict = get_config() config: dict = get_config()
# Check if the secrets .env file is available. # Check if the secrets .env file is available.
try: try:
dotenv_path = join(dirname(__file__), '.env') dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path) load_dotenv(dotenv_path)
@ -98,43 +82,34 @@ def get_env():
raise Exception(dotenv_path, "file not found") raise Exception(dotenv_path, "file not found")
# Retrieve URLs from .env # Retrieve URLs from .env
discord_url = os.getenv("DISCORD_URL") discord_url = os.getenv("DISCORD_URL")
ntfy_url = os.getenv("NTFY_URL") ntfy_url = os.getenv("NTFY_URL")
slack_url = os.getenv("SLACK_URL") slack_url = os.getenv("SLACK_URL")
except Exception as err: except Exception as err:
# output error, and return with an error code # output error, and return with an error code
logger(0, config, me, him, 'Error reading ' + str(err)) logger(0, config, me, him, 'Error reading ' + str(err))
exit(err) exit(err)
logger(2, config, me, him, dotenv_path + " loaded") logger(2, config, me, him, dotenv_path + " loaded")
return discord_url, ntfy_url, slack_url return discord_url, ntfy_url, slack_url
# Read and process configuration settings from wazuh-notify-config.yaml and create dictionary. # Read and process configuration settings from wazuh-notify-config.yaml and create dictionary.
def get_config(): def get_config():
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
this_config_path: str = "" this_config_path: str = ""
config: dict = {} config: dict = {}
# logger(2, config, me, him, "Loading TOML configuration")
try: try:
_, _, this_config_path = set_environment() _, _, this_config_path = set_environment()
with open(this_config_path, 'rb') as ntfier_config: with open(this_config_path, 'rb') as ntfier_config:
config: dict = tomli.load(ntfier_config) config: dict = tomli.load(ntfier_config)
except (FileNotFoundError, PermissionError, OSError): except (FileNotFoundError, PermissionError, OSError):
logger(2, config, me, him, "Error accessing configuration file: " + this_config_path) logger(2, config, me, him, "Error accessing configuration file: " + this_config_path)
logger(2, config, me, him, "Reading configuration file: " + this_config_path) logger(2, config, me, him, "Reading configuration file: " + this_config_path)
config['targets'] = config.get('general').get('targets', 'discord, slack, ntfy') config['targets'] = config.get('general').get('targets', 'discord, slack, ntfy')
@ -145,7 +120,6 @@ def get_config():
config['sender'] = config.get('general').get('sender', 'Wazuh (IDS)') config['sender'] = config.get('general').get('sender', 'Wazuh (IDS)')
config['click'] = config.get('general').get('click', 'https://wazuh.com') config['click'] = config.get('general').get('click', 'https://wazuh.com')
config['md_e'] = config.get('general').get('markdown_emphasis', '') config['md_e'] = config.get('general').get('markdown_emphasis', '')
config['excluded_days'] = config.get('python').get('excluded_days', '') config['excluded_days'] = config.get('python').get('excluded_days', '')
config['excluded_hours'] = config.get('python').get('excluded_hours', '') config['excluded_hours'] = config.get('python').get('excluded_hours', '')
config['test_mode'] = config.get('python').get('test_mode', False) config['test_mode'] = config.get('python').get('test_mode', False)
@ -156,9 +130,7 @@ def get_config():
# Show configuration settings from wazuh-notify-config.yaml # Show configuration settings from wazuh-notify-config.yaml
def view_config(): def view_config():
_, _, this_config_path, _ = set_environment() _, _, this_config_path, _ = set_environment()
try: try:
@ -170,23 +142,18 @@ def view_config():
# Get script arguments during execution. Params found here override config settings. # Get script arguments during execution. Params found here override config settings.
def get_arguments(): def get_arguments():
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
# Retrieve the configuration information # Retrieve the configuration information
config: dict = get_config() config: dict = get_config()
# Short options # Short options
options: str = "u:s:p:m:t:c:hv" options: str = "u:s:p:m:t:c:hv"
# Long options # Long options
long_options: list = ["url=", long_options: list = ["url=",
"sender=", "sender=",
"targets=", "targets=",
@ -212,7 +179,6 @@ def get_arguments():
""" """
# Initialize some variables. # Initialize some variables.
url: str = "" url: str = ""
sender: str = "" sender: str = ""
targets: str = "" targets: str = ""
@ -222,17 +188,13 @@ def get_arguments():
click: str = "" click: str = ""
# Fetch the arguments from the command line, skipping the first argument (name of the script). # Fetch the arguments from the command line, skipping the first argument (name of the script).
argument_list: list = sys.argv[1:] argument_list: list = sys.argv[1:]
logger(2, config, me, him, "Found arguments:" + str(argument_list)) logger(2, config, me, him, "Found arguments:" + str(argument_list))
if not argument_list: if not argument_list:
logger(1, config, me, him, 'No argument list found (no arguments provided with script execution') logger(1, config, me, him, 'No argument list found (no arguments provided with script execution')
# Store defaults for the non-existing arguments in the arguments dictionary to avoid None errors. # Store defaults for the non-existing arguments in the arguments dictionary to avoid None errors.
arguments: dict = {'url': url, arguments: dict = {'url': url,
'sender': sender, 'sender': sender,
'targets': targets, 'targets': targets,
@ -241,78 +203,55 @@ def get_arguments():
'tags': tags, 'tags': tags,
'click': click} 'click': click}
return arguments return arguments
else: else:
try: try:
logger(2, config, me, him, "Parsing arguments") logger(2, config, me, him, "Parsing arguments")
# Parsing arguments # Parsing arguments
p_arguments, values = getopt.getopt(argument_list, options, long_options) p_arguments, values = getopt.getopt(argument_list, options, long_options)
# Check each argument. Arguments that are present will override the defaults. # Check each argument. Arguments that are present will override the defaults.
for current_argument, current_value in p_arguments: for current_argument, current_value in p_arguments:
if current_argument in ("-h", "--help"): if current_argument in ("-h", "--help"):
print(help_text) print(help_text)
exit() exit()
elif current_argument in ("-v", "--view"): elif current_argument in ("-v", "--view"):
view_config() view_config()
exit() exit()
elif current_argument in ("-u", "--url"): elif current_argument in ("-u", "--url"):
url: str = current_value url: str = current_value
elif current_argument in ("-s", "--sender"): elif current_argument in ("-s", "--sender"):
sender: str = current_value sender: str = current_value
elif current_argument in ("-d", "--targets"): elif current_argument in ("-d", "--targets"):
targets: str = current_value targets: str = current_value
elif current_argument in ("-p", "--priority"): elif current_argument in ("-p", "--priority"):
priority: int = int(current_value) priority: int = int(current_value)
elif current_argument in ("-m", "--message"): elif current_argument in ("-m", "--message"):
message: str = current_value message: str = current_value
elif current_argument in ("-t", "--tags"): elif current_argument in ("-t", "--tags"):
tags: str = current_value tags: str = current_value
elif current_argument in ("-c", "--click"): elif current_argument in ("-c", "--click"):
click: str = current_value click: str = current_value
except getopt.error as err: except getopt.error as err:
# Output error, and return error code # Output error, and return error code
logger(0, config, me, him, "Error during argument parsing:" + str(err)) logger(0, config, me, him, "Error during argument parsing:" + str(err))
logger(2, config, me, him, "Arguments returned as dictionary") logger(2, config, me, him, "Arguments returned as dictionary")
# Store the arguments in the arguments dictionary. # Store the arguments in the arguments dictionary.
arguments: dict = {'url': url, 'sender': sender, 'targets': targets, 'message': message, arguments: dict = {'url': url, 'sender': sender, 'targets': targets, 'message': message,
'priority': priority, 'tags': tags, 'click': click} 'priority': priority, 'tags': tags, 'click': click}
return arguments return arguments
# Receive and load message from Wazuh # Receive and load message from Wazuh
def load_message(): def load_message():
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
config: dict = get_config() config: dict = get_config()
# get alert from stdin # get alert from stdin
logger(2, config, me, him, "Loading event message from STDIN") logger(2, config, me, him, "Loading event message from STDIN")
input_str: str = "" input_str: str = ""
@ -325,153 +264,136 @@ def load_message():
if data.get("command") == "add": if data.get("command") == "add":
logger(1, config, me, him, "Relevant event data found") logger(1, config, me, him, "Relevant event data found")
return data return data
else: else:
# Event came in, but wasn't processed. # Event came in, but wasn't processed.
logger(0, config, me, him, "Event data not found") logger(0, config, me, him, "Event data not found")
sys.exit(1) sys.exit(1)
# Check if there are reasons not to process this event. Check exclusions for rules, agents, days and hours. # Check if we are in test mode (test_mode setting in config yaml). If so, load test event instead of live event.
def check_test_mode(config):
me = frame(0).f_code.co_name
him = frame(1).f_code.co_name
if config.get('python').get('test_mode'):
logger(1, config, me, him, "Running in test mode: using test message wazuh-notify-test-event.json")
# Load the test event data.
home_path, _, _ = set_environment()
with (open(home_path + '/etc/wazuh-notify-test-event.json') as event_file):
data: dict = json.loads(event_file.read())
else:
# We are running live. Load the data from the Wazuh process.
logger(2, config, me, him, "Running in live mode: using live message")
data = load_message()
return data
# Check if there are reasons not to process this event. Check exclusions for rules, agents, days and hours.
def exclusions_check(config, alert): def exclusions_check(config, alert):
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
# Set some environment # Set some environment
now_message, now_logging, now_weekday, now_time = set_time_format() now_message, now_logging, now_weekday, now_time = set_time_format()
# Check the exclusion records from the configuration yaml. # Check the exclusion records from the configuration yaml.
logger(1, config, me, him, "Checking if we are outside of the exclusion rules: ")
ex_hours: tuple = config.get('python', 'excluded_hours') ex_hours: tuple = config.get('python').get('excluded_hours')
# Start hour may not be later than end hours. End hour may not exceed 00:00 midnight to avoid day jump. # Start hour may not be later than end hours. End hour may not exceed 00:00 midnight to avoid day jump.
################################################
ex_hours = [ex_hours[0], "23:59"] if (ex_hours[1] >= '23:59' or ex_hours[1] < ex_hours[0]) else ex_hours ex_hours = [ex_hours[0], "23:59"] if (ex_hours[1] >= '23:59' or ex_hours[1] < ex_hours[0]) else ex_hours
# Get some more exclusion records from the config. # Get some more exclusion records from the config.
ex_days = config.get('python').get('excluded_days') ex_days = config.get('python').get('excluded_days')
ex_agents = config.get('general').get("excluded_agents") ex_agents = config.get('general').get("excluded_agents")
ex_rules = config.get('general').get("excluded_rules") ex_rules = config.get('general').get("excluded_rules")
# Check agent and rule from within the event. # Check agent and rule from within the event.
ev_agent = alert['agent']['id'] ev_agent = alert['agent']['id']
ev_rule = alert['rule']['id'] ev_rule = alert['rule']['id']
# Let's assume all lights are green, until proven otherwise. # Let's assume all lights are green, until proven otherwise.
ex_hours_eval, ex_weekday_eval, ev_rule_eval, ev_agent_eval = True, True, True, True ex_hours_eval, ex_weekday_eval, ev_rule_eval, ev_agent_eval = True, True, True, True
# Evaluate the conditions for sending a notification. Any False will cause the notification to be discarded. # Evaluate the conditions for sending a notification. Any False will cause the notification to be discarded.
if (now_time > ex_hours[0]) and (now_time < ex_hours[1]): if (now_time > ex_hours[0]) and (now_time < ex_hours[1]):
logger(2, config, me, him, "excluded: event inside exclusion time frame") logger(2, config, me, him, "excluded: event inside exclusion time frame")
ex_hours_eval = False ex_hours_eval = False
elif now_weekday in ex_days: elif now_weekday in ex_days:
logger(2, config, me, him, "excluded: event inside excluded weekdays") logger(2, config, me, him, "excluded: event inside excluded weekdays")
ex_weekday_eval = False ex_weekday_eval = False
elif ev_rule in ex_rules: elif ev_rule in ex_rules:
logger(2, config, me, him, "excluded: event id inside exclusion list") logger(2, config, me, him, "excluded: event id inside exclusion list")
ev_rule_eval = False ev_rule_eval = False
elif ev_agent in ex_agents: elif ev_agent in ex_agents:
logger(2, config, me, him, "excluded: event agent inside exclusion list") logger(2, config, me, him, "excluded: event agent inside exclusion list")
ev_rule_eval = False ev_rule_eval = False
notification_eval = True if (ex_hours_eval and ex_weekday_eval and ev_rule_eval and ev_agent_eval) else False notification_eval = True if (ex_hours_eval and ex_weekday_eval and ev_rule_eval and ev_agent_eval) else False
logger(1, config, me, him, "Exclusion rules evaluated. Final decision: " + str(notification_eval)) logger(1, config, me, him, "Exclusion rules evaluated. Final decision: " + str(notification_eval))
return notification_eval if not notification_eval:
# The event was excluded by the exclusion rules in the configuration.
logger(1, config, me, him, "Event excluded, no notification sent. Exiting")
exit()
else:
# The event was not excluded by the exclusion rules in the configuration. Keep processing.
logger(2, config, me, him, "Event NOT excluded, notification will be sent")
return
# Map the event threat level to the appropriate 5-level priority scale and color for use in the notification platforms. # Map the event threat level to the appropriate 5-level priority scale and color for use in the notification platforms.
def threat_mapping(config, threat_level, fired_times): def threat_mapping(config, threat_level, fired_times):
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
# Map threat level to priority. Enters Homeland Security :-). # Map threat level to priority. Enters Homeland Security :-).
p_map = config.get('priority_map') p_map = config.get('priority_map')
logger(2, config, me, him, "Prio map: " + str(p_map)) logger(2, config, me, him, "Prio map: " + str(p_map))
for i in range(len(p_map)): for i in range(len(p_map)):
logger(2, config, me, him, "Loop: " + str(i)) logger(2, config, me, him, "Loop: " + str(i))
logger(2, config, me, him, "Level: " + str(threat_level)) logger(2, config, me, him, "Level: " + str(threat_level))
if threat_level in p_map[i]["threat_map"]: if threat_level in p_map[i]["threat_map"]:
color_mapping = p_map[i]["color"] color_mapping = p_map[i]["color"]
priority_mapping = 5 - i priority_mapping = 5 - i
logger(2, config, me, him, "Prio: " + str(priority_mapping)) logger(2, config, me, him, "Prio: " + str(priority_mapping))
logger(2, config, me, him, "Color: " + str(color_mapping)) logger(2, config, me, him, "Color: " + str(color_mapping))
if fired_times >= p_map[i]["mention_threshold"]: if fired_times >= p_map[i]["mention_threshold"]:
# When this flag is set, Discord recipients get a stronger message (DM). # When this flag is set, Discord recipients get a stronger message (DM).
mention_flag = "@here" mention_flag = "@here"
else: else:
mention_flag = "" mention_flag = ""
logger(2, config, me, him, "Threat level mapped as: " + logger(2, config, me, him, "Threat level mapped as: " +
"prio:" + str(priority_mapping) + " color: " + str(color_mapping) + " mention: " + mention_flag) "prio:" + str(priority_mapping) + " color: " + str(color_mapping) + " mention: " + mention_flag)
return priority_mapping, color_mapping, mention_flag return priority_mapping, color_mapping, mention_flag
logger(0, config, me, him, "Threat level mapping failed! Returning garbage (99, 99, 99)") logger(0, config, me, him, "Threat level mapping failed! Returning garbage (99, 99, 99)")
return 99, 99, "99" return 99, 99, "99"
# Construct the message that will be sent to the notifier platforms. # Construct the message that will be sent to the notifier platforms.
def construct_message_body(caller, config, arguments, data: dict) -> str:
def construct_basic_message(config, arguments, caller: str, data: dict) -> str:
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
# Include a specific control sequence for markdown bold parameter names. # Include a specific control sequence for markdown bold parameter names.
# todo To be fixed
md_map = config.get('python').get('markdown_emphasis', '') md_map = config.get('markdown_emphasis', '')
md_e = md_map[caller] md_e = md_map[caller]
# If the --message (-m) argument was fulfilled, use this message to be sent. # If the --message (-m) argument was fulfilled, use this message to be sent.
if arguments['message']: if arguments['message']:
message_body = arguments['message']
basic_msg = arguments['message']
else: else:
_, timestamp, _, _ = set_time_format() _, timestamp, _, _ = set_time_format()
basic_msg: str = \ message_body: str = \
( (
md_e + "Timestamp:" + md_e + " " + timestamp + "\n" + md_e + "Timestamp:" + md_e + " " + timestamp + "\n" +
md_e + "Agent:" + md_e + " " + data["agent"]["name"] + " (" + data["agent"]["id"] + ")" + "\n" + md_e + "Agent:" + md_e + " " + data["agent"]["name"] + " (" + data["agent"]["id"] + ")" + "\n" +
@ -479,32 +401,25 @@ def construct_basic_message(config, arguments, caller: str, data: dict) -> str:
md_e + "Rule:" + md_e + " " + data["rule"]["description"] + "\n" + md_e + "Rule:" + md_e + " " + data["rule"]["description"] + "\n" +
md_e + "Description:" + md_e + " " + data["full_log"] + "\n" + md_e + "Description:" + md_e + " " + data["full_log"] + "\n" +
md_e + "Threat level:" + md_e + " " + str(data["rule"]["level"]) + "\n" + md_e + "Threat level:" + md_e + " " + str(data["rule"]["level"]) + "\n" +
md_e + "Times fired:" + md_e + " " + str(data["rule"]["firedtimes"]) + "\n") md_e + "Times fired:" + md_e + " " + str(data["rule"]["firedtimes"]) + "\n"
)
if caller == "ntfy":
# todo Check this out
basic_msg = "&nbsp;\n" + basic_msg
logger(2, config, me, him, caller + " basic message constructed.") logger(2, config, me, him, caller + " basic message constructed.")
return message_body
return basic_msg
# Construct the notification (message + additional information) that will be sent to the notifier platforms. # Construct the notification (message + additional information) that will be sent to the notifier platforms.
def prepare_payload(caller, config, arguments, message_body, alert, priority):
def build_notification(caller, config, arguments, notification, alert, priority, color, mention):
# The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging. # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name me = frame(0).f_code.co_name
him = frame(1).f_code.co_name him = frame(1).f_code.co_name
logger(2, config, me, him, caller + " notification being constructed.") logger(2, config, me, him, "Notification being constructed.")
md_map = config.get('python').get('markdown_emphasis', '') md_map = config.get('markdown_emphasis', '')
md_e = md_map[caller] md_e = md_map[caller]
click: str = config.get('general').get('click', 'https://wazuh.com') click: str = config.get('general').get('click', 'https://wazuh.com')
sender: str = config.get('general').get('sender', 'Wazuh (IDS)')
priority: str = str(priority) priority: str = str(priority)
tags = (str(alert['rule']['groups']).replace("[", "") tags = (str(alert['rule']['groups']).replace("[", "")
.replace("]", "") .replace("]", "")
@ -522,70 +437,85 @@ def build_notification(caller, config, arguments, notification, alert, priority,
.replace(',', ' ') .replace(',', ' ')
) )
# Fill some of the variables with argument values if available. # Fill some of the variables with argument values if available.
# todo Redundant?
click = arguments['click'] if arguments['click'] else click
priority = arguments['priority'] if arguments['priority'] else priority priority = arguments['priority'] if arguments['priority'] else priority
sender = arguments['sender'] if arguments['sender'] else sender
tags = arguments['tags'] if arguments['tags'] else tags tags = arguments['tags'] if arguments['tags'] else tags
sender: str = config.get('general').get('sender', 'Wazuh (IDS)')
sender = arguments['sender'] if arguments['sender'] else sender
click: str = config.get('general').get('click', 'https://wazuh.com')
click = arguments['click'] if arguments['click'] else click
# Add the full alert data to the notification. # Add the full alert data to the notification.
if caller in config["full_alert"]: if caller in config["full_alert"]:
logger(2, config, me, him, caller + "Full alert data will be sent.") logger(2, config, me, him, caller + "Full alert data will be attached.")
# Add the full alert data to the notification body
notification: str = ("\n\n" + notification + "\n" + notification_body: str = ("\n\n" + message_body + "\n" +
md_e + "__Full event__" + md_e + "\n" + "```\n" + full_event + "```") md_e + "__Full event__" + md_e + "\n" + "```\n" + full_event + "```")
else:
# Add Priority & tags to the notification notification_body: str = message_body
# Add priority & tags to the notification body
notification_body = (notification_body + "\n\n" + md_e + "Priority:" + md_e + " " + str(priority) + "\n" +
md_e + "Tags:" + md_e + " " + tags + "\n\n" + click)
logger(2, config, me, him, caller + " adding priority and tags") logger(2, config, me, him, caller + " adding priority and tags")
notification = (notification + "\n\n" + md_e + "Priority:" + md_e + " " + str(priority) + "\n" +
md_e + "Tags:" + md_e + " " + tags + "\n\n" + click)
config["targets"] = arguments['targets'] if arguments['targets'] != "" else config["targets"] config["targets"] = arguments['targets'] if arguments['targets'] != "" else config["targets"]
# Prepare the messaging platform specific notification and execute return notification_body, click, sender
if caller == "discord":
logger(2, config, me, him, caller + " payload created") def build_discord_notification(caller, config, notification_body, color, mention, sender):
payload_json = {"username": sender, # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
"content": mention, me = frame(0).f_code.co_name
"embeds": [{"description": notification, him = frame(1).f_code.co_name
"color": color, logger(2, config, me, him, caller + " payload created")
"title": sender}]}
logger(2, config, me, him, caller + " notification built") payload_json = {"username": sender,
"content": mention,
"embeds": [{"description": notification_body,
"color": color,
"title": sender}]}
logger(2, config, me, him, caller + " notification built")
return "", "", payload_json
return "", "", payload_json
if caller == "ntfy": def build_ntfy_notification(caller, config, notification_body, priority, click, sender):
logger(2, config, me, him, caller + " payloads created") # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name
him = frame(1).f_code.co_name
logger(2, config, me, him, caller + " payloads created")
payload_data = notification # if caller == "ntfy":
payload_headers = {"Markdown": "yes", # todo Check this out
"Title": sender, # basic_msg = "&nbsp;\n" + basic_msg
"Priority": str(priority),
"Click": click}
logger(2, config, me, him, caller + " notification built") payload_data = notification_body
payload_headers = {"Markdown": "yes",
"Title": sender,
"Priority": str(priority),
"Click": click}
logger(2, config, me, him, caller + " notification built")
return payload_headers, payload_data, ""
return payload_headers, payload_data, ""
if caller == "slack": def build_slack_notification(caller, config, arguments, notification_body, priority, color, mention, click, sender):
logger(2, config, me, him, caller + " payloads created") # The 'me' variable sets the called function (current function), the 'him' the calling function. Used for logging.
me = frame(0).f_code.co_name
him = frame(1).f_code.co_name
sender: str = config.get('general').get('sender', 'Wazuh (IDS)')
sender = arguments['sender'] if arguments['sender'] else sender
# todo Need some investigation. logger(2, config, me, him, caller + " payloads created")
payload_json = {"text": notification} # todo Need some investigation.
# payload_json = {"username": sender,
# "content": mention,
# "embeds": [{"description": notification,
# "color": color,
# "title": sender}]}
logger(2, config, me, him, caller + " notification built") payload_json = {"text": notification_body}
# payload_json = {"username": sender,
# "content": mention,
# "embeds": [{"description": notification,
# "color": color,
# "title": sender}]}
return "", "", payload_json logger(2, config, me, him, caller + " notification built")
return "", "", payload_json