From 731bbfd13bcb5dbbbde43ae5f2a5045576af98e7 Mon Sep 17 00:00:00 2001 From: Rudi Klein Date: Sun, 28 Apr 2024 20:27:23 +0200 Subject: [PATCH] first commit --- .env | 2 + custom-active-response.py | 169 +++++++++++++++++++++++++++++++++++++ wazuh-discord-notifier.py | 144 +++++++++++++++++++++++++++++++ wazuh-notifier-config.yaml | 66 +++++++++++++++ wazuh-ntfy-notifier.py | 132 +++++++++++++++++++++++++++++ wazuh_notifier_lib.py | 63 ++++++++++++++ 6 files changed, 576 insertions(+) create mode 100755 .env create mode 100755 custom-active-response.py create mode 100755 wazuh-discord-notifier.py create mode 100755 wazuh-notifier-config.yaml create mode 100755 wazuh-ntfy-notifier.py create mode 100755 wazuh_notifier_lib.py diff --git a/.env b/.env new file mode 100755 index 0000000..956ce40 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DISCORD_WEBHOOK=https://discord.com/api/webhooks/1233074098939232286/zc-9n4LsTxSSMjg3aSUmvvAfne1Jqs-IqOA6G4ZUi1zluvmK2NOmCaO1u8KjaQ1PovVB + diff --git a/custom-active-response.py b/custom-active-response.py new file mode 100755 index 0000000..59bfbbb --- /dev/null +++ b/custom-active-response.py @@ -0,0 +1,169 @@ +#!/usr/bin/python3 +# Copyright (C) 2015-2022, Wazuh Inc. +# All rights reserved. + +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License (version 2) as published by the FSF - Free Software +# Foundation. + +import os +import sys +import json +import datetime +from pathlib import PureWindowsPath, PurePosixPath + +if os.name == 'nt': + LOG_FILE = "C:\\Program Files (x86)\\ossec-agent\\active-response\\active-responses.log" +else: + LOG_FILE = "/var/ossec/logs/active-responses.log" + +ADD_COMMAND = 0 +DELETE_COMMAND = 1 +CONTINUE_COMMAND = 2 +ABORT_COMMAND = 3 + +OS_SUCCESS = 0 +OS_INVALID = -1 + +class message: + def __init__(self): + self.alert = "" + self.command = 0 + + +def write_debug_file(ar_name, msg): + with open(LOG_FILE, mode="a") as log_file: + ar_name_posix = str(PurePosixPath(PureWindowsPath(ar_name[ar_name.find("active-response"):]))) + log_file.write(str(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S')) + " " + ar_name_posix + ": " + msg +"\n") + + +def setup_and_check_message(argv): + + # get alert from stdin + input_str = "" + for line in sys.stdin: + input_str = line + break + + write_debug_file(argv[0], input_str) + + try: + data = json.loads(input_str) + except ValueError: + write_debug_file(argv[0], 'Decoding JSON has failed, invalid input format') + message.command = OS_INVALID + return message + + message.alert = data + + command = data.get("command") + + if command == "add": + message.command = ADD_COMMAND + elif command == "delete": + message.command = DELETE_COMMAND + else: + message.command = OS_INVALID + write_debug_file(argv[0], 'Not valid command: ' + command) + + return message + + +def send_keys_and_check_message(argv, keys): + + # build and send message with keys + keys_msg = json.dumps({"version": 1,"origin":{"name": argv[0],"module":"active-response"},"command":"check_keys","parameters":{"keys":keys}}) + + write_debug_file(argv[0], keys_msg) + + print(keys_msg) + sys.stdout.flush() + + # read the response of previous message + input_str = "" + while True: + line = sys.stdin.readline() + if line: + input_str = line + break + + write_debug_file(argv[0], input_str) + + try: + data = json.loads(input_str) + except ValueError: + write_debug_file(argv[0], 'Decoding JSON has failed, invalid input format') + return message + + action = data.get("command") + + if "continue" == action: + ret = CONTINUE_COMMAND + elif "abort" == action: + ret = ABORT_COMMAND + else: + ret = OS_INVALID + write_debug_file(argv[0], "Invalid value of 'command'") + + return ret + + +def main(argv): + + write_debug_file(argv[0], "Started") + + # validate json and get command + msg = setup_and_check_message(argv) + + if msg.command < 0: + sys.exit(OS_INVALID) + + if msg.command == ADD_COMMAND: + + """ Start Custom Key + At this point, it is necessary to select the keys from the alert and add them into the keys array. + """ + + alert = msg.alert["parameters"]["alert"] + keys = [alert["rule"]["id"]] + + """ End Custom Key """ + + action = send_keys_and_check_message(argv, keys) + + # if necessary, abort execution + if action != CONTINUE_COMMAND: + + if action == ABORT_COMMAND: + write_debug_file(argv[0], "Aborted") + sys.exit(OS_SUCCESS) + else: + write_debug_file(argv[0], "Invalid command") + sys.exit(OS_INVALID) + + """ Start Custom Action Add """ + + with open("ar-test-result.txt", mode="a") as test_file: + test_file.write("Active response triggered by rule ID: <" + str(keys) + ">\n") + + """ End Custom Action Add """ + + elif msg.command == DELETE_COMMAND: + + """ Start Custom Action Delete """ + + os.remove("ar-test-result.txt") + + """ End Custom Action Delete """ + + else: + write_debug_file(argv[0], "Invalid command") + + write_debug_file(argv[0], "Ended") + + sys.exit(OS_SUCCESS) + + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file diff --git a/wazuh-discord-notifier.py b/wazuh-discord-notifier.py new file mode 100755 index 0000000..2a14aa0 --- /dev/null +++ b/wazuh-discord-notifier.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +# This script is free software. +# +# Copyright (C) 2024, Rudi Klein. +# All rights reserved. +# +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License (version 2) as published by the FSF - Free Software +# Foundation. +# +# This script is executed by the active response script (custom-active-response.py), which is triggered by rules firing. +# +# Discord is a voice, video and text communication service used by over a hundred million people to hang out and talk +# with their friends and communities. It allows for receiving message using webhooks. +# For more information: https://discord.com. + +import requests +import getopt +import sys +import os +from dotenv import load_dotenv +from os import getenv +from os.path import join, dirname + +from wazuh_notifier_lib import set_env as se +from wazuh_notifier_lib import set_time as st +from wazuh_notifier_lib import import_config as ic +from wazuh_notifier_lib import view_config as vc + +# Get path values +wazuh_path, ar_path, config_path = se() + +# Get time value +now_message, now_logging = st() + +# Retrieve webhook from .env +try: + dotenv_path = join(dirname(__file__), '.env') + load_dotenv(dotenv_path) + if not os.path.isfile(dotenv_path): raise Exception(dotenv_path, "file not found") + discord_webhook = os.getenv("DISCORD_WEBHOOK") + + +except Exception as err: + # output error, and return with an error code + print(str(Exception(err.args))) + exit(err) + +# the POST builder. Prepares https and sends the request. + + +def discord_command(n_server, n_sender, n_destination, n_priority, n_message, n_tags, n_click): + + x_message = (now_message + + "\n\n" + "**__" + n_message + "__**\n\n" + + "Priority: " + n_priority + "\n" + + "Tags: " + n_tags + "\n\n" + n_click + ) + n_data = {"username": n_sender,"embeds": [{"description": x_message, "title": n_destination}]} + + result = requests.post(n_server, json=n_data) + + +# Remove 1st argument from the list of command line arguments +argument_list: list = sys.argv[1:] + +# Short options +options: str = "u:s:p:m:t:c:hv" + +# Long options +long_options: list = ["server=", "sender=", "destination=", "priority=", "message=", "tags=", "click=", "help", "view"] + +# Setting some basic defaults. +d_sender: str = "Security message" +d_destination: str = "WAZUH (IDS)" +d_priority: str = "5" +d_message: str = "Test message" +d_tags: str = "informational, testing, hard-code" +d_click: str = "https://google.com" + +# Use the values from the config yaml if available. Overrides the basic defaults. +server = discord_webhook +sender = d_sender if (ic("discord_sender") is None) else ic("discord_sender") +destination = d_destination if (ic("discord_destination") is None) else ic("discord_destination") +priority = d_priority if (ic("discord_priority") is None) else ic("discord_priority") +message = d_message if (ic("discord_message") is None) else ic("discord_message") +tags = d_tags if (ic("discord_tags") is None) else ic("discord_tags") +click = d_click if (ic("discord_click") is None) else ic("discord_click") + +help_text: str = """ + -u, --server is the webhook URL of the Discord server. It is stored in .env. + -s, --sender is the sender of the message, either an app name or a person. The default is "Security message". + -d, --destination is the destination (actually the originator) of the message, either an app name or a person. Default is "Wazuh (IDS)" + -p, --priority is the priority of the message, ranging from 1 (highest), to 5 (lowest). Default is 5. + -m, --message is the text of the message to be sent. Default is "Test message", but may include --tags and/or --click. + -t, --tags is an arbitrary strings of tags (keywords), seperated by a "," (comma). Default is "informational, testing, hard-coded". + -c, --click is a link (URL) that can be followed by tapping/clicking inside the message. Default is https://google.com. + -h, --help shows this help message. Must have no value argument. + -v, --view show config. +""" + +# Get params during execution. Params found here, override minimal defaults and/or config settings. +try: + # Parsing argument + arguments, values = getopt.getopt(argument_list, options, long_options) + + # checking each argument + for current_argument, current_value in arguments: + + if current_argument in ("-h", "--help"): + print(help_text) + exit() + + elif current_argument in ("-v", "--view"): + vc() + exit() + + elif current_argument in ("-s", "--sender"): + sender = current_value + + elif current_argument in ("-d", "--destination"): + destination = current_value + + elif current_argument in ("-p", "--priority"): + priority = current_value + + elif current_argument in ("-m", "--message"): + message = current_value + + elif current_argument in ("-t", "--tags"): + tags = current_value + + elif current_argument in ("-c", "--click"): + click = current_value + +except getopt.error as err: + # output error, and return with an error code + print(str(err)) + +# Finally, execute the POST request +discord_command(discord_webhook, sender, destination, priority, message, tags, click) + diff --git a/wazuh-notifier-config.yaml b/wazuh-notifier-config.yaml new file mode 100755 index 0000000..22c3698 --- /dev/null +++ b/wazuh-notifier-config.yaml @@ -0,0 +1,66 @@ +--- +#start of yaml + +# This is the yaml config file for both the wazuh-ntfy-notifier.py and wazuh-discord-notifier.py. +# The yaml needs to be in the same folder as the wazuh-ntfy-notifier.py and wazuh-discord-notifier.py + +# COMMON configuration settings start here. +# 1 = messages will be sent through this message server. 0 = messages will NOT be sent through this message server. + +discord_enabled: 1 +ntfy_enabled: 1 + +# COMMON configuration settings start here. + + +# NTFY configuration settings start here. +# The default values refer to the hard-coded defaults, if no yaml configuration is found. +# +# -u, --server is the URL of the NTFY server, ending with a "/". Default is https://ntfy.sh/. +# -s, --sender is the sender of the message, either an app name or a person. Default is "Wazuh (IDS)". +# -d, --destination is the NTFY subscription, to send the message to. Default is none. +# -p, --priority is the priority of the message, ranging from 1 (highest), to 5 (lowest). Default is 5. +# -m, --message is the text of the message to be sent. Default is "Test message". +# -t, --tags is an arbitrary strings of tags (keywords), seperated by a "," (comma). Default is "informational, testing, hard-coded". +# -c, --click is a link (URL) that can be followed by tapping/clicking inside the message. Default is https://google.com. +# -h, --help shows this help message. Must have no value argument. +# -v, --view show config. + +ntfy_server: "https://ntfy.sh/" +ntfy_sender: "Wazuh (IDS)" +ntfy_destination: "none" +ntfy_priority: "5" +ntfy_message: "Test message" +ntfy_tags: "information, testing, yaml" +ntfy_click: "https://google.com" + +# NTFY configuration settings ends here. + +# DISCORD configuration settings start here. +# The default values refer to the hard-coded defaults, if no yaml configuration is found. + +# -u, --server is the webhook URL of the Discord server. It is stored in .env. +# -s, --sender is the sender of the message, either an app name or a person. The default is "Security message". +# -d, --destination is the destination (actually the originator) of the message, either an app name or a person. Default is "Wazuh (IDS)" +# -p, --priority is the priority of the message, ranging from 1 (highest), to 5 (lowest). Default is 5. +# -m, --message is the text of the message to be sent. Default is "Test message", but may include --tags and/or --click. +# -t, --tags is an arbitrary strings of tags (keywords), seperated by a "," (comma). Default is "informational, testing, hard-coded". +# -c, --click is a link (URL) that can be followed by tapping/clicking inside the message. Default is https://google.com. +# -h, --help shows this help message. Must have no value argument. +# -v, --view show config. + +discord_server: "not used. The webhook (server) is a secret stored in .env" +discord_sender: "Security message" +discord_destination: "WAZUH (IDS)" +discord_priority: "5" +discord_message: "Test message" +discord_tags: "informational, testing, yaml" +discord_click: "https://google.com" + +# DISCORD configuration settings ends here. + +#end of yaml +... + + + diff --git a/wazuh-ntfy-notifier.py b/wazuh-ntfy-notifier.py new file mode 100755 index 0000000..68c695a --- /dev/null +++ b/wazuh-ntfy-notifier.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 + +# This script is free software. +# +# Copyright (C) 2024, Rudi Klein. +# All rights reserved. +# +# This program is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public +# License (version 2) as published by the FSF - Free Software +# Foundation. +# +# This script is executed by the active response script (custom-active-response.py), which is triggered by rules firing. +# +# ntfy (pronounced notify) is a simple HTTP-based pub-sub notification service. +# It allows you to send notifications to your phone or desktop via scripts from any computer, and/or using a REST API. +# It's infinitely flexible, and 100% free software. For more information: https://ntfy.sh. + +import json +import requests +import getopt +import sys + +from wazuh_notifier_lib import set_env as se +from wazuh_notifier_lib import set_time as st +from wazuh_notifier_lib import import_config as ic +from wazuh_notifier_lib import view_config as vc + +# Get path values +wazuh_path, ar_path, config_path = se() + +# Get time value +now_message, now_logging = st() + +# the POST builder + + +def ntfy_command(n_server, n_sender, n_destination, n_priority, n_message, n_tags, n_click): + n_header = "" + if n_sender != "": n_header = n_header + '"Title"' + ": " + '"' + n_sender + '"' + ", " + if n_tags != "": n_header = n_header + '"Tags"' + ": " + '"' + n_tags + '"' + ", " + if n_click != "": n_header = n_header + '"Click"' + ": " + '"' + n_click + '"' + ", " + if n_priority != "": n_header = n_header + '"Priority"' + ": " + '"' + n_priority + '"' + n_header = json.loads("{" + n_header + "}") + x_message = now_message + "\n\n" + n_message + +# todo POST the request **** NEEDS future TRY **** + requests.post(n_server + n_destination, data=x_message, headers=n_header) + +# Remove 1st argument from the list of command line arguments +argument_list = sys.argv[1:] + +# Short options +options: str = "u:s:d:p:m:t:c:hv" + +# Long options +long_options: list = ["server=", "sender=", "destination=", "priority=", "message=", "tags=", "click", "help", "view"] + +# Setting some minimal defaults in case the yaml config isn't available +d_server: str = "https://ntfy.sh/" +d_sender: str = "Security message" +d_destination: str = "phil_alerts" +d_priority: str = "5" +d_message: str = "Test message" +d_tags: str = "informational, testing, hard-coded" +d_click: str = "https://google.com" + +# Use the values from the config yaml if available. Overrides the minimal defaults. +server = d_server if (ic("ntfy_server") is None) else ic("ntfy_server") +sender = d_sender if (ic("ntfy_sender") is None) else ic("ntfy_sender") +destination = d_destination if (ic("ntfy_destination") is None) else ic("ntfy_destination") +priority = d_priority if (ic("ntfy_priority") is None) else ic("ntfy_priority") +message = d_message if (ic("ntfy_message") is None) else ic("ntfy_message") +tags = d_tags if (ic("ntfy_tags") is None) else ic("ntfy_tags") +click = d_click if (ic("ntfy_click") is None) else ic("ntfy_click") + +help_text: str = """ + -u, --server is the URL of the NTFY server, ending with a "/". Default is https://ntfy.sh/. + -s, --sender is the sender of the message, either an app name or a person. Default is "Wazuh (IDS)". + -d, --destination is the NTFY subscription, to send the message to. Default is none. + -p, --priority is the priority of the message, ranging from 1 (highest), to 5 (lowest). Default is 5. + -m, --message is the text of the message to be sent. Default is "Test message". + -t, --tags is an arbitrary strings of tags (keywords), seperated by a "," (comma). Default is "informational, testing, hard-coded". + -c, --click is a link (URL) that can be followed by tapping/clicking inside the message. Default is https://google.com. + -h, --help shows this help message. Must have no value argument. + -v, --view show config. +""" + +# Get params during execution. Params found here, override minimal defaults and/or config settings. +try: + # Parsing argument + arguments, values = getopt.getopt(argument_list, options, long_options) + + # Checking each argument + for current_argument, current_value in arguments: + + if current_argument in ("-h", "--help"): + print(help_text) + exit() + + elif current_argument in ("-v", "--view"): + vc() + exit() + + elif current_argument in ("-u", "--server"): + server = current_value + + elif current_argument in ("-s", "--sender"): + sender = current_value + + elif current_argument in ("-d", "--destination"): + destination = current_value + + elif current_argument in ("-p", "--priority"): + priority = current_value + + elif current_argument in ("-m", "--message"): + message = current_value + + elif current_argument in ("-t", "--tags"): + tags = current_value + + elif current_argument in ("-c", "--click"): + click = current_value + +except getopt.error as err: + # output error, and return with an error code + print(str(err)) + +# Finally, execute the POST request +ntfy_command(server, sender, destination, priority, message, tags, click) + diff --git a/wazuh_notifier_lib.py b/wazuh_notifier_lib.py new file mode 100755 index 0000000..ec51953 --- /dev/null +++ b/wazuh_notifier_lib.py @@ -0,0 +1,63 @@ +import os +import time +import yaml +from dotenv import load_dotenv + + +# Set structured timestamp. + + +def set_time(): + now_message = time.strftime('%a, %d %b %Y %H:%M:%S') + now_logging = time.strftime('%Y/%m/%d %H:%M:%S') + return now_message, now_logging + +# Define paths + + +def set_env(): + + wazuh_path = os.path.abspath(os.path.join(__file__, "../../..")) + ar_path = '{0}/logs/active-responses.log'.format(wazuh_path) + config_path = 'wazuh-notifier-config.yaml'.format(wazuh_path) + + return wazuh_path, ar_path, config_path + + +def import_config(key): + try: + _, _, config_path = set_env() + + with open(config_path, 'r') as ntfier_config: + config: dict = yaml.safe_load(ntfier_config) + value: str = config.get(key) + return value + except (FileNotFoundError, PermissionError, OSError): + return None + + +# Showing yaml config + +def view_config(): + + _, _, config_path = set_env() + + try: + with open(config_path, 'r') as ntfier_config: + print(ntfier_config.read()) + except (FileNotFoundError, PermissionError, OSError): + print(config_path + " does not exist or is not accessible") + return + + +# Logging the Wazuh active Response request + + +def ar_log(): + now = set_time() + _, ar_path, _ = set_env() + msg = '{0} {1} {2}'.format(now, os.path.realpath(__file__), 'Post JSON Alert') + f = open(ar_path, 'a') + f.write(msg + '\n') + f.close() +