Compare commits

...

44 Commits

Author SHA1 Message Date
9cdb96036f Improved logging
All checks were successful
Go / build (push) Successful in 22s
2025-11-20 17:45:41 +01:00
5b763dc7b4 Improved logging and stdin buffering
All checks were successful
Go / build (push) Successful in 20s
2025-11-19 21:57:32 +01:00
bfc763c0c0 added logging
All checks were successful
Go / build (push) Successful in 19s
2025-11-17 21:37:02 +01:00
2117fcc783 updated config logic
All checks were successful
Go / build (push) Successful in 20s
2025-11-17 21:27:53 +01:00
5205c2fb22 set default-config.toml
All checks were successful
Go / build (push) Successful in 22s
2025-11-17 20:31:09 +01:00
cf155a951c Merge pull request 'refactor-go' (#2) from refactor-go into master
All checks were successful
Go / build (push) Successful in 20s
Go / release (push) Successful in 12s
Reviewed-on: #2
Reviewed-by: Rudi klein <rudi.klein@rudiklein.nl>
2025-11-17 20:03:32 +01:00
26e6b08371 updated pipeline
All checks were successful
Go / build (pull_request) Successful in 21s
Go / release (pull_request) Has been skipped
2025-11-15 23:12:16 +01:00
033b36d1d1 updated pipeline
All checks were successful
Go / build (pull_request) Successful in 26s
2025-11-15 23:09:24 +01:00
8f5663981a removed docs pipeline awaiting migration to common docs
Some checks failed
Go / build (pull_request) Failing after 44s
2025-11-15 23:03:22 +01:00
c307c8c7a4 updated pipeline
Some checks failed
build and deploy docs / build (pull_request) Failing after 4s
Go / build (pull_request) Failing after 54s
2025-11-15 23:00:58 +01:00
63e28bc5e9 Added wazuh-notifier-v2 2025-11-15 23:00:03 +01:00
38b6c997db fixed pipeline
All checks were successful
build and deploy docs / build (push) Successful in 16s
build and deploy docs / publish (push) Successful in 3s
2025-03-07 13:05:57 +01:00
0e5b20f0dd Update .github/workflows/deploy-docs.yml
Some checks failed
build and deploy docs / build (push) Successful in 15s
build and deploy docs / publish (push) Failing after 4s
2025-02-23 14:20:01 +01:00
ac1476b703 Update .github/workflows/deploy-docs.yml
Some checks failed
build and deploy docs / build (push) Successful in 15s
build and deploy docs / publish (push) Failing after 3s
2025-02-23 14:18:51 +01:00
ffc8bd66ee Update .github/workflows/deploy-docs.yml
All checks were successful
build and deploy docs / build (push) Successful in 16s
build and deploy docs / publish (push) Successful in 3s
2025-02-23 14:18:00 +01:00
d5b2bda535 Update .github/workflows/release.yml 2025-02-23 14:17:20 +01:00
2377469461 Update Writerside/topics/Wazuh-notifier.md
Some checks failed
Go / build (push) Failing after 14s
build and deploy docs / build (push) Successful in 1m3s
build and deploy docs / publish (push) Successful in 3s
2025-02-06 08:59:13 +01:00
e9d82ba5d9 Update .github/workflows/deploy-docs.yml
Some checks failed
Go / build (push) Failing after 13s
2025-02-06 08:58:52 +01:00
da325de501 Update Writerside/topics/Wazuh-notifier.md
Some checks failed
build and deploy docs / build (push) Waiting to run
build and deploy docs / publish (push) Blocked by required conditions
Go / build (push) Failing after 30s
2025-02-06 08:58:09 +01:00
6131edcdd5 Update .github/workflows/deploy-docs.yml
Some checks failed
Go / build (push) Failing after 40s
2025-02-06 08:55:13 +01:00
dklein
58852326ea added .toml to python
fixed python release version
2024-11-29 12:29:30 +01:00
dklein
e914886efd pipeline fix 2024-11-29 12:26:50 +01:00
dklein
c998b4a43f typo 2024-11-29 12:16:46 +01:00
dklein
6c68447ef4 Added versioning and python release.yml 2024-11-29 12:15:31 +01:00
dklein
ac5d2babbd added filter based on description 2024-11-29 11:48:51 +01:00
bb4d4cf76f
Update README.md 2024-06-10 17:00:09 +02:00
a330c855e2
Update README.md 2024-06-10 16:57:45 +02:00
DariusKlein
84d8b348d8
Update golang.yml 2024-06-10 15:50:54 +02:00
DariusKlein
4f9a49e1f3
Update golang.yml 2024-06-10 15:49:31 +02:00
DariusKlein
77b45dce9a
Update golang.yml 2024-06-10 15:48:31 +02:00
DariusKlein
83321c5910
Update golang.yml 2024-06-10 15:47:19 +02:00
DariusKlein
b5982d348d
Update golang.yml 2024-06-10 15:43:41 +02:00
DariusKlein
656a96ab0f
Update golang.yml 2024-06-10 15:40:27 +02:00
DariusKlein
1a3581153c
Update golang.yml 2024-06-10 15:29:29 +02:00
DariusKlein
db562ab4e8
Update golang.yml 2024-06-10 15:23:01 +02:00
DariusKlein
3534d09d3a
Update golang.yml 2024-06-10 15:19:20 +02:00
darius
9625a86095 test 2024-06-10 15:14:27 +02:00
DariusKlein
10fe9fd5de
Update golang.yml 2024-06-10 15:11:47 +02:00
DariusKlein
cfa778df1e
Update golang.yml 2024-06-10 15:11:17 +02:00
DariusKlein
25f739c434
Update golang.yml 2024-06-10 15:10:25 +02:00
DariusKlein
d2e1e4ea65
Update golang.yml 2024-06-10 15:09:10 +02:00
DariusKlein
ec49876a95
Update golang.yml 2024-06-10 15:07:47 +02:00
DariusKlein
3bb4653440
Create golang.yml 2024-06-10 15:06:42 +02:00
583a6f3f02 Setup, license and requirements.txt added 2024-06-07 18:02:26 +02:00
32 changed files with 974 additions and 369 deletions

View File

@ -1,41 +0,0 @@
name: build and deploy docs
on:
push:
branches: [ "master" ]
paths: ['Writerside/**']
pull_request:
branches: [ "master" ]
paths: ['Writerside/**']
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v3
- name: Docker login
run: docker login docker.dariusklein.nl -u Darius -p ${{ secrets.DOCKER_PASSWORD }}
- name: Build the Docker image
run: docker build . --file Writerside/Dockerfile --tag docker.dariusklein.nl/wazuh-notifier-docs
- name: Docker push
run: docker push docker.dariusklein.nl/wazuh-notifier-docs
publish:
needs: build
runs-on: self-hosted
steps:
- name: Docker stop
run: docker stop WazuhNotifier || true
- name: Docker login
run: docker login docker.dariusklein.nl -u Darius -p ${{ secrets.DOCKER_PASSWORD }}
- name: Docker pull
run: docker pull docker.dariusklein.nl/wazuh-notifier-docs
- name: Docker run
run: docker run --rm -dit -p 9091:80 --name WazuhNotifier docker.dariusklein.nl/wazuh-notifier-docs

43
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Go
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
- name: Build wazuh-notify-go
run: |
cd wazuh-notify-go
go build -o wazuh-notifier-go .
- name: Upload wazuh-notify-go artifact
uses: christopherhx/gitea-upload-artifact@v4
with:
name: wazuh-notifier-go-binary
path: wazuh-notify-go/wazuh-notifier-go
- name: Build wazuh-notify-go-v2
run: |
cd wazuh-notify-go-v2
go build -o wazuh-notifier-go-v2 .
- name: Upload wazuh-notify-go-v2 artifact
uses: christopherhx/gitea-upload-artifact@v4
with:
name: wazuh-notifier-go-v2-binary
path: wazuh-notify-go-v2/wazuh-notifier-go-v2

321
README.md
View File

@ -1,23 +1,6 @@
# Wazuh notify
*version 1.0*
## Table of Contents
- [Introduction](#introduction)
- [Installation](#installation)
- [Step 1: download](#step-1-download)
- [Step 2: copy files](#step-2-copy-files)
- [Python](#python_1)
- [Golang](#golang_1)
- [Step 3: copy the TOML file](#step-3-copy-the-toml-configuration-file)
- [Step 4: create .env file](#step-4-create-env-file)
- [Wazuh configuration](#wazuh-configuration)
- [Golang](#golang_2)
- [Python](#python_2)
- [Note](#note)
- [The TOML configuration file](#the-toml-configuration)
- [Setting up the platforms](#setting-up-the-platforms-receiving-the-notifications)
## Introduction
Wazuh notifier enables the Wazuh manager to be notified when Wazuh selected events occur, using 3 messaging platforms:
@ -30,306 +13,4 @@ Wazuh notify is a stateless implementation and only notifies: triggered by speci
Wazuh notify is executed by configuring the **ossec.conf** and adding an **active response configuration**.
## Installation
### Step 1: download
Download the files from https://github.com/kleinprojects/wazuh-notify to your server.
### Step 2: copy files
#### _Python_ {id="python_1"}
Copy the 2 Python scripts to the /var/ossec/active-response/bin/ folder
```
$ sudo cp <download folder>/wazuh-*.py /var/ossec/active-response/bin/
```
Set the correct ownership {id="set-the-correct-ownership_1"}
```
$ sudo chown root:wazuh /var/ossec/active-response/bin/wazuh-notify.py
$ sudo chown root:wazuh /var/ossec/active-response/bin/wazuh_notify_module.py
```
Set the correct permissions {id="set-the-correct-permissions_1"}
```
$ sudo chmod uog+rx /var/ossec/active-response/bin/wazuh-notify.py
$ sudo chmod uog+rx /var/ossec/active-response/bin/wazuh_notify_module.py
```
#### _Golang_ {id="golang_1"}
Copy the Go executable to the /var/ossec/active-response/bin/ folder
```
$ sudo cp <download folder>/wazuh-notify /var/ossec/active-response/bin/
```
Set the correct ownership {id="set-the-correct-ownership_2"}
```
$ sudo chown root:wazuh /var/ossec/active-response/bin/wazuh-notify
```
Set the correct permissions {id="set-the-correct-permissions_2"}
```
$ sudo chmod uog+rx /var/ossec/active-response/bin/wazuh-notify
```
### Step 3: copy the TOML configuration file
Copy the TOML file to /var/ossec/etc/
```
$ sudo cp <download folder>/wazuh-notify-config.toml /var/ossec/etc/
```
Set the correct ownership {id="set-the-correct-ownership_3"}
```
$ sudo chown root:wazuh /var/ossec/etc/wazuh-notify-config.toml
```
Set the correct permissions {id="set-the-correct-permissions_3"}
```
$ sudo chmod uog+r /var/ossec/etc/wazuh-notify-config.toml
```
### Step 4: create .env file
Create an .env file in /var/ossec/etc/
```
$ sudo touch /var/ossec/etc/.env
```
Set the correct ownership {id="set-the-correct-ownership_4"}
```
$ sudo chown root:wazuh /var/ossec/etc/wazuh-notify-config.toml
```
Set the correct permissions {id="set-the-correct-permissions_4"}
```
$ sudo chmod uog+r /var/ossec/etc/wazuh-notify-config.toml
```
## Wazuh configuration
#### _Golang_ {id="golang_2"}
Modify the /var/ossec/etc/ossec.conf configuration file and add the following:<br/>
*Command section*
```
<command>
<name>wazuh-notify-go</name>
<executable>wazuh-notify</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
```
*Active response section*
```
<active-response>
<command>wazuh-notify-go</command>
<location>server</location>
<level></level>
<rules_id></rules_id>
</active-response>
```
#### _Python_ {id="python_2"}
*Command section*
```
<command>
<name>wazuh-notify-py</name>
<executable>wazuh-notify.py</executable>
<timeout_allowed>yes</timeout_allowed>
</command>
```
*Active response section*
```
<active-response>
<command>wazuh-notify-py</command>
<location>server</location>
<level></level>
<rules_id></rules_id>
</active-response>
```
#### NOTE: <format color="OrangeRed">!</format>
The ```<name>``` in the ```<command>``` section needs to be the same as the ```<command>``` in
the ```<active-response>``` section.
The ```<command>``` section describes the program that is executed. The ```<active-response>``` section describes the
trigger that runs the ```<command>```.
Add the rules you want to be informed about between the ```<rules_id></rules_id>```, with the rules id's separated by
comma's.
Example: ```<rules_id>5402, 3461, 8777</rules_id>```.
Please refer to
the [Wazuh online documentation](https://documentation.wazuh.com/current/user-manual/capabilities/active-response/index.html)
for more information.
## The TOML configuration
This is the toml configuration file for wazuh-notify (for both the Python and Golang version).
The targets setting defines the platforms where notifications will be sent to.
Platforms in this comma-separated string will receive notifications, if and when they are set up.
Refer to [setting up the platforms](#setting-up-the-platforms-receiving-the-notifications).
```
targets: "slack, ntfy, discord"
```
Platforms in this comma-separated string will receive the full event information.
```
full_alert: ""
```
Exclude_rules and excluded_agents will disable notification for these particular events or agents that are enabled in
the ossec.conf active response definition.
These settings provide an easier way to disable event notifications from firing. No need to restart Wazuh-manager.
Enter rule numbers as a string with comma-separated values.
Enter numeric agent id's as a string with comma-separated values.
```
excluded_rules: "99999, 00000"
excluded_agents: "99999"
```
[The threat levels used in Wazuh](https://documentation.wazuh.com/current/user-manual/ruleset/rules-classification.html)
(0-15) are mapped to notification priority levels (1-5), and their respective colors (Discord only).
The Wazuh threat level scale runs from 0-15, where 15 is the most severe threat. It corresponds to the
[HSAS](https://en.wikipedia.org/wiki/Homeland_Security_Advisory_System) threat scale that runs from 5-1, whereby 1 is
the highest threat level. The configuration allows for customized mapping: in some use cases the mapping could be different.
The mention threshold defines when Discord users receive a DM, next to the common messages they receive in their channel.
Often these common channels are muted and DM's will draw more attention. 1 means that for every notification a DM will be sent.
A mention threshold of 5 means that for every 5th occurrence of this specific event, a DM will be sent also.
The notify threshold is somewhat similar to the mention threshold. A notify threshold of 1 will send each notification,
a notify threshold of 4 will only send each 4th notification triggered by a specific event. This will reduce high amounts
of notifications for the same event. The fired_times value in the message will show the actual number of the times this
specific event was generated.
Enter a threat_map as a list of integers,
color as a hex RGB color values,
mention/notify_threshold as integers.
```
[[priority_map]] # Priority 1 on the HSAS scale
threat_map = [15, 14, 13, 12] # Wazuh threat levels -> priority 2
color = 0xec3e40 # Red, SEVERE on the HSAS scale
mention_threshold = 1
notify_threshold = 1
[[priority_map]] # Priority 2 on the HSAS scale
threat_map = [11, 10, 9] # Wazuh threat levels -> priority 2
color = 0xff9b2b # Orange, HIGH on the HSAS scale
mention_threshold = 1
notify_threshold = 1
[[priority_map]] # Priority 3 on the HSAS scale
threat_map = [8, 7, 6] # Wazuh threat levels -> priority 3
color = 0xf5d800 # Yellow, ELEVATED on the HSAS scale
mention_threshold = 5
notify_threshold = 5
[[priority_map]] # Priority 4 on the HSAS scale
threat_map = [5, 4] # Wazuh threat levels -> priority 4
color = 0x377fc7 # Blue, GUARDED on the HSAS scale
mention_threshold = 20
notify_threshold = 5
[[priority_map]] # Priority 5 on the HSAS scale
threat_map = [3, 2, 1, 0] # Wazuh threat levels -> priority 5
color = 0x01a465 # Green, LOW on the HSAS scale
mention_threshold = 20
notify_threshold = 1
```
The next settings are used to add information to the messages.
```Sender``` translate to the ``` username ``` field in Discord and Slack and to the ```title``` field in ntfy.sh.
The ```click``` parameter adds an arbitrary URL to the message.
```
sender: "Wazuh (IDS)"
click: "https://documentation.wazuh.com/"
```
### From here on the settings are ONLY used by the Python version of wazuh-notify.
Below settings provide for a window that enable/disables events from firing the notifiers.
Enter ```excluded_days``` as a string with comma separated values. Be aware of your regional settings.
```
excluded_days: ""
```
Enter ```excluded_hours``` as a tuple of string values.
```
excluded_hours: [ "23:59", "00:00" ]
```
The following parameters define the markdown characters used to emphasise the parameter names in the notification
messages (Markdown style). This is a dictionary notation.
```
markdown_emphasis:
slack: "*"
ntfy: "**"
discord: "**"
```
The next settings are used for testing purposes.
```Test mode``` will add an example event (```wazuh-notify-test-event.json```) instead of the message received through Wazuh.
This enables customization for testing of a particular event.
```
test_mode: False
```
Setting the ```extended_logging``` and ```extended_print``` parameters provides more logging to the wazuh-notifier log
and console. The possible values are:
0-> limited logging
1-> basic logging
2-> verbose logging
```
extended_logging: 2
extended_print: 0
```
### Setting up the platforms receiving the notifications
Each of the 3 platforms make use of webhooks or similar API's. In order to have the right information in the ```.env```
file, please refer to the platform's documentation.
[Slack](https://api.slack.com/) API documentation
[ntfy.sh](https://docs.ntfy.sh/subscribe/api/) API documentation
[ntfy.sh](https://docs.ntfy.sh/examples/) examples
[Discord](https://discord.com/developers/docs/intro) developers documentation
### Please refer to https://docs.notifier.kleinsense.nl/wazuh-notifier.html for the full documentation.

23
license.MD Normal file
View File

@ -0,0 +1,23 @@
MIT License
Copyright (c) [2024] [Darius Klein]
MIT license for Wazuh-notify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,78 @@
package common
import "strings"
type ActiveResponse 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 []string `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"`
}
func (a *ActiveResponse) Tags() string {
return strings.Join(a.Parameters.Alert.Rule.Groups, ",")
}

View File

@ -0,0 +1,73 @@
package common
import (
"encoding/json"
"fmt"
"os"
"slices"
"strconv"
"strings"
"time"
"wazuh-notify/config"
logger "wazuh-notify/log"
)
func ReadFile(path string) (*os.File, error) {
fmt.Printf("Reading from file: %s\n", path)
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open file %s: %w", path, err)
}
return file, nil
}
func BuildMessage(ar ActiveResponse, target string, emphasis string, priority int) string {
if slices.Contains(strings.Split(config.File.General.FullAlert, ","), target) {
fullAlert, _ := json.MarshalIndent(ar, "", " ")
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) + ar.Parameters.Alert.Agent.Name + "\n" +
fmt.Sprintf("%sEvent id:%s ", emphasis, emphasis) + ar.Parameters.Alert.Rule.ID + "\n" +
fmt.Sprintf("%sRule:%s ", emphasis, emphasis) + ar.Parameters.Alert.Rule.Description + "\n" +
fmt.Sprintf("%sDescription:%s ", emphasis, emphasis) + ar.Parameters.Alert.FullLog + "\n" +
fmt.Sprintf("%sThreat level:%s ", emphasis, emphasis) + strconv.Itoa(ar.Parameters.Alert.Rule.Level) + "\n" +
fmt.Sprintf("%sTimes fired:%s ", emphasis, emphasis) + strconv.Itoa(ar.Parameters.Alert.Rule.FiredTimes) +
"\n\n" +
fmt.Sprintf("%sPriority:%s ", emphasis, emphasis) + strconv.Itoa(priority) + "\n"
}
}
func Ignored(ar ActiveResponse) bool {
for _, rule := range strings.Split(config.File.General.ExcludedRules, ",") {
if rule == ar.Parameters.Alert.Rule.ID {
logger.Log("rule excluded")
return true
}
}
for _, agent := range strings.Split(config.File.General.ExcludedAgents, ",") {
if agent == ar.Parameters.Alert.Agent.ID {
logger.Log("agent excluded")
return true
}
}
for _, description := range config.File.General.ExcludeDescriptions {
if description != "" && strings.Contains(ar.Parameters.Alert.FullLog, description) {
logger.Log("excluded based on description")
return true
}
}
return false
}

View File

@ -0,0 +1,110 @@
package config
import (
_ "embed"
"fmt"
"os"
"path"
"wazuh-notify/log"
"github.com/BurntSushi/toml"
)
//go:embed default-config.toml
var DefaultConfigFile []byte
var File Config
// WARNING: this code is ai generated
func Read() error {
const SystemConfigPath = "/etc/wazuh-notify/wazuh-notify-config.toml"
execPath, _ := os.Executable()
LocalConfigPath := path.Join(path.Dir(execPath), "wazuh-notify-config.toml")
err := toml.Unmarshal(DefaultConfigFile, &File)
if err != nil {
log.Log(fmt.Sprintf("CRITICAL: Failed to parse embedded default config: %v", err))
return err
}
var userTomlFile []byte
var readErr error
var configPath string
userTomlFile, readErr = os.ReadFile(SystemConfigPath)
if readErr == nil {
configPath = SystemConfigPath
} else {
userTomlFile, readErr = os.ReadFile(LocalConfigPath)
if readErr == nil {
configPath = LocalConfigPath
}
}
if readErr != nil {
log.Log("No user config file found. Attempting to create default on disk.")
errMkdir := os.MkdirAll(path.Dir(SystemConfigPath), os.ModePerm)
errWrite := os.WriteFile(SystemConfigPath, DefaultConfigFile, 0600)
if errMkdir != nil || errWrite != nil {
log.Log(fmt.Sprintf("Warning: Could not write default config to %s (%v).", SystemConfigPath, errWrite))
log.Log("Using embedded default configuration only.")
} else {
log.Log(fmt.Sprintf("Successfully created default config at %s.", SystemConfigPath))
}
log.Log("TOML configuration loaded successfully from Embedded Default")
return nil
}
overrideErr := toml.Unmarshal(userTomlFile, &File)
if overrideErr != nil {
log.Log(fmt.Sprintf("Error parsing user configuration from %s: %v", configPath, overrideErr))
return overrideErr
}
log.Log(fmt.Sprintf("TOML configuration loaded successfully. Defaults merged with %s", configPath))
return nil
}
// Config holds the entire configuration structure defined in the TOML file.
type Config struct {
General General `toml:"general"`
PriorityMaps []PriorityMap `toml:"priority_map"`
Discord Discord `toml:"discord"`
Ntfy Ntfy `toml:"ntfy"`
Slack Slack `toml:"slack"`
}
// General maps the values within the [general] TOML table.
type General struct {
Targets string `toml:"targets"`
FullAlert string `toml:"full_alert"`
ExcludedRules string `toml:"excluded_rules"`
ExcludedAgents string `toml:"excluded_agents"`
ExcludeDescriptions []string `toml:"exclude_descriptions"`
Sender string `toml:"sender"`
Click string `toml:"click"`
}
// PriorityMap maps the values within a single [[priority_map]] TOML table entry.
type PriorityMap struct {
ThreatMap []int `toml:"threat_map"`
MentionThreshold int `toml:"mention_threshold"`
NotifyThreshold int `toml:"notify_threshold"`
Color int `toml:"color"`
}
type Discord struct {
Webhook string `toml:"webhook"`
}
// Ntfy maps the values within the [ntfy] TOML table.
type Ntfy struct {
Webhook string `toml:"webhook"`
}
// Slack maps the values within the [slack] TOML table.
type Slack struct {
Webhook string `toml:"webhook"`
}

View File

@ -0,0 +1,67 @@
#############################################################################################################
# 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"
# Exclude specific rules by string contained in description
# These settings provide an easier way to disable events from firing the notifiers.
exclude_descriptions = [
""
]
# The next 2 settings are used to add information to the messages.
sender = "Wazuh (IDS)"
click = "https://documentation.wazuh.com/"
[discord]
webhook = "https://discord.com/api/webhooks/XXX"
[ntfy]
webhook = "https://ntfy.sh/XXX"
[slack]
webhook = "https://hooks.slack.com/services/XXX"
# 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 = 1
color = 0x01a465 # Green, LOW

View File

@ -0,0 +1,14 @@
package constants
const (
Discord = "discord"
Ntfy = "ntfy"
Slack = "slack"
Targets = "targets"
Click = "click"
DiscordMention = "@here"
Sender = "sender"
Source = "source"
EmphasisDouble = "**"
EmphasisSingle = "*"
)

View File

@ -0,0 +1,67 @@
#############################################################################################################
# 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"
# Exclude specific rules by string contained in description
# These settings provide an easier way to disable events from firing the notifiers.
exclude_descriptions = [
""
]
# The next 2 settings are used to add information to the messages.
sender = "Wazuh (IDS)"
click = "https://documentation.wazuh.com/"
[discord]
webhook = "https://discord.com/api/webhooks/XXX"
[ntfy]
webhook = "https://ntfy.sh/XXX"
[slack]
webhook = "https://hooks.slack.com/services/XXX"
# 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 = 1
color = 0x01a465 # Green, LOW

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,40 @@
package discord
import (
"bytes"
"encoding/json"
"log"
"net/http"
"wazuh-notify/common"
"wazuh-notify/config"
"wazuh-notify/constants"
)
func Send(ar common.ActiveResponse, color int, mention string, priority int) {
body := common.BuildMessage(ar, constants.Discord, constants.EmphasisDouble, priority)
message := DiscordMessage{
Username: config.File.General.Sender,
Content: mention,
Embeds: []Embed{
{
Title: config.File.General.Sender,
Description: body,
Color: 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(config.File.Discord.Webhook, "application/json", payload)
if err != nil {
log.Fatalf("An Error Occured %v", err)
}
}

View File

@ -0,0 +1,52 @@
{
"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"
}
}

View File

@ -0,0 +1,9 @@
module wazuh-notify
go 1.25.4
require (
github.com/BurntSushi/toml v1.4.0
github.com/joho/godotenv v1.5.1
github.com/urfave/cli/v3 v3.3.8
)

14
wazuh-notify-go-v2/go.sum Normal file
View File

@ -0,0 +1,14 @@
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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,46 @@
package log
import (
"log/slog"
"os"
"path"
"time"
)
var logFile *os.File
func OpenLogFile(BasePath string) {
logFile, _ = os.OpenFile(path.Join(BasePath, "active-responses.log"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0777)
_, err := logFile.WriteString(
"\n#######################################\n## START ##" +
"\n" + time.Now().String() +
"\n#######################################\n",
)
if err != nil {
panic(err)
}
if logFile == nil {
panic("logFile is nil")
}
}
func CloseLogFile() {
if logFile != nil {
_, err := logFile.WriteString(
"\n\n#######################################\n## CLOSE ##" +
"\n" + time.Now().String() +
"\n#######################################\n",
)
if err != nil {
panic(err)
}
logFile.Close()
}
}
func Log(message string) {
slog.Info(message)
if _, err := logFile.WriteString("\n" + message + ": " + time.Now().String()); err != nil {
panic(err)
}
}

View File

@ -0,0 +1,55 @@
package main
import (
"context"
"fmt"
"log"
"net/mail"
"os"
"wazuh-notify/config"
logger "wazuh-notify/log"
"github.com/urfave/cli/v3"
)
func main() {
app := &cli.Command{
Name: "KleinCommand",
Usage: "CLI tool for internal use",
UsageText: "kleinCommand [category] [command] [arguments...]",
Version: "v0.1.0",
HideVersion: true,
Authors: []any{
mail.Address{
Name: "Darius",
Address: "darius.klein@dariusklein.nl",
},
},
DefaultCommand: "help",
Commands: []*cli.Command{
Notify(),
},
Before: func(context context.Context, c *cli.Command) (context.Context, error) {
const WazuhLogDir = "/var/ossec/logs/active-responses"
if err := os.MkdirAll(WazuhLogDir, 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating log directory %s: %v\n", WazuhLogDir, err)
}
logger.OpenLogFile(WazuhLogDir)
config.Read()
return context, nil
},
After: func(context context.Context, c *cli.Command) error {
logger.Log("Starting cleanup")
logger.CloseLogFile()
return nil
},
}
if err := app.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,153 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os"
"slices"
"strings"
"wazuh-notify/common"
"wazuh-notify/config"
"wazuh-notify/constants"
"wazuh-notify/discord"
logger "wazuh-notify/log"
"wazuh-notify/ntfy"
"wazuh-notify/slack"
"github.com/urfave/cli/v3"
)
func Notify() *cli.Command {
return &cli.Command{
Name: "notify",
Usage: "notify services about security event",
Action: action,
Flags: flags(),
ArgsUsage: "args usage",
}
}
func flags() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{
Name: constants.Source,
Aliases: []string{"s"},
Usage: "The file path to read from. Defaults to **standard in** if not set.",
Value: "",
DefaultText: "stdin",
},
&cli.StringFlag{
Name: constants.Click,
Usage: "is a link (URL) that can be followed by tapping/clicking inside the message. (Overrides TOML config)",
Value: "",
DefaultText: "Defined in config",
},
&cli.StringFlag{
Name: constants.Sender,
Usage: "is the sender of the message, either an app name or a person. (Overrides TOML config)",
Value: "Wazuh (IDS) Golang",
},
&cli.StringSliceFlag{
Name: constants.Targets,
Aliases: []string{"t"},
Usage: "is a comma-separated list of targets (slack, ntfy, discord) to send notifications to. (Overrides TOML config)",
Value: []string{},
DefaultText: "Defined in config",
},
}
}
func action(context context.Context, c *cli.Command) error {
logger.Log("Notify starting")
sourcePath := c.String(constants.Source)
var input io.Reader
if sourcePath == "" {
logger.Log("Reading from standard input (stdin)...")
input = bufio.NewReader(os.Stdin)
} else {
logger.Log("Reading from file (stdin)...")
file, err := common.ReadFile(sourcePath)
if err != nil {
return err
}
defer file.Close()
input = file
}
ar, err := readInput(input)
if err != nil {
return err
}
var color int
var priority int
var mention string = ""
for i := range config.File.PriorityMaps {
if slices.Contains(config.File.PriorityMaps[i].ThreatMap, ar.Parameters.Alert.Rule.Level) {
//Check notify threshold
if ar.Parameters.Alert.Rule.FiredTimes%config.File.PriorityMaps[i].NotifyThreshold != 0 {
logger.Log("threshold not met")
return nil
}
//Set color based on config map
color = config.File.PriorityMaps[i].Color
//Check mention threshold
if ar.Parameters.Alert.Rule.FiredTimes >= config.File.PriorityMaps[i].MentionThreshold {
mention = constants.DiscordMention
}
break
}
}
targets := c.StringSlice(constants.Targets)
if len(targets) == 0 {
targets = strings.Split(config.File.General.Targets, ",")
}
if c.String(constants.Click) != "" {
config.File.General.Click = c.String(constants.Click)
}
// Ignored out messages based on config
if common.Ignored(ar) {
return nil
}
logger.Log("Finished processing message. sending to enabled targets.")
if slices.Contains(targets, constants.Discord) {
logger.Log("Discord targets found.")
discord.Send(ar, color, mention, priority)
}
if slices.Contains(targets, constants.Ntfy) {
logger.Log("Ntfy targets found.")
ntfy.Send(ar, priority)
}
if slices.Contains(targets, constants.Slack) {
logger.Log("Slack targets found.")
slack.Send(ar, priority)
}
logger.Log("Finished sending messages")
return nil
}
func readInput(input io.Reader) (common.ActiveResponse, error) {
logger.Log("Parsing input")
var ar common.ActiveResponse
decoder := json.NewDecoder(input)
err := decoder.Decode(&ar)
if err != nil {
logger.Log(fmt.Sprintf("Error decoding JSON from stdin: %v\n", err))
return ar, err
}
return ar, nil
}

View File

@ -0,0 +1,35 @@
package ntfy
import (
"net/http"
"strconv"
"strings"
"wazuh-notify/common"
"wazuh-notify/config"
"wazuh-notify/constants"
)
func Send(params common.ActiveResponse, priority int) {
//Create request and build message
req, _ := http.NewRequest(
"POST",
config.File.Ntfy.Webhook,
strings.NewReader("&nbsp;"+common.BuildMessage(params, constants.Ntfy, constants.EmphasisDouble, priority)))
req.Header.Set("Content-Type", "text/markdown")
//Set headers if not empty
if config.File.General.Sender != "" {
req.Header.Add("Title", config.File.General.Sender)
}
if params.Tags() != "" {
req.Header.Add("Tags", params.Tags())
}
if config.File.General.Click != "" {
req.Header.Add("Click", config.File.General.Click)
}
if priority != 0 {
req.Header.Add("Priority", strconv.Itoa(priority))
}
//Send request
http.DefaultClient.Do(req)
}

View File

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

View File

@ -0,0 +1,32 @@
package slack
import (
"bytes"
"encoding/json"
"log"
"net/http"
"wazuh-notify/common"
"wazuh-notify/config"
"wazuh-notify/constants"
)
func Send(params common.ActiveResponse, priority int) {
//Build message
message := SlackMessage{
Text: common.BuildMessage(params, constants.Slack, constants.EmphasisSingle, priority) +
"*Tags:* " + params.Tags() + "\n\n" +
config.File.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(config.File.Slack.Webhook, "application/json", payload)
if err != nil {
log.Fatalf("An Error Occured %v", err)
}
}

1
wazuh-notify-go/VERSION Normal file
View File

@ -0,0 +1 @@
0.1.1-Release

View File

@ -1,6 +1,6 @@
module wazuh-notify
go 1.22
go 1.25.4
require (
github.com/BurntSushi/toml v1.4.0

View File

@ -9,6 +9,7 @@ import (
"wazuh-notify/targets/slack"
)
// test
func main() {
//Read config file and .env
configParams := services.ReadConfig()

View File

@ -22,4 +22,11 @@ func Filter(params types.Params) {
os.Exit(0)
}
}
for _, description := range params.General.ExcludedDescription {
if strings.Contains(params.WazuhMessage.Parameters.Alert.FullLog, description) {
log.Log("excluded based on description")
log.CloseLogFile()
os.Exit(0)
}
}
}

View File

@ -13,12 +13,13 @@ type Params struct {
}
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"`
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"`
ExcludedDescription []string `toml:"exclude_descriptions"`
}
type PriorityMap struct {
ThreatMap []int `toml:"threat_map"`

View File

@ -14,6 +14,12 @@ full_alert = ""
excluded_rules = "99999, 00000"
excluded_agents = "99999"
# Exclude specific rules by string contained in description
# These settings provide an easier way to disable events from firing the notifiers.
exclude_descriptions = [
""
]
# The next 2 settings are used to add information to the messages.
sender = "Wazuh (IDS)"
click = "https://documentation.wazuh.com/"

View File

@ -0,0 +1 @@
0.1.0-Release

View File

@ -0,0 +1,4 @@
setuptools~=59.6.0
requests~=2.25.1
tomli~=2.0.1
python-dotenv~=1.0.1

View File

@ -0,0 +1,14 @@
from setuptools import setup
import github
setup(
name='wazuh-notify',
version='1.0',
packages=[''],
url='https://github.com/KleinProjects/wazuh-notify.git',
license='MIT',
author='Darius Klein',
author_email='darius.klein@dariusklein.nl',
description='Wazuh notify'
)

View File

@ -10,7 +10,7 @@
import requests
from requests import Response
import github
from wazuh_notify_module import *