diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml deleted file mode 100644 index 514070f..0000000 --- a/.github/workflows/deploy-docs.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: build and deploy docs - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Docker login - run: docker login 192.168.1.200:3000 -p ${{secrets.docker_password}} -u ${{secrets.docker_username}} - - name: Build the Docker image - run: docker build . --file Writerside/Dockerfile --tag 192.168.1.200:3000/klein-projects/wazuh-notifier-docs - - name: Docker push - run: docker push 192.168.1.200:3000/klein-projects/wazuh-notifier-docs - - - publish: - - needs: build - - runs-on: ubuntu-latest - - steps: - - name: Docker stop - run: docker stop servers-notifier-docs || true - - name: Docker rm - run: docker rm servers-notifier-docs || true - - name: Docker login - run: docker login 192.168.1.200:3000 -p ${{secrets.docker_password}} -u ${{secrets.docker_username}} - - name: Docker pull - run: docker pull 192.168.1.200:3000/klein-projects/wazuh-notifier-docs - - name: Docker run - run: docker run -dit -p 9091:80 --name servers-notifier-docs 192.168.1.200:3000/klein-projects/wazuh-notifier-docs \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d35d67..7b7a6bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,60 +1,66 @@ -# This workflow will build a golang project -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go +name: Go -#DISABLED - -#name: Go - -#on: -# push: -# branches: [ "master" ] -# pull_request: -# branches: [ "master" ] +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] jobs: + build: + runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23' + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25' - - name: Build - run: | - cd wazuh-notify-go - go build -v . + - name: Build wazuh-notify-go + run: | + cd wazuh-notify-go + go build -o wazuh-notifier-go . - - name: Set go variables - run: | - cd wazuh-notify-go && GO_VER=$(cat VERSION) - echo "GO_VERSION=$GO_VER" >> $GITHUB_ENV - - - name: Release Go - uses: softprops/action-gh-release@v2 - with: - token: ${{ secrets.RELEASE_TOKEN }} - tag_name: Golang-v${{ env.GO_VERSION }} - files: | - wazuh-notify-go/wazuh-notify - wazuh-notify-go/wazuh-notify-config.toml - licence.MD + - 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: Set python variables - run: | - cd wazuh-notify-python && PY_VER=$(cat VERSION) - echo "PY_VERSION=$PY_VER" >> $GITHUB_ENV + - name: Build wazuh-notify-go-v2 + run: | + cd wazuh-notify-go-v2 + go build -o wazuh-notifier-go-v2 . - - name: zip Python - run: zip -r wazuh-notify-python.zip wazuh-notify-python wazuh-notify-go/wazuh-notify-config.toml + - 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 - - name: Release python - uses: softprops/action-gh-release@v2 - with: - token: ${{ secrets.RELEASE_TOKEN }} - tag_name: Python-v${{ env.PY_VERSION }} - files: | - wazuh-notify-python.zip - licence.MD + release: + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Download all build artifacts + uses: actions/download-artifact@v3 + with: + path: artifacts + + - name: Release wazuh-notify-go artifact (Gitea Upload) + uses: christopherhx/gitea-upload-artifact@v4 + with: + name: wazuh-notifier-go-binary + path: artifacts/wazuh-notifier-go-binary-internal/wazuh-notifier-go + + - name: Release wazuh-notify-go-v2 artifact (Gitea Upload) + uses: christopherhx/gitea-upload-artifact@v4 + with: + name: wazuh-notifier-go-v2-binary + path: artifacts/wazuh-notifier-go-v2-binary-internal/wazuh-notifier-go-v2 diff --git a/wazuh-notify-go-v2/common/activeResponse.go b/wazuh-notify-go-v2/common/activeResponse.go new file mode 100644 index 0000000..e2138f0 --- /dev/null +++ b/wazuh-notify-go-v2/common/activeResponse.go @@ -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, ",") +} diff --git a/wazuh-notify-go-v2/common/common.go b/wazuh-notify-go-v2/common/common.go new file mode 100644 index 0000000..bec9787 --- /dev/null +++ b/wazuh-notify-go-v2/common/common.go @@ -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 +} diff --git a/wazuh-notify-go-v2/config/config.go b/wazuh-notify-go-v2/config/config.go new file mode 100644 index 0000000..126d048 --- /dev/null +++ b/wazuh-notify-go-v2/config/config.go @@ -0,0 +1,95 @@ +package config + +import ( + "errors" + "fmt" + "os" + "path" + "wazuh-notify/log" + + "github.com/BurntSushi/toml" +) + +var File Config + +func Read() error { + + const SystemConfigPath = "/etc/wazuh-notify/wazuh-notify-config.toml" + var LocalConfigPath string + + execPath, _ := os.Executable() + LocalConfigPath = path.Join(path.Dir(execPath), "wazuh-notify-config.toml") + + var tomlFile []byte + var err error + + tomlFile, err = os.ReadFile(SystemConfigPath) + if err == nil { + log.Log(fmt.Sprintf("TOML loaded from system path: %s", SystemConfigPath)) + } + + if errors.Is(err, os.ErrNotExist) { + log.Log("TOML not found in system path, attempting local fallback.") + + tomlFile, err = os.ReadFile(LocalConfigPath) + if err == nil { + log.Log(fmt.Sprintf("TOML loaded from local path: %s", LocalConfigPath)) + } + } + + if err != nil { + log.Log(fmt.Sprintf("FATAL: TOML config failed to load from both paths. Last error: %v", err)) + return err + } + + err = toml.Unmarshal(tomlFile, &File) + if err != nil { + log.Log(err.Error()) + return err + } else { + log.Log("yaml loaded") + } + 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"` +} diff --git a/wazuh-notify-go-v2/constants/constants.go b/wazuh-notify-go-v2/constants/constants.go new file mode 100644 index 0000000..312b861 --- /dev/null +++ b/wazuh-notify-go-v2/constants/constants.go @@ -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 = "*" +) diff --git a/wazuh-notify-go-v2/default-config.toml b/wazuh-notify-go-v2/default-config.toml new file mode 100644 index 0000000..8e610ac --- /dev/null +++ b/wazuh-notify-go-v2/default-config.toml @@ -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 \ No newline at end of file diff --git a/wazuh-notify-go-v2/discord/message.go b/wazuh-notify-go-v2/discord/message.go new file mode 100644 index 0000000..dbaa586 --- /dev/null +++ b/wazuh-notify-go-v2/discord/message.go @@ -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"` +} diff --git a/wazuh-notify-go-v2/discord/send.go b/wazuh-notify-go-v2/discord/send.go new file mode 100644 index 0000000..100d8df --- /dev/null +++ b/wazuh-notify-go-v2/discord/send.go @@ -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) + } +} diff --git a/wazuh-notify-go-v2/example.json b/wazuh-notify-go-v2/example.json new file mode 100644 index 0000000..5e272ba --- /dev/null +++ b/wazuh-notify-go-v2/example.json @@ -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" + } +} \ No newline at end of file diff --git a/wazuh-notify-go-v2/go.mod b/wazuh-notify-go-v2/go.mod new file mode 100644 index 0000000..5a2d405 --- /dev/null +++ b/wazuh-notify-go-v2/go.mod @@ -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 +) diff --git a/wazuh-notify-go-v2/go.sum b/wazuh-notify-go-v2/go.sum new file mode 100644 index 0000000..131ee49 --- /dev/null +++ b/wazuh-notify-go-v2/go.sum @@ -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= diff --git a/wazuh-notify-go-v2/log/log.go b/wazuh-notify-go-v2/log/log.go new file mode 100644 index 0000000..962b0f1 --- /dev/null +++ b/wazuh-notify-go-v2/log/log.go @@ -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) + } +} diff --git a/wazuh-notify-go-v2/main.go b/wazuh-notify-go-v2/main.go new file mode 100644 index 0000000..db3c453 --- /dev/null +++ b/wazuh-notify-go-v2/main.go @@ -0,0 +1,54 @@ +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.CloseLogFile() + return nil + }, + } + + if err := app.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/wazuh-notify-go-v2/notify.go b/wazuh-notify-go-v2/notify.go new file mode 100644 index 0000000..5a8ce8d --- /dev/null +++ b/wazuh-notify-go-v2/notify.go @@ -0,0 +1,142 @@ +package main + +import ( + "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 { + sourcePath := c.String(constants.Source) + var input io.Reader + + if sourcePath == "" { + fmt.Println("Reading from standard input (stdin)...") + input = os.Stdin + } else { + 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 + } + + if slices.Contains(targets, constants.Discord) { + discord.Send(ar, color, mention, priority) + } + if slices.Contains(targets, constants.Ntfy) { + ntfy.Send(ar, priority) + } + if slices.Contains(targets, constants.Slack) { + slack.Send(ar, priority) + } + return nil +} + +func readInput(input io.Reader) (common.ActiveResponse, error) { + var ar common.ActiveResponse + + decoder := json.NewDecoder(input) + + err := decoder.Decode(&ar) + if err != nil { + fmt.Fprintf(os.Stderr, "Error decoding JSON from stdin: %v\n", err) + return ar, err + } + + return ar, nil +} diff --git a/wazuh-notify-go-v2/ntfy/send.go b/wazuh-notify-go-v2/ntfy/send.go new file mode 100644 index 0000000..9e1c7c1 --- /dev/null +++ b/wazuh-notify-go-v2/ntfy/send.go @@ -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(" "+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) +} diff --git a/wazuh-notify-go-v2/slack/message.go b/wazuh-notify-go-v2/slack/message.go new file mode 100644 index 0000000..cca8e79 --- /dev/null +++ b/wazuh-notify-go-v2/slack/message.go @@ -0,0 +1,5 @@ +package slack + +type SlackMessage struct { + Text string `json:"text,omitempty"` +} diff --git a/wazuh-notify-go-v2/slack/send.go b/wazuh-notify-go-v2/slack/send.go new file mode 100644 index 0000000..f6facc1 --- /dev/null +++ b/wazuh-notify-go-v2/slack/send.go @@ -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) + } +} diff --git a/wazuh-notify-go/go.mod b/wazuh-notify-go/go.mod index 6744fd6..90cccec 100644 --- a/wazuh-notify-go/go.mod +++ b/wazuh-notify-go/go.mod @@ -1,6 +1,6 @@ module wazuh-notify -go 1.23 +go 1.25.4 require ( github.com/BurntSushi/toml v1.4.0