Merge pull request #4 from RudiKlein/TOML

Toml update
This commit is contained in:
Rudi klein 2024-05-28 13:25:32 +02:00 committed by GitHub
commit 79e3e9e1a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 692 additions and 772 deletions

84
wazuh-notify-config.toml Normal file
View File

@ -0,0 +1,84 @@
#############################################################################################################
# This is the TOML config file for wazuh-notify (active response) for both the Python and Go implementation #
#############################################################################################################
[general]
# Platforms in this string with comma seperated values are triggered.
targets = "slack, ntfy, discord"
# Platforms in this string will enable sending the full event information.
full_alert = ""
# 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"
excluded_agents = "99999"
# The next 2 settings are used to add information to the messages.
sender = "Wazuh (IDS)"
click = "https://documentation.wazuh.com/"
# 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
notify_threshold = 1
color = 0xec3e40 # Red, SEVERE
[[priority_map]]
threat_map = [11, 10, 9]
mention_threshold = 1
notify_threshold = 1
color = 0xff9b2b # Orange, HIGH
[[priority_map]]
threat_map = [8, 7, 6]
mention_threshold = 5
notify_threshold = 5
color = 0xf5d800 # Yellow, ELEVATED
[[priority_map]]
threat_map = [5, 4]
mention_threshold = 20
notify_threshold = 5
color = 0x377fc7 # Blue, GUARDED
[[priority_map]]
threat_map = [3, 2, 1, 0]
mention_threshold = 20
notify_threshold = 5
color = 0x01a465 # Green, LOW
################ End of priority mapping ##################################
# Following parameter defines the markdown characters to emphasise the parameter names in the notification messages
[markdown_emphasis]
slack = "*"
ntfy = "**"
discord = "**"
##################################################################################
# From here on the settings are ONLY used by the Python version of wazuh-notify. #
##################################################################################
[python]
# The next settings are used for testing and troubleshooting.
# 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 = 2
# Enabling this parameter provides extended logging to the console.
extended_print = 2
# Below settings provide for a window that enable/disables events from firing the notifiers.
excluded_days = ""
# Enter as a tuple of string values. Be aware of your regional settings.
excluded_hours = ["23:59", "00:00"]

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

@ -1,2 +0,0 @@
DISCORD_URL=https://discord.com/api/webhooks/1237526475306176572/kHGnaQiM8qWOfdLIN1LWqgq3dsfqiHtsfs-Z5FralJNdX5hdw-MOPf4zzIDiFVjcIat4
NTFY_URL=https://ntfy.sh/__KleinTest

View File

@ -3,6 +3,6 @@ module wazuh-notify
go 1.22 go 1.22
require ( require (
github.com/BurntSushi/toml v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
gopkg.in/yaml.v2 v2.4.0
) )

View File

@ -1,3 +1,5 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -2,25 +2,32 @@ package main
import ( import (
"strings" "strings"
"wazuh-notify/log"
"wazuh-notify/notification"
"wazuh-notify/services" "wazuh-notify/services"
"wazuh-notify/services/log"
"wazuh-notify/targets/discord"
"wazuh-notify/targets/ntfy"
"wazuh-notify/targets/slack"
) )
func main() { func main() {
inputParams := services.InitNotify() //Read config file and .env
configParams := services.ReadConfig()
//Parse command line flags
inputParams := services.ParseFlags(configParams)
//Parse wazuh input data from stdin
Params := services.ParseWazuhInput(inputParams)
for _, target := range strings.Split(inputParams.Targets, ",") { for _, target := range strings.Split(Params.General.Targets, ", ") {
switch target { switch target {
case "discord": case "discord":
log.Log(target) log.Log(target)
notification.SendDiscord(inputParams) discord.SendDiscord(Params)
case "ntfy": case "ntfy":
log.Log(target) log.Log(target)
notification.SendNtfy(inputParams) ntfy.SendNtfy(Params)
case "slack": case "slack":
log.Log(target) log.Log(target)
notification.SendSlack(inputParams) slack.SendSlack(Params)
} }
} }
log.CloseLogFile() log.CloseLogFile()

View File

@ -1,73 +0,0 @@
package notification
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"wazuh-notify/types"
)
func SendDiscord(params types.Params) {
var embedDescription string
if slices.Contains(strings.Split(params.FullAlert, ","), "discord") {
fullAlert, _ := json.MarshalIndent(params.WazuhMessage, "", " ")
fullAlertString := strings.ReplaceAll(string(fullAlert), `"`, "")
fullAlertString = strings.ReplaceAll(fullAlertString, "{", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "}", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "[", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "]", "")
fullAlertString = strings.ReplaceAll(fullAlertString, " ,", "")
embedDescription = "\n\n ```" +
fullAlertString +
"```\n\n" +
"Priority: " + strconv.Itoa(params.Priority) + "\n" +
"Tags: " + params.Tags + "\n\n" +
params.Click
} else {
embedDescription = "\n\n" +
"**Timestamp: **" + time.Now().Format(time.DateTime) + "\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)
}
}

View File

@ -1,56 +0,0 @@
package notification
import (
"encoding/json"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"wazuh-notify/types"
)
func SendNtfy(params types.Params) {
var payload string
if slices.Contains(strings.Split(params.FullAlert, ","), "discord") {
fullAlert, _ := json.MarshalIndent(params.WazuhMessage, "", " ")
fullAlertString := strings.ReplaceAll(string(fullAlert), `"`, "")
fullAlertString = strings.ReplaceAll(fullAlertString, "{", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "}", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "[", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "]", "")
fullAlertString = strings.ReplaceAll(fullAlertString, " ,", "")
payload = "\n\n ```" +
fullAlertString +
"```"
} else {
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)
}

View File

@ -1,73 +0,0 @@
package notification
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"wazuh-notify/types"
)
func SendSlack(params types.Params) {
var embedDescription string
if slices.Contains(strings.Split(params.FullAlert, ","), "slack") {
fullAlert, _ := json.MarshalIndent(params.WazuhMessage, "", " ")
fullAlertString := strings.ReplaceAll(string(fullAlert), `"`, "")
fullAlertString = strings.ReplaceAll(fullAlertString, "{", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "}", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "[", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "]", "")
fullAlertString = strings.ReplaceAll(fullAlertString, " ,", "")
embedDescription = "\n\n ```" +
fullAlertString +
"```\n\n" +
"Priority: " + strconv.Itoa(params.Priority) + "\n" +
"Tags: " + params.Tags + "\n\n" +
params.Click
} else {
embedDescription = "\n\n" +
"**Timestamp: **" + time.Now().Format(time.DateTime) + "\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("SLACK_URL"), "application/json", payload)
if err != nil {
log.Fatalf("An Error Occured %v", err)
}
}

View File

@ -0,0 +1,42 @@
package services
import (
"github.com/BurntSushi/toml"
"github.com/joho/godotenv"
"os"
"path"
"wazuh-notify/services/log"
"wazuh-notify/types"
)
func ReadConfig() types.Params {
var configParams types.Params
//Get Path of executable location
baseFilePath, _ := os.Executable()
baseDirPath := path.Dir(baseFilePath)
//Open log file and set first message
log.OpenLogFile(baseDirPath)
//Load .env into environment variables
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")
}
//Read config file
tomlFile, err := os.ReadFile(path.Join(baseDirPath, "../../etc/wazuh-notify-config.toml"))
if err != nil {
log.Log("toml failed to load")
tomlFile, err = os.ReadFile(path.Join(baseDirPath, "wazuh-notify-config.toml"))
}
err = toml.Unmarshal(tomlFile, &configParams)
if err != nil {
print(err)
} else {
log.Log("yaml loaded")
}
return configParams
}

View File

@ -3,19 +3,20 @@ package services
import ( import (
"os" "os"
"strings" "strings"
"wazuh-notify/log" "wazuh-notify/services/log"
"wazuh-notify/types"
) )
func Filter() { func Filter(params types.Params) {
for _, rule := range strings.Split(inputParams.ExcludedRules, ",") { for _, rule := range strings.Split(params.General.ExcludedRules, ",") {
if rule == inputParams.WazuhMessage.Parameters.Alert.Rule.ID { if rule == params.WazuhMessage.Parameters.Alert.Rule.ID {
log.Log("rule excluded") log.Log("rule excluded")
log.CloseLogFile() log.CloseLogFile()
os.Exit(0) os.Exit(0)
} }
} }
for _, agent := range strings.Split(inputParams.ExcludedAgents, ",") { for _, agent := range strings.Split(params.General.ExcludedAgents, ",") {
if agent == inputParams.WazuhMessage.Parameters.Alert.Agent.ID { if agent == params.WazuhMessage.Parameters.Alert.Agent.ID {
log.Log("agent excluded") log.Log("agent excluded")
log.CloseLogFile() log.CloseLogFile()
os.Exit(0) os.Exit(0)

View File

@ -0,0 +1,20 @@
package services
import (
"flag"
"wazuh-notify/services/log"
"wazuh-notify/types"
)
func ParseFlags(params types.Params) types.Params {
//Set command line flags
flag.StringVar(&params.General.Click, "click", params.General.Click, "is a link (URL) that can be followed by tapping/clicking inside the message. Default is https://google.com.")
flag.StringVar(&params.General.Sender, "sender", params.General.Sender+" Golang", "is the sender of the message, either an app name or a person. The default is \"Security message\".")
flag.StringVar(&params.General.Targets, "targets", params.General.Targets, "is a list of targets to send notifications to. Default is \"discord\".")
//Get flag values
flag.Parse()
log.Log("flags loaded")
return params
}

View File

@ -1,97 +0,0 @@
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.FullAlert = configParams.FullAlert
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))
}

View File

@ -29,7 +29,7 @@ func CloseLogFile() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
logFile.Close() logFile.Close()
} }
func Log(message string) { func Log(message string) {

View File

@ -0,0 +1,40 @@
package services
import (
"encoding/json"
"fmt"
"slices"
"strconv"
"strings"
"time"
"wazuh-notify/types"
)
func BuildMessage(params types.Params, target string, emphasis string) string {
if slices.Contains(strings.Split(params.General.FullAlert, ","), target) {
fullAlert, _ := json.MarshalIndent(params.WazuhMessage, "", " ")
fullAlertString := strings.ReplaceAll(string(fullAlert), `"`, "")
fullAlertString = strings.ReplaceAll(fullAlertString, "{", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "}", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "[", "")
fullAlertString = strings.ReplaceAll(fullAlertString, "]", "")
fullAlertString = strings.ReplaceAll(fullAlertString, " ,", "")
return "\n\n ```" +
fullAlertString +
"```\n\n"
} else {
return "\n\n" +
fmt.Sprintf("%sTimestamp:%s ", emphasis, emphasis) + time.Now().Format(time.DateTime) + "\n" +
fmt.Sprintf("%sAgent:%s ", emphasis, emphasis) + params.WazuhMessage.Parameters.Alert.Agent.Name + "\n" +
fmt.Sprintf("%sEvent id:%s ", emphasis, emphasis) + params.WazuhMessage.Parameters.Alert.Rule.ID + "\n" +
fmt.Sprintf("%sRule:%s ", emphasis, emphasis) + params.WazuhMessage.Parameters.Alert.Rule.Description + "\n" +
fmt.Sprintf("%sDescription:%s ", emphasis, emphasis) + params.WazuhMessage.Parameters.Alert.FullLog + "\n" +
fmt.Sprintf("%sThreat level:%s ", emphasis, emphasis) + strconv.Itoa(params.WazuhMessage.Parameters.Alert.Rule.Level) + "\n" +
fmt.Sprintf("%sTimes fired:%s ", emphasis, emphasis) + strconv.Itoa(params.WazuhMessage.Parameters.Alert.Rule.Firedtimes) +
"\n\n" +
fmt.Sprintf("%sPriority:%s ", emphasis, emphasis) + strconv.Itoa(params.Priority) + "\n"
}
}

View File

@ -0,0 +1,48 @@
package services
import (
"bufio"
"encoding/json"
"os"
"slices"
"strings"
"wazuh-notify/services/log"
"wazuh-notify/types"
)
func ParseWazuhInput(params types.Params) types.Params {
var wazuhData types.WazuhMessage
//Read stdin
reader := bufio.NewReader(os.Stdin)
//Decode stdin to wazuhData
json.NewDecoder(reader).Decode(&wazuhData)
//Parse tags
params.Tags += strings.Join(wazuhData.Parameters.Alert.Rule.Groups, ",")
params.WazuhMessage = wazuhData
//Map priority and color based on config
for i := range params.PriorityMap {
if slices.Contains(params.PriorityMap[i].ThreatMap, wazuhData.Parameters.Alert.Rule.Level) {
//Check notify threshold
if params.WazuhMessage.Parameters.Alert.Rule.Firedtimes%params.PriorityMap[i].NotifyThreshold != 0 {
log.Log("threshold not met")
log.CloseLogFile()
os.Exit(0)
}
//Set color based on config map
params.Color = params.PriorityMap[i].Color
//Check mention threshold
if params.WazuhMessage.Parameters.Alert.Rule.Firedtimes >= params.PriorityMap[i].MentionThreshold {
params.Mention = "@here"
}
params.Priority = 5 - i
}
}
log.Log("Wazuh data loaded")
//Filter messages based on rules defined in config
Filter(params)
return params
}

View File

@ -0,0 +1,42 @@
package discord
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
"wazuh-notify/services"
"wazuh-notify/types"
)
func SendDiscord(params types.Params) {
//Build message content
embedDescription := services.BuildMessage(params, "discord", params.MarkdownEmphasis.Discord) +
"**Tags:** " + params.Tags + "\n\n" +
params.General.Click
//Build message
message := DiscordMessage{
Username: params.General.Sender,
Content: params.Mention,
Embeds: []Embed{
{
Title: params.General.Sender,
Description: embedDescription,
Color: params.Color,
},
},
}
payload := new(bytes.Buffer)
//Parse message to json
err := json.NewEncoder(payload).Encode(message)
if err != nil {
return
}
//Send message to webhook
_, err = http.Post(os.Getenv("DISCORD_URL"), "application/json", payload)
if err != nil {
log.Fatalf("An Error Occured %v", err)
}
}

View File

@ -0,0 +1,14 @@
package discord
type DiscordMessage 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"`
}

View File

@ -0,0 +1,35 @@
package ntfy
import (
"net/http"
"os"
"strconv"
"strings"
"wazuh-notify/services"
"wazuh-notify/types"
)
func SendNtfy(params types.Params) {
//Create request and build message
req, _ := http.NewRequest(
"POST",
os.Getenv("NTFY_URL"),
strings.NewReader(" "+services.BuildMessage(params, "ntfy", params.MarkdownEmphasis.Ntfy)))
req.Header.Set("Content-Type", "text/markdown")
//Set headers if not empty
if params.General.Sender != "" {
req.Header.Add("Title", params.General.Sender)
}
if params.Tags != "" {
req.Header.Add("Tags", params.Tags)
}
if params.General.Click != "" {
req.Header.Add("Click", params.General.Click)
}
if params.Priority != 0 {
req.Header.Add("Priority", strconv.Itoa(params.Priority))
}
//Send request
http.DefaultClient.Do(req)
}

View File

@ -0,0 +1 @@
package ntfy

View File

@ -0,0 +1,32 @@
package slack
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
"wazuh-notify/services"
"wazuh-notify/types"
)
func SendSlack(params types.Params) {
//Build message
message := SlackMessage{
Text: services.BuildMessage(params, "slack", params.MarkdownEmphasis.Slack) +
"*Tags:* " + params.Tags + "\n\n" +
params.General.Click,
}
payload := new(bytes.Buffer)
//Parse message to json
err := json.NewEncoder(payload).Encode(message)
if err != nil {
return
}
//Send message to webhook
_, err = http.Post(os.Getenv("SLACK_URL"), "application/json", payload)
if err != nil {
log.Fatalf("An Error Occured %v", err)
}
}

View File

@ -0,0 +1,5 @@
package slack
type SlackMessage struct {
Text string `json:"text,omitempty"`
}

View File

@ -0,0 +1,33 @@
package types
type Params struct {
General General `toml:"general"`
Url string
Priority int
Tags string
Color int
Mention string
WazuhMessage WazuhMessage
PriorityMap []PriorityMap `toml:"priority_map"`
MarkdownEmphasis MarkdownEmphasis `toml:"markdown_emphasis"`
}
type General struct {
Targets string `toml:"targets"`
FullAlert string `toml:"full_alert"`
ExcludedRules string `toml:"excluded_rules"`
ExcludedAgents string `toml:"excluded_agents"`
Sender string `toml:"sender"`
Click string `toml:"click"`
}
type PriorityMap struct {
ThreatMap []int `toml:"threat_map"`
MentionThreshold int `toml:"mention_threshold"`
NotifyThreshold int `toml:"notify_threshold"`
Color int `toml:"color"`
}
type MarkdownEmphasis struct {
Slack string `toml:"slack"`
Ntfy string `toml:"ntfy"`
Discord string `toml:"discord"`
}

View File

@ -1,36 +0,0 @@
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"`
FullAlert 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"`
}

View File

@ -0,0 +1,84 @@
#############################################################################################################
# This is the TOML config file for wazuh-notify (active response) for both the Python and Go implementation #
#############################################################################################################
[general]
# Platforms in this string with comma seperated values are triggered.
targets = "slack, ntfy, discord"
# Platforms in this string will enable sending the full event information.
full_alert = ""
# 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"
excluded_agents = "99999"
# The next 2 settings are used to add information to the messages.
sender = "Wazuh (IDS)"
click = "https://documentation.wazuh.com/"
# 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/notify_threshold as integer and color as Hex integer
[[priority_map]]
threat_map = [15, 14, 13, 12]
mention_threshold = 1
notify_threshold = 1
color = 0xec3e40 # Red, SEVERE
[[priority_map]]
threat_map = [11, 10, 9]
mention_threshold = 1
notify_threshold = 1
color = 0xff9b2b # Orange, HIGH
[[priority_map]]
threat_map = [8, 7, 6]
mention_threshold = 5
notify_threshold = 5
color = 0xf5d800 # Yellow, ELEVATED
[[priority_map]]
threat_map = [5, 4]
mention_threshold = 20
notify_threshold = 5
color = 0x377fc7 # Blue, GUARDED
[[priority_map]]
threat_map = [3, 2, 1, 0]
mention_threshold = 20
notify_threshold = 5
color = 0x01a465 # Green, LOW
################ End of priority mapping ##################################
# Following parameter defines the markdown characters to emphasise the parameter names in the notification messages
[markdown_emphasis]
slack = "*"
ntfy = "**"
discord = "**"
##################################################################################
# From here on the settings are ONLY used by the Python version of wazuh-notify. #
##################################################################################
[python]
# The next settings are used for testing and troubleshooting.
# 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 = 2
# Enabling this parameter provides extended logging to the console.
extended_print = 2
# Below settings provide for a window that enable/disables events from firing the notifiers.
excluded_days = ""
# Enter as a tuple of string values. Be aware of your regional settings.
excluded_hours = ["23:59", "00:00"]

View File

@ -1,46 +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
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
...

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("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

@ -8,7 +8,7 @@ import time
from os.path import join, dirname from os.path import join, dirname
from sys import _getframe as frame from sys import _getframe as frame
import yaml import tomli
from dotenv import load_dotenv from dotenv import load_dotenv
@ -17,23 +17,20 @@ from dotenv import load_dotenv
# config_path = wazuh-notify-config.yaml # config_path = wazuh-notify-config.yaml
def set_environment() -> tuple: def set_environment() -> tuple:
set_wazuh_path = os.path.abspath(os.path.join(__file__, "../.."))
set_wazuh_path = os.path.abspath(os.path.join(__file__, "../../..")) # set_wazuh_path = os.path.abspath(os.path.join(__file__, "../../.."))
set_log_path = '{0}/logs/wazuh-notify.log'.format(set_wazuh_path) set_log_path = '{0}/logs/wazuh-notify.log'.format(set_wazuh_path)
set_config_path = '{0}/etc/wazuh-notify-config.yaml'.format(set_wazuh_path) set_config_path = '{0}/etc/wazuh-notify-config.toml'.format(set_wazuh_path)
return set_wazuh_path, set_log_path, set_config_path return set_wazuh_path, set_log_path, set_config_path
# 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,53 +40,40 @@ 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()
logger_wazuh_path = os.path.abspath(os.path.join(__file__, "../../..")) logger_wazuh_path = os.path.abspath(os.path.join(__file__, "../../.."))
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('extended_print') >= 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("extended_logging") >= 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,67 +82,55 @@ 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, 'r') as ntfier_config: config: dict = tomli.load(ntfier_config)
config: dict = yaml.safe_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('targets', 'discord, ntfy, slack') config['targets'] = config.get('general').get('targets', 'discord, slack, ntfy')
config['full_alert'] = config.get('full_alert', '') config['full_alert'] = config.get('general').get('full_alert', False)
config['excluded_rules'] = config.get('excluded_rules', '') config['excluded_rules'] = config.get('general').get('excluded_rules', '')
config['excluded_agents'] = config.get('excluded_agents', '') config['excluded_agents'] = config.get('general').get('excluded_agents', '')
config['priority_map'] = config.get('priority_map', []) config['priority_map'] = config.get('priority_map', [])
config['sender'] = config.get('sender', 'Wazuh (IDS)') config['sender'] = config.get('general').get('sender', 'Wazuh (IDS)')
config['click'] = config.get('click', 'https://wazuh.org') config['click'] = config.get('general').get('click', 'https://wazuh.com')
config['md_e'] = config.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('excluded_days', '') config['excluded_hours'] = config.get('python').get('excluded_hours', '')
config['excluded_hours'] = config.get('excluded_hours', '') config['test_mode'] = config.get('python').get('test_mode', False)
config['test_mode'] = config.get('test_mode', False) config['extended_logging'] = config.get('python').get('extended_logging', 0)
config['extended_logging'] = config.get('extended_logging', True) config['extended_print'] = config.get('python').get('extended_print', 0)
config['extended_print'] = config.get('extended_print', True)
return config return 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('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('excluded_days') ex_agents = config.get('general').get("excluded_agents")
ex_agents = config.get("excluded_agents") ex_rules = config.get('general').get("excluded_rules")
ex_rules = config.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('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('markdown_emphasis') md_map = config.get('markdown_emphasis', '')
md_e = md_map[caller] md_e = md_map[caller]
click: str = config.get('click') click: str = config.get('general').get('click', 'https://wazuh.com')
sender: str = config.get('sender')
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