diff --git a/.env b/.env index be7ea5c..2b864a6 100755 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -DISCORD_WEBHOOK=https://discord.com/api/webhooks/1235943329854783530/lgAd6On2gtLPCAZ0HABXCJvVFut7zTL0eGwYs7akkSQ7LEZA2hGtqKYag5vXMdBXJv6L - +DISCORD_URL=https://discord.com/api/webhooks/1235943329854783530/lgAd6On2gtLPCAZ0HABXCJvVFut7zTL0eGwYs7akkSQ7LEZA2hGtqKYag5vXMdBXJv6L +NTFY_URL=https://ntfy.sh/__KleinTest diff --git a/README.md b/README.md deleted file mode 100644 index 3287897..0000000 --- a/README.md +++ /dev/null @@ -1,126 +0,0 @@ -# Wazuh notifier - -Wazuh notifier enables the Wazuh manager to be notified when selected events occur. - -## Contents - -The main script is a custom active response Python script: wazuh-active-response.py.
-The actual sending of the messages is done by 2 notifier Python scripts:
-**Discord notifier**: wazuh-discord-notifier.py, and **NTFY.sh notifier**: wazuh-ntfy-notifier.py
-A YAML configuration: wazuh-notifier-config.yaml, and a Python module: wazuh_notifier_lib.py - -Wazuh notifier is a stateless implementation and only notifies, using the Discord and/or NTFY.sh messaging services. - -The Wazuh notifier is triggered by configuring the **ossec.conf** and adding an **active response configuration.** - -## Installation ## - -### Step 1 ### - -Download the files from https://github.com/RudiKlein/wazuh-notifier to your server. - -### Step 2 ### - -Copy the 4 Python files to the /var/ossec/active-response/bin/ folder - -``` -$ cp /wazuh-*.py /var/ossec/active-response/bin/ -``` - -Set the correct ownership - -``` -$ chown root:wazuh /var/ossec/active-response/bin/wazuh-*.py -``` - -Set the correct permissions - -``` -$ chmod uog+rx /var/ossec/active-response/bin/wazuh-*.py -``` - -### Step 3 ### - -Copy the YAML file to /var/ossec/etc/ - -``` -$ cp /wazuh-notifier-config.yaml /var/ossec/etc/ -``` - -Set the correct ownership - -``` -$ chown root:wazuh /var/ossec/etc/wazuh-notifier-config.yaml -``` - -Set the correct permissions - -``` -$ chmod uog+r /var/ossec/etc/wazuh-notifier-config.yaml -``` - -### Step 4 ### - -Modify the ossec.conf configuration file and add the following
- -``` - - wazuh-active-response - wazuh-active-response.py - yes - -``` - -``` - - wazuh-active-response - server - - - -``` - -Add the rules you want to be informed about between the , with the rules id's seperated by comma's. -Example: 5402, 3461, 8777
-(Please refer to the Wazuh online documentation for more information [^Wazuh docs]) - -[^Wazuh docs]: https://documentation.wazuh.com/current/user-manual/capabilities/active-response/index.html - -## The Active Response module ## - -The wazuh-active-response.py acts as the interface between Wazuh and the messaging notifiers for Discord and ntfy. -It is based on the example active response Python script in the [^Wazuh docs]. - -## The Discord notifier ## - -## The ntfy.sh notifier ## - -## The YAML configuration ## - -**Enable/disable the notifiers**
- -``` -discord_enabled: 1 (0 if not set in the yaml configuration) -ntfy_enabled: 1 (0 if not set in the yaml configuration) -``` - -**Exclude rules that are enabled in the ossec.conf active response definition.**
-This prevents the need to alter the ossec.conf for temporary rule disabling and stopping/starting wazuh-manager. -Additionally, agents can also be excluded from notifications. - -``` -excluded_rules: "5401, 5402, 5403" -excluded_agents: "999" -``` - -Default settings for the ntfy notifier. This overrules the hardcoded defaults. - -``` -ntfy_server: "https://ntfy.sh/" -ntfy_sender: "Wazuh (IDS)" -ntfy_destination: "__KleinTest" -ntfy_priority: "5" -ntfy_message: "Test message" -ntfy_tags: "information, testing, yaml" -ntfy_click: "https://google.com" -``` \ No newline at end of file diff --git a/golive.bsh b/golive.bsh new file mode 100755 index 0000000..df5c880 --- /dev/null +++ b/golive.bsh @@ -0,0 +1,10 @@ +#!/bin/bash +echo "Going live!" +sudo cp /home/rudi/pycharm/wazuh-notifier.py /var/ossec/active-response/bin +sudo cp /home/rudi/pycharm/wazuh_notifier_module.py /var/ossec/active-response/bin +sudo cp /home/rudi/pycharm/etc/wazuh-notify-config.yaml /var/ossec/etc +sudo cp /home/rudi/pycharm/wazuh-test-event.json /var/ossec/etc + +echo copied: wazuh-notifier.py, wazuh_notifier_module.py +echo "Live!" + diff --git a/wazuh-active-response.py b/wazuh-active-response.py deleted file mode 100755 index cf9db3c..0000000 --- a/wazuh-active-response.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python3 - - -# This script is adapted version of the Python active response script sample, provided by Wazuh, in the documentation: -# https://documentation.wazuh.com/current/user-manual/capabilities/active-response/custom-active-response-scripts.html -# It is provided under the below copyright statement: -# -# 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. -# -# This version has changes in -# 1) the first lines of code with the assignments, and -# 2) the Start Custom Action Add section -# This adapted version is free software. Rudi Klein, april 2024 - -import datetime -import json -import os -import sys -from pathlib import PureWindowsPath, PurePosixPath - -from wazuh_notifier_lib import import_config as ic -from wazuh_notifier_lib import set_env as se - -# Some variable assignments - -wazuh_path, ar_path, config_path = se() - -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(ar_path, 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 parameters_deconstruct(argv, event_keys): - a_id: str = str(event_keys["agent"]["id"]) - a_name: str = str(event_keys["agent"]["name"]) - e_id: str = str(event_keys["rule"]["id"]) - e_description: str = str(event_keys["rule"]["description"]) - e_level: str = str(event_keys["rule"]["level"]) - e_fired_times: str = str(event_keys["rule"]["firedtimes"]) - e_full_event: str = str(json.dumps(event_keys, indent=0).replace('"', '') - .replace('{', '') - .replace('}', '') - .replace('[', '') - .replace(']', '') - .replace(',', '') - .replace(' ', '') - ) - - if e_id in ic("excluded_rules") or a_id in ic("excluded_agents"): - - write_debug_file(argv[0], "Excluded rule or agent: " + e_id + "/" + a_id) - - else: - - return a_id, a_name, e_id, e_description, e_level, e_fired_times, e_full_event - - -def construct_basic_message(argv, accent: str, a_id: str, a_name: str, e_id: str, e_description: str, e_level: str, - e_fired_times: str): - # Adding the BOLD text string to the Discord message. Ntfy has a different message format. - - basic_message: str = ("--message " + '"' + - accent + "Agent: " + accent + a_name + " (" + a_id + ")" + "\n" + - accent + "Event id: " + accent + e_id + "\n" + - accent + "Description: " + accent + e_description + "\n" + - accent + "Threat level: " + accent + e_level + "\n" + - # Watch this last addition to the string. It should include the closing quote for the - # basic_message string. It must be closed by -> '"'. This will be done outside this function - # in order to enable another specific addition (event_full_message) in the calling procedure. - accent + "Times fired: " + accent + e_fired_times + "\n") - - return basic_message - - -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"]] - - agent_id, agent_name, event_id, event_description, event_level, event_fired_times, event_full_message = \ - parameters_deconstruct(argv, alert) - - 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 """ - - if str(ic("discord_enabled")) == "1": - - accent = "**" - discord_notifier = '{0}/active-response/bin/wazuh-discord-notifier.py'.format(wazuh_path) - discord_exec = "python3 " + discord_notifier + " " - write_debug_file(argv[0], "Start Discord notifier") - discord_message = construct_basic_message(argv, accent, agent_id, agent_name, event_id, event_description, - event_level, event_fired_times) - - - if ic("discord_full_message") == "1": - discord_message = discord_message + "\n" + accent + "__Full event__" + accent + event_full_message + '"' - else: - discord_message = discord_message + '"' - discord_command = discord_exec + discord_message - os.system(discord_command) - - if str(ic("ntfy_enabled")) == "1": - accent = "" - ntfy_notifier = '{0}/active-response/bin/wazuh-ntfy-notifier.py'.format(wazuh_path) - ntfy_exec = "python3 " + ntfy_notifier + " " - write_debug_file(argv[0], "Start NTFY notifier") - ntfy_message = construct_basic_message(argv, accent, agent_id, agent_name, event_level, event_description, - event_id, event_fired_times) - - # If the full message flag is set, the full message PLUS the closing parenthesis will be added - if ic("ntfy_full_message") == "1": - ntfy_message = ntfy_message + "\n" + "Full event" + event_full_message + '"' - else: - ntfy_message = ntfy_message + '"' - - ntfier_command = ntfy_exec + ntfy_message - os.system(ntfier_command) - - """ End Custom Action Add """ - - elif msg.command == DELETE_COMMAND: - - """ Start Custom Action Delete """ - - pass - - """ 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) diff --git a/wazuh-discord-notifier.py b/wazuh-discord-notifier.py deleted file mode 100755 index b96b798..0000000 --- a/wazuh-discord-notifier.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/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 (wazuh-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 getopt -import os -import sys -from os.path import join, dirname - -import requests -from dotenv import load_dotenv - -from wazuh_notifier_lib import import_config as ic -from wazuh_notifier_lib import set_env as se -from wazuh_notifier_lib import set_time as st -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 - -# Catching some path errors. -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. - -v, --view Show yaml configuration. -""" - -# 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 deleted file mode 100755 index 89f2f74..0000000 --- a/wazuh-notifier-config.yaml +++ /dev/null @@ -1,77 +0,0 @@ ---- -#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 (custom-wazuh-notifiers.py) 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 - -# Exclude rules that are listed in the ossec.conf active response definition. - -excluded_rules: "5401, 5402, 5403" -excluded_agents: "999" - -# COMMON configuration settings end 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: "__KleinTest" -ntfy_priority: "5" -ntfy_message: "Test message" -ntfy_tags: "information, testing, yaml" -ntfy_click: "https://google.com" - -# 1 to send the full event data with the message. 0 only sends the message with basic details -ntfy_full_message: "0" - -# NTFY configuration settings end 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" - -# 1 to send the full event data with the message. 0 only sends the message with basic details -discord_full_message: "0" - -# DISCORD configuration settings ends here. - -#end of yaml -... - - - diff --git a/wazuh-notify-config.yaml b/wazuh-notify-config.yaml new file mode 100644 index 0000000..1dd5c3b --- /dev/null +++ b/wazuh-notify-config.yaml @@ -0,0 +1,58 @@ +--- +# Start of wazuh notifier configuration yaml. + +# This is the yaml config file for wazuh-active-response (for both the Python and Go version) + +targets: "discord" # Platforms in this string with comma seperated values are triggered. +full_message: "ntfy, slack" # Platforms in this string will enable the sending of 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 as lists of integers. + + +priority_map: + - threat_map: [ 15,14,13,12 ] + mention_threshold: 1 + color: 0xcc3300 + - threat_map: [ 11,10,9 ] + mention_threshold: 1 + color: 0xff9966 + - threat_map: [ 8,7,6 ] + mention_threshold: 5 + color: 0xffcc00 + - threat_map: [ 5,4 ] + mention_threshold: 20 + color: 0x99cc33 + - threat_map: [ 3,2,1,0 ] + mention_threshold: 20 + color: 0x339900 + +# 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. + +# 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: True + +# Enabling this parameter provides more logging to the wazuh-notifier log. +extended_logging: True + +# Enabling this parameter provides extended logging to the console. +extended_print: True + +# End of wazuh notifier configuration yaml +... diff --git a/wazuh-notify-go/.env b/wazuh-notify-go/.env new file mode 100644 index 0000000..cecf3fc --- /dev/null +++ b/wazuh-notify-go/.env @@ -0,0 +1,2 @@ +DISCORD_URL=https://discord.com/api/webhooks/1237526475306176572/kHGnaQiM8qWOfdLIN1LWqgq3dsfqiHtsfs-Z5FralJNdX5hdw-MOPf4zzIDiFVjcIat4 +NTFY_URL=https://ntfy.sh/__KleinTest \ No newline at end of file diff --git a/wazuh-notify-go/go.mod b/wazuh-notify-go/go.mod new file mode 100644 index 0000000..5897e35 --- /dev/null +++ b/wazuh-notify-go/go.mod @@ -0,0 +1,8 @@ +module wazuh-notify + +go 1.22 + +require ( + github.com/joho/godotenv v1.5.1 + gopkg.in/yaml.v2 v2.4.0 +) diff --git a/wazuh-notify-go/go.sum b/wazuh-notify-go/go.sum new file mode 100644 index 0000000..f7e7502 --- /dev/null +++ b/wazuh-notify-go/go.sum @@ -0,0 +1,6 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/wazuh-notify-go/log/log.go b/wazuh-notify-go/log/log.go new file mode 100644 index 0000000..6597be4 --- /dev/null +++ b/wazuh-notify-go/log/log.go @@ -0,0 +1,39 @@ +package log + +import ( + "os" + "path" + "time" +) + +var logFile *os.File + +func OpenLogFile(BasePath string) { + logFile, _ = os.OpenFile(path.Join(BasePath, "../../logs/active-responses.log"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) + _, err := logFile.WriteString( + "\n#######################################\n## START ##" + + "\n" + time.Now().String() + + "\n#######################################\n", + ) + if err != nil { + panic(err) + } +} + +func CloseLogFile() { + _, err := logFile.WriteString( + "\n\n#######################################\n## CLOSE ##" + + "\n" + time.Now().String() + + "\n#######################################\n", + ) + if err != nil { + panic(err) + } + logFile.Close() +} + +func Log(message string) { + if _, err := logFile.WriteString("\n" + message + ": " + time.Now().String()); err != nil { + panic(err) + } +} diff --git a/wazuh-notify-go/main.go b/wazuh-notify-go/main.go new file mode 100644 index 0000000..d0a2980 --- /dev/null +++ b/wazuh-notify-go/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "strings" + "wazuh-notify/log" + "wazuh-notify/notification" + "wazuh-notify/services" +) + +func main() { + inputParams := services.InitNotify() + + for _, target := range strings.Split(inputParams.Targets, ",") { + switch target { + case "discord": + log.Log(target) + notification.SendDiscord(inputParams) + case "ntfy": + log.Log(target) + notification.SendNtfy(inputParams) + } + } + log.CloseLogFile() +} diff --git a/wazuh-notify-go/notification/discord.go b/wazuh-notify-go/notification/discord.go new file mode 100644 index 0000000..ac543a5 --- /dev/null +++ b/wazuh-notify-go/notification/discord.go @@ -0,0 +1,71 @@ +package notification + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "os" + "slices" + "strconv" + "strings" + "wazuh-notify/types" +) + +func SendDiscord(params types.Params) { + + var embedDescription string + + if slices.Contains(strings.Split(params.FullMessage, ","), "discord") { + fullMessage, _ := json.MarshalIndent(params.WazuhMessage, "", " ") + fullMessageString := strings.ReplaceAll(string(fullMessage), `"`, "") + fullMessageString = strings.ReplaceAll(fullMessageString, "{", "") + fullMessageString = strings.ReplaceAll(fullMessageString, "}", "") + fullMessageString = strings.ReplaceAll(fullMessageString, "[", "") + fullMessageString = strings.ReplaceAll(fullMessageString, "]", "") + fullMessageString = strings.ReplaceAll(fullMessageString, " ,", "") + + embedDescription = "\n\n ```" + + fullMessageString + + "```\n\n" + + "Priority: " + strconv.Itoa(params.Priority) + "\n" + + "Tags: " + params.Tags + "\n\n" + + params.Click + } else { + embedDescription = "\n\n" + + "**Agent:** " + params.WazuhMessage.Parameters.Alert.Agent.Name + "\n" + + "**Event id:** " + params.WazuhMessage.Parameters.Alert.Rule.ID + "\n" + + "**Rule:** " + params.WazuhMessage.Parameters.Alert.Rule.Description + "\n" + + "**Description: **" + params.WazuhMessage.Parameters.Alert.FullLog + "\n" + + "**Threat level:** " + strconv.Itoa(params.WazuhMessage.Parameters.Alert.Rule.Level) + "\n" + + "**Times fired:** " + strconv.Itoa(params.WazuhMessage.Parameters.Alert.Rule.Firedtimes) + + "\n\n" + + "Priority: " + strconv.Itoa(params.Priority) + "\n" + + "Tags: " + params.Tags + "\n\n" + + params.Click + } + + message := types.Message{ + Username: params.Sender, + Content: params.Mention, + Embeds: []types.Embed{ + { + Title: params.Sender, + Description: embedDescription, + Color: params.Color, + }, + }, + } + + payload := new(bytes.Buffer) + + err := json.NewEncoder(payload).Encode(message) + if err != nil { + return + } + + _, err = http.Post(os.Getenv("DISCORD_URL"), "application/json", payload) + if err != nil { + log.Fatalf("An Error Occured %v", err) + } +} diff --git a/wazuh-notify-go/notification/ntfy.go b/wazuh-notify-go/notification/ntfy.go new file mode 100644 index 0000000..995a102 --- /dev/null +++ b/wazuh-notify-go/notification/ntfy.go @@ -0,0 +1,38 @@ +package notification + +import ( + "net/http" + "os" + "strconv" + "strings" + "time" + "wazuh-notify/types" +) + +func SendNtfy(params types.Params) { + + payload := time.Now().Format(time.RFC3339) + "\n\n" + + "Agent: " + params.WazuhMessage.Parameters.Alert.Agent.Name + "\n" + + "Event id: " + params.WazuhMessage.Parameters.Alert.Rule.ID + "\n" + + "Description: " + params.WazuhMessage.Parameters.Alert.Rule.Description + "\n" + + "Threat level: " + strconv.Itoa(params.WazuhMessage.Parameters.Alert.Rule.Level) + "\n" + + "Times fired: " + strconv.Itoa(params.WazuhMessage.Parameters.Alert.Rule.Firedtimes) + "\n" + + req, _ := http.NewRequest("POST", os.Getenv("NTFY_URL"), strings.NewReader(payload)) + req.Header.Set("Content-Type", "text/plain") + + if params.Sender != "" { + req.Header.Add("Title", params.Sender) + } + if params.Tags != "" { + req.Header.Add("Tags", params.Tags) + } + if params.Click != "" { + req.Header.Add("Click", params.Click) + } + if params.Priority != 0 { + req.Header.Add("Priority", strconv.Itoa(params.Priority)) + } + + http.DefaultClient.Do(req) +} diff --git a/wazuh-notify-go/services/filters.go b/wazuh-notify-go/services/filters.go new file mode 100644 index 0000000..8f630b3 --- /dev/null +++ b/wazuh-notify-go/services/filters.go @@ -0,0 +1,24 @@ +package services + +import ( + "os" + "strings" + "wazuh-notify/log" +) + +func Filter() { + for _, rule := range strings.Split(inputParams.ExcludedRules, ",") { + if rule == inputParams.WazuhMessage.Parameters.Alert.Rule.ID { + log.Log("rule excluded") + log.CloseLogFile() + os.Exit(0) + } + } + for _, agent := range strings.Split(inputParams.ExcludedAgents, ",") { + if agent == inputParams.WazuhMessage.Parameters.Alert.Agent.ID { + log.Log("agent excluded") + log.CloseLogFile() + os.Exit(0) + } + } +} diff --git a/wazuh-notify-go/services/init.go b/wazuh-notify-go/services/init.go new file mode 100644 index 0000000..0a39453 --- /dev/null +++ b/wazuh-notify-go/services/init.go @@ -0,0 +1,97 @@ +package services + +import ( + "bufio" + "encoding/json" + "flag" + "github.com/joho/godotenv" + "gopkg.in/yaml.v2" + "os" + "path" + "slices" + "strings" + "wazuh-notify/log" + "wazuh-notify/types" +) + +var inputParams types.Params +var configParams types.Params +var wazuhData types.WazuhMessage + +func InitNotify() types.Params { + BaseFilePath, _ := os.Executable() + BaseDirPath := path.Dir(BaseFilePath) + + log.OpenLogFile(BaseDirPath) + + err := godotenv.Load(path.Join(BaseDirPath, "../../etc/.env")) + if err != nil { + log.Log("env failed to load") + godotenv.Load(path.Join(BaseDirPath, ".env")) + } else { + log.Log("env loaded") + } + + yamlFile, err := os.ReadFile(path.Join(BaseDirPath, "../../etc/wazuh-notify-config.yaml")) + if err != nil { + log.Log("yaml failed to load") + yamlFile, err = os.ReadFile(path.Join(BaseDirPath, "wazuh-notify-config.yaml")) + } + err = yaml.Unmarshal(yamlFile, &configParams) + if err != nil { + print(err) + } + + log.Log("yaml loaded") + configParamString, _ := json.Marshal(configParams) + log.Log(string(configParamString)) + + flag.StringVar(&inputParams.Url, "url", "", "is the webhook URL of the Discord server. It is stored in .env.") + flag.StringVar(&inputParams.Click, "click", configParams.Click, "is a link (URL) that can be followed by tapping/clicking inside the message. Default is https://google.com.") + flag.IntVar(&inputParams.Priority, "priority", 0, "is the priority of the message, ranging from 1 (highest), to 5 (lowest). Default is 5.") + flag.StringVar(&inputParams.Sender, "sender", configParams.Sender, "is the sender of the message, either an app name or a person. The default is \"Security message\".") + flag.StringVar(&inputParams.Tags, "tags", "", "is an arbitrary strings of tags (keywords), seperated by a \",\" (comma). Default is \"informational,testing,hard-coded\".") + flag.StringVar(&inputParams.Targets, "targets", "", "is a list of targets to send notifications to. Default is \"discord\".") + + flag.Parse() + + log.Log("params loaded") + inputParamString, _ := json.Marshal(inputParams) + log.Log(string(inputParamString)) + + inputParams.Targets = configParams.Targets + inputParams.FullMessage = configParams.FullMessage + inputParams.ExcludedAgents = configParams.ExcludedAgents + inputParams.ExcludedRules = configParams.ExcludedRules + inputParams.PriorityMaps = configParams.PriorityMaps + + wazuhInput() + + return inputParams +} + +func wazuhInput() { + reader := bufio.NewReader(os.Stdin) + + json.NewDecoder(reader).Decode(&wazuhData) + + inputParams.Tags += strings.Join(wazuhData.Parameters.Alert.Rule.Groups, ",") + + inputParams.WazuhMessage = wazuhData + + for i, _ := range configParams.PriorityMaps { + if slices.Contains(configParams.PriorityMaps[i].ThreatMap, wazuhData.Parameters.Alert.Rule.Level) { + inputParams.Color = inputParams.PriorityMaps[i].Color + if inputParams.WazuhMessage.Parameters.Alert.Rule.Firedtimes >= inputParams.PriorityMaps[i].MentionThreshold { + inputParams.Mention = "@here" + } + inputParams.Priority = 5 - i + } + } + + Filter() + + log.Log("Wazuh data loaded") + inputParamString, _ := json.Marshal(inputParams) + log.Log(string(inputParamString)) +} diff --git a/wazuh-notify-go/types/types.go b/wazuh-notify-go/types/types.go new file mode 100644 index 0000000..ce40ca3 --- /dev/null +++ b/wazuh-notify-go/types/types.go @@ -0,0 +1,36 @@ +package types + +type Params struct { + Url string + Sender string `yaml:"sender,omitempty"` + Priority int + Tags string + Click string `yaml:"click,omitempty"` + Targets string `yaml:"targets,omitempty"` + FullMessage string `yaml:"full_message,omitempty"` + ExcludedRules string `yaml:"excluded_rules,omitempty"` + ExcludedAgents string `yaml:"excluded_agents,omitempty"` + Color int + Mention string + WazuhMessage WazuhMessage + PriorityMaps []PriorityMap `yaml:"priority_map"` +} + +type PriorityMap struct { + ThreatMap []int `yaml:"threat_map"` + MentionThreshold int `yaml:"mention_threshold"` + Color int `yaml:"color"` +} + +type Message struct { + Username string `json:"username,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Content string `json:"content,omitempty"` + Embeds []Embed `json:"embeds,omitempty"` +} + +type Embed struct { + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Color int `json:"color,omitempty"` +} diff --git a/wazuh-notify-go/types/wazuh.go b/wazuh-notify-go/types/wazuh.go new file mode 100644 index 0000000..4a4da76 --- /dev/null +++ b/wazuh-notify-go/types/wazuh.go @@ -0,0 +1,72 @@ +package types + +type WazuhMessage struct { + Version int `json:"version"` + Origin Origin `json:"origin"` + Command string `json:"command"` + Parameters Parameters `json:"parameters"` +} + +type Origin struct { + Name string `json:"name"` + Module string `json:"module"` +} + +type Parameters struct { + ExtraArgs []interface{} `json:"extra_args"` + Alert Alert `json:"alert"` + Program string `json:"program"` +} + +type Alert struct { + Timestamp string `json:"timestamp"` + Rule Rule `json:"rule"` + Agent Agent `json:"agent"` + Manager Manager `json:"manager"` + ID string `json:"id"` + FullLog string `json:"full_log"` + Decoder Decoder `json:"decoder"` + Data Data `json:"data"` + Location string `json:"location"` +} + +type Rule struct { + Level int `json:"level"` + Description string `json:"description"` + ID string `json:"id"` + Mitre Mitre `json:"mitre"` + Info string `json:"info"` + Firedtimes int `json:"firedtimes"` + Mail bool `json:"mail"` + Groups []string `json:"groups"` + PciDss []string `json:"pci_dss"` + Gdpr []string `json:"gdpr"` + Nist80053 []string `json:"nist_800_53"` + Tsc []string `json:"tsc"` +} + +type Mitre struct { + ID []string `json:"id"` + Tactic []string `json:"tactic"` + Technique []string `json:"technique"` +} + +type Agent struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type Manager struct { + Name string `json:"name"` +} + +type Decoder struct { + Name string `json:"name"` +} + +type Data struct { + Protocol string `json:"protocol"` + Srcip string `json:"srcip"` + ID string `json:"id"` + URL string `json:"url"` +} diff --git a/wazuh-notify-go/wazuh-notify-config.yaml b/wazuh-notify-go/wazuh-notify-config.yaml new file mode 100644 index 0000000..e455811 --- /dev/null +++ b/wazuh-notify-go/wazuh-notify-config.yaml @@ -0,0 +1,49 @@ +--- +#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 + +targets: "discord,ntfy" +full_message: "ntfy" + +# Exclude rules that are listed in the ossec.conf active response definition. + +excluded_rules: "5401,5403" +excluded_agents: "999" + +# Priority mapping from 1-12 (Wazuh events) to 1-5 (Discord and ntfy notification) +# Discord mention after x amount of event fired times + +priority_map: + - + threat_map: [15,14,13,12] + mention_threshold: 1 + color: 0xcc3300 + - + threat_map: [11,10,9] + mention_threshold: 1 + color: 0xff9966 + - + threat_map: [8,7,6] + mention_threshold: 5 + color: 0xffcc00 + - + threat_map: [5,4] + mention_threshold: 5 + color: 0x99cc33 + - + threat_map: [3,2,1,0] + mention_threshold: 5 + color: 0x339900 + + +sender: "Wazuh (IDS)" +click: "https://google.com" + + +#end of yaml +... + + + diff --git a/wazuh-notify-python/requirements.txt b/wazuh-notify-python/requirements.txt new file mode 100644 index 0000000..5b5f95f --- /dev/null +++ b/wazuh-notify-python/requirements.txt @@ -0,0 +1,3 @@ +requests~=2.31.0 +PyYAML~=5.4.1 +python-dotenv~=1.0.1 \ No newline at end of file diff --git a/wazuh-notify-python/wazuh-notify.py b/wazuh-notify-python/wazuh-notify.py new file mode 100755 index 0000000..3e56d8b --- /dev/null +++ b/wazuh-notify-python/wazuh-notify.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +# This script is adapted version of the Python active response script sample, provided by Wazuh, in the documentation: +# https://documentation.wazuh.com/current/user-manual/capabilities/active-response/custom-active-response-scripts.html +# It is provided under the below copyright statement: +# +# 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. +# +# This adapted version is free software. Rudi Klein, april 2024 + +import json +import sys + +import requests + +from wazuh_notify_module import build_notification +from wazuh_notify_module import construct_basic_message +from wazuh_notify_module import exclusions_check +from wazuh_notify_module import get_arguments +from wazuh_notify_module import get_config +from wazuh_notify_module import get_env +from wazuh_notify_module import load_message +from wazuh_notify_module import logger +from wazuh_notify_module import set_environment +from wazuh_notify_module import threat_mapping + + +def main(argv): + # Load the YAML config + config: dict = get_config() + + # Path variables assignments + wazuh_path, ar_path, config_path = set_environment() + + # Get the arguments used with running the script + arg_url, arg_sender, arg_destination, arg_message, arg_priority, arg_tags, arg_click = get_arguments() + + # Check if we are in test mode (test_mode setting in config yaml). If so, load test event instead of live event. + if config.get("test_mode"): + logger(config, "In test mode: using test message wazuh-notify-test-event.json") + + with (open('wazuh-notify-test-event.json') as event_file): + data: dict = json.loads(event_file.read()) + + else: + logger(config, "In live mode: using live message") + data = load_message() + + # Extract the 'alert' section of the (JSON) event + alert = data["parameters"]["alert"] + + # Check the config for any exclusion rules + fire_notification = exclusions_check(config, alert) + + if not fire_notification: + logger(config, "Event excluded, no notification sent. Exiting") + exit() + + # Include a specific control sequence for Discord bold text + if "discord" in config["targets"]: + accent: str = "**" + else: + accent: str = "" + + # Create the notification text to be sent. + notification: str = construct_basic_message(accent, alert) + logger(config, "Notification constructed") + + # todo Not used? + # Get the mapping from event threat level to priority (Discord/ntfy), color (Discord) and mention_flag (Discord) + priority, color, mention = threat_mapping(config, alert['rule']['level'], + alert['rule']['firedtimes']) + + result = "" + # Prepare the messaging platform specific request and execute + if "discord" in config["targets"]: + caller = "discord" + discord_url, _, _ = get_env() + payload = build_notification(caller, config, notification, alert, priority, color, mention) + result = requests.post(discord_url, json=payload) + exit(result) + + if "ntfy" in config["targets"]: + caller = "ntfy" + ntfy_url, _, _ = get_env() + payload = build_notification(caller, config, notification, alert, priority, color, mention) + result = requests.post(ntfy_url, json=payload) + exit(result) + + if "slack" in config["targets"]: + caller = "slack" + slack_url, _, _ = get_env() + payload = build_notification(caller, config, notification, alert, priority, color, mention) + result = requests.post(slack_url, json=payload) + exit(result) + + +if __name__ == "__main__": + main(sys.argv) diff --git a/wazuh-notify-python/wazuh_notify_module.py b/wazuh-notify-python/wazuh_notify_module.py new file mode 100755 index 0000000..311cada --- /dev/null +++ b/wazuh-notify-python/wazuh_notify_module.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 + +import datetime +import getopt +import json +import os +import sys +import time +from os.path import join, dirname +from pathlib import PureWindowsPath, PurePosixPath + +import yaml +from dotenv import load_dotenv + + +# Define paths: wazuh_path = wazuh root directory +# ar_path = active-responses.log path, +# config_path = wazuh-notify-config.yaml +# +def set_environment() -> tuple: + # todo fix reference when running manually/in process + + set_wazuh_path = "/home/rudi/pycharm" + # set_wazuh_path = os.path.abspath(os.path.join(__file__, "../../..")) + set_ar_path = '{0}/logs/wazuh-notifier.log'.format(set_wazuh_path) + set_config_path = '{0}/etc/wazuh-notify-config.yaml'.format(set_wazuh_path) + + return set_wazuh_path, set_ar_path, set_config_path + + +# Set paths for use in this module +wazuh_path, ar_path, config_path = set_environment() + + +# Set structured timestamps for logging and notifications. + + +def set_time_format(): + now_message = time.strftime('%A, %d %b %Y %H:%M:%S') + now_logging = time.strftime('%Y/%m/%d %H:%M:%S') + now_time = time.strftime('%H:%M') + now_weekday = time.strftime('%A') + return now_message, now_logging, now_weekday, now_time + + +# Logger +def logger(config, message): + # todo fix logging + + _, log_path, _ = set_environment() + + if config.get('extended_print', True): + print(time.strftime('%Y/%m/%d %H:%M:%S'), "|", message) + + if config.get("extended_logging"): + with open(ar_path, mode="a") as log_file: + ar_name_posix = str(PurePosixPath(PureWindowsPath(log_path[log_path.find("active-response"):]))) + log_file.write( + str(datetime.datetime.now().strftime( + '%Y/%m/%d %H:%M:%S')) + " " + ar_name_posix + ": " + message + "\n") + else: + pass + + +# Get the content of the .env file + + +def get_env(): + config: dict = get_config() + + try: + dotenv_path = join(dirname(__file__), '.env') + load_dotenv(dotenv_path) + if not os.path.isfile(dotenv_path): + logger(config, ".env not found") + raise Exception(dotenv_path, "file not found") + + # Retrieve url from .env + discord_url = os.getenv("DISCORD_URL") + ntfy_url = os.getenv("NTFY_URL") + slack_url = os.getenv("SLACK_URL") + + except Exception as err: + # output error, and return with an error code + logger(config, str(Exception(err.args))) + exit(err) + + return discord_url, ntfy_url, slack_url + + +# Process configuration settings from wazuh-notify-config.yaml + + +def get_config(): + # DO NOT USE logger() IN THIS FUNCTION. IT WILL CREATE A RECURSION LOOP! + + this_config_path: str = "" + + try: + _, _, this_config_path = set_environment() + + with open(this_config_path, 'r') as ntfier_config: + config: dict = yaml.safe_load(ntfier_config) + except (FileNotFoundError, PermissionError, OSError): + print(time.strftime('%Y/%m/%d %H:%M:%S') + " | " + this_config_path + " missing") + + print(time.strftime('%Y/%m/%d %H:%M:%S') + " | " + "reading config: " + this_config_path) + config['targets'] = config.get('targets', 'ntfy, discord') + config['excluded_rules'] = config.get('excluded_rules', '') + config['excluded_agents'] = config.get('excluded_agents', '') + config['excluded_days'] = config.get('excluded_days', '') + config['excluded_hours'] = config.get('excluded_hours', '') + config['test_mode'] = config.get('test_mode', True) + config['extended_logging'] = config.get('extended_logging', True) + config['extended_print'] = config.get('extended_print', True) + + config['sender'] = 'Wazuh (IDS)' + config['click'] = 'https://wazuh.org' + + return config + + +# Show configuration settings from wazuh-notify-config.yaml + + +def view_config(): + _, _, this_config_path, _ = set_environment() + + try: + with open(this_config_path, 'r') as ntfier_config: + print(ntfier_config.read()) + except (FileNotFoundError, PermissionError, OSError): + print(this_config_path + " does not exist or is not accessible") + return + + # Get params during execution. Params found here override config settings. + + +def get_arguments(): + config: dict = get_config() + # Short options + options: str = "u:s:p:m:t:c:hv" + + # Long options + long_options: list = ["url=", "sender=", "destination=", "priority=", "message=", "tags=", "click=", "help", + "view"] + + help_text: str = """ + -u, --url is the url for the server, ending with a "/". + -s, --sender is the sender of the message, either an app name or a person. + -d, --destination is the NTFY subscription or Discord title, to send the message to. + -p, --priority is the priority of the message, ranging from 1 (lowest), to 5 (highest). + -m, --message is the text of the message to be sent. + -t, --tags is an arbitrary strings of tags (keywords), seperated by a "," (comma). + -c, --click is a link (URL) that can be followed by tapping/clicking inside the message. + -h, --help shows this help message. Must have no value argument. + -v, --view show config. + + """ + url: str + sender: str + destination: str + message: str + priority: int + tags: str + click: str + + url, sender, destination, message, priority, tags, click = "", "", "", "", 0, "", "" + + argument_list: list = sys.argv[1:] + + if not argument_list: + logger(config, 'No argument list found (no arguments provided with script execution') + return url, sender, destination, message, priority, tags, click + + else: + + try: + + logger(config, "Parsing arguments") + + # Parsing arguments + 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"): + view_config() + exit() + + elif current_argument in ("-u", "--url"): + url: str = current_value + + elif current_argument in ("-s", "--sender"): + sender: str = current_value + + elif current_argument in ("-d", "--destination"): + destination: str = current_value + + elif current_argument in ("-p", "--priority"): + priority: int = int(current_value) + + elif current_argument in ("-m", "--message"): + message: str = current_value + + elif current_argument in ("-t", "--tags"): + tags: str = current_value + + elif current_argument in ("-c", "--click"): + click: str = current_value + + except getopt.error as err: + # output error, and return with an error code + + logger(config, str(err)) + + return url, sender, destination, message, priority, tags, click + + +# Receive and load message from Wazuh + + +def load_message(): + config: dict = get_config() + + # get alert from stdin + + logger(config, "Loading event message from STDIN") + + input_str: str = "" + for line in sys.stdin: + input_str: str = line + break + + data: json = json.loads(input_str) + + if data.get("command") == "add": + logger(config, "Relevant event data found") + return data + else: + # todo fix error message + sys.exit(1) + + +# Check if there are reasons not to process this event (as per config yaml) + + +def exclusions_check(config, alert): + # Set some environment + now_message, now_logging, now_weekday, now_time = set_time_format() + + # Check the exclusion records from the configuration yaml + ex_hours: tuple = config.get('excluded_hours') + + # 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 + + # Get some more exclusion records from the config + ex_days = config.get('excluded_days') + ex_agents = config.get("excluded_agents") + ex_rules = config.get("excluded_rules") + + # Check agent and rule from within the event + ev_agent = alert['agent']['id'] + ev_rule = alert['rule']['id'] + + # Let's assume all lights are green + 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. + if (now_time > ex_hours[0]) and (now_time < ex_hours[1]): + logger(config, "excluded: event inside exclusion time frame") + ex_hours_eval = False + elif now_weekday in ex_days: + logger(config, "excluded: event inside excluded weekdays") + ex_weekday_eval = False + elif ev_rule in ex_rules: + logger(config, "excluded: event id inside exclusion list") + ev_rule_eval = False + elif ev_agent in ex_agents: + logger(config, "excluded: event agent inside exclusion list") + ev_rule_eval = False + + notification_eval = ex_hours_eval and ex_weekday_eval and ev_rule_eval and ev_agent + + return notification_eval + + +# 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): + # Map threat level v/s priority + + p_map = config.get('priority_map') + + for i in range(len(p_map)): + + if threat_level in p_map[i]["threat_map"]: + color_mapping = p_map[i]["color"] + priority_mapping = 5 - i + if fired_times >= p_map[i]["mention_threshold"]: + mention_flag = "@here" + else: + mention_flag = "" + return priority_mapping, color_mapping, mention_flag + else: + return 0, 0, 0 + + +# Construct the message that will be sent to the notifier platforms + + +def construct_basic_message(accent: str, data: dict) -> str: + # Adding the BOLD text string for Discord use + + basic_msg: str = (accent + + "Agent:" + " " + accent + data["agent"]["name"] + " (" + data["agent"][ + "id"] + ")" + "\n" + accent + + "Rule id: " + accent + data["rule"]["id"] + "\n" + accent + + "Rule: " + accent + data["rule"]["description"] + "\n" + accent + + "Description: " + accent + data["full_log"] + "\n" + accent + + "Threat level: " + accent + str(data["rule"]["level"]) + "\n" + accent + + "Times fired: " + accent + str(data["rule"]["firedtimes"]) + "\n") + + return basic_msg + + +def build_notification(caller, config, notification, alert, priority, color, mention): + click: str = config.get('click') + sender: str = config.get('sender') + priority: str = str(priority) + tags = (str(alert['rule']['groups']).replace("[", "") + .replace("]", "") + .replace("'", "") + .replace(",", ", ") + ) + full_event: str = str(json.dumps(alert, indent=4) + .replace('"', '') + .replace('{', '') + .replace('}', '') + .replace('[', '') + .replace(']', '') + .replace(',', ' ') + ) + # Add the full alert data to the notification + if caller in config["full_message"]: + notification: str = ("\n\n" + notification + "\n" + + "**" + "__Full event__" + "**" + "\n" + "```\n" + full_event + "```") + + # Add Priority & tags to the notification + notification = (notification + "\n\n" + "Priority: " + priority + "\n" + + "Tags: " + tags + "\n\n" + click) + + # Prepare the messaging platform specific notification and execute + if "discord" in config["targets"]: + url, _, _ = get_env() + + payload = {"username": "sender", + "content": mention, + "embeds": [{"description": notification, + "color": color, + "title": sender}]} + return payload + + if "ntfy" in config["targets"]: + caller = "ntfy" + ntfy_url, _, _ = get_env() + + payload = {"username": "sender", + "content": mention, + "embeds": [{"description": notification, + "color": color, + "title": sender}]} + return payload + + if "slack" in config["targets"]: + caller = "slack" + slack_url, _, _ = get_env() + + payload = {"username": "sender", + "content": mention, + "embeds": [{"description": notification, + "color": color, + "title": sender}]} + return payload diff --git a/wazuh-notify-test-event.json b/wazuh-notify-test-event.json new file mode 100644 index 0000000..b7105eb --- /dev/null +++ b/wazuh-notify-test-event.json @@ -0,0 +1,76 @@ +{ + "version": 1, + "origin": { + "name": "worker01", + "module": "wazuh-execd" + }, + "command": "add", + "parameters": { + "extra_args": [], + "alert": { + "timestamp": "2021-02-01T20:58:44.830+0000", + "rule": { + "level": 15, + "description": "Shellshock attack detected", + "id": "31168", + "mitre": { + "id": [ + "T1068", + "T1190" + ], + "tactic": [ + "Privilege Escalation", + "Initial Access" + ], + "technique": [ + "Exploitation for Privilege Escalation", + "Exploit Public-Facing Application" + ] + }, + "info": "CVE-2014-6271https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-6271", + "firedtimes": 2, + "mail": true, + "groups": [ + "web", + "accesslog", + "attack" + ], + "pci_dss": [ + "11.4" + ], + "gdpr": [ + "IV_35.7.d" + ], + "nist_800_53": [ + "SI.4" + ], + "tsc": [ + "CC6.1", + "CC6.8", + "CC7.2", + "CC7.3" + ] + }, + "agent": { + "id": "000", + "name": "wazuh-server" + }, + "manager": { + "name": "wazuh-server" + }, + "id": "1612213124.6448363", + "full_log": "192.168.0.223 - - [01/Feb/2021:20:58:43 +0000] \"GET / HTTP/1.1\" 200 612 \"-\" \"() { :; }; /bin/cat /etc/passwd\"", + "decoder": { + "name": "web-accesslog" + }, + "data": { + "protocol": "GET", + "srcip": "192.168.0.223", + "id": "200", + "url": "/" + }, + "location": "/var/log/nginx/access.log" + }, + "program": "/var/ossec/active-response/bin/firewall-drop" + } +} \ No newline at end of file diff --git a/wazuh-ntfy-notifier.py b/wazuh-ntfy-notifier.py deleted file mode 100755 index f037fe8..0000000 --- a/wazuh-ntfy-notifier.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/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 (wazuh-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 getopt -import json -import sys - -import requests - -from wazuh_notifier_lib import import_config as ic -from wazuh_notifier_lib import set_env as se -from wazuh_notifier_lib import set_time as st -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 deleted file mode 100755 index 127dd88..0000000 --- a/wazuh_notifier_lib.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import time - -import yaml - - -# Set structured timestamp for logging and discord/ntfy message. - - -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: wazuh_path = wazuh root directory -# ar_path = active-responses.log path, -# config_path = wazuh-notifier-config.yaml - -def set_env(): - - wazuh_path = os.path.abspath(os.path.join(__file__, "../../..")) - ar_path = '{0}/logs/active-responses.log'.format(wazuh_path) - config_path = '{0}/etc/wazuh-notifier-config.yaml'.format(wazuh_path) - - return wazuh_path, ar_path, config_path - - -# Import configuration settings from wazuh-notifier-config.yaml - - -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 - - -# Show configuration settings from wazuh-notifier-config.yaml - - -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() -