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
33 changed files with 974 additions and 66 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

View File

@ -1,16 +0,0 @@
# Wazuh notify
*version 1.0*
## Introduction
Wazuh notifier enables the Wazuh manager to be notified when Wazuh selected events occur, using 3 messaging platforms:
[ntfy.sh](https://ntfy.sh), [Discord](https://discord.com) and [Slack](https://slack.com).
There are 2 implementations of Wazuh notify. One written in Golang, the other in Python. Both implementations have
similar functionality, but the Python version is slightly more configurable for testing purposes.
Wazuh notify is a stateless implementation and only notifies: triggered by specific rules, agents, or threat levels.
Wazuh notify is executed by configuring the **ossec.conf** and adding an **active response configuration**.
### 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 module wazuh-notify
go 1.22 go 1.25.4
require ( require (
github.com/BurntSushi/toml v1.4.0 github.com/BurntSushi/toml v1.4.0

View File

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

View File

@ -22,4 +22,11 @@ func Filter(params types.Params) {
os.Exit(0) 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 { type General struct {
Targets string `toml:"targets"` Targets string `toml:"targets"`
FullAlert string `toml:"full_alert"` FullAlert string `toml:"full_alert"`
ExcludedRules string `toml:"excluded_rules"` ExcludedRules string `toml:"excluded_rules"`
ExcludedAgents string `toml:"excluded_agents"` ExcludedAgents string `toml:"excluded_agents"`
Sender string `toml:"sender"` Sender string `toml:"sender"`
Click string `toml:"click"` Click string `toml:"click"`
ExcludedDescription []string `toml:"exclude_descriptions"`
} }
type PriorityMap struct { type PriorityMap struct {
ThreatMap []int `toml:"threat_map"` ThreatMap []int `toml:"threat_map"`

View File

@ -14,6 +14,12 @@ full_alert = ""
excluded_rules = "99999, 00000" excluded_rules = "99999, 00000"
excluded_agents = "99999" 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. # The next 2 settings are used to add information to the messages.
sender = "Wazuh (IDS)" sender = "Wazuh (IDS)"
click = "https://documentation.wazuh.com/" 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 import requests
from requests import Response from requests import Response
import github
from wazuh_notify_module import * from wazuh_notify_module import *