Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions cmd/systray/main_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"unsafe"

"github.com/Microsoft/go-winio"
"github.com/mitchellh/mapstructure"
"github.com/rabbitstack/fibratus/pkg/alertsender"
Expand All @@ -31,11 +37,6 @@ import (
"github.com/rabbitstack/fibratus/pkg/util/signals"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
"io"
"net"
"os"
"path/filepath"
"unsafe"
)

const systrayPipe = `\\.\pipe\fibratus-systray`
Expand Down Expand Up @@ -65,6 +66,7 @@ func (m Msg) decode(output any) error {
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
alertsender.StringToSeverityDecodeHook(),
),
}
decoder, err := mapstructure.NewDecoder(decoderConfig)
Expand Down
3 changes: 3 additions & 0 deletions configs/fibratus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ alertsenders:
# in the log message.
verbose: true

# Specifies the eventlog record format for the alert. Can be pretty|json
format: pretty

# =============================== API ==================================================

# Settings that influence the behaviour of the HTTP server that exposes a number of endpoints such as
Expand Down
204 changes: 203 additions & 1 deletion pkg/alertsender/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,19 @@ package alertsender

import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strings"
"time"

"github.com/mitchellh/mapstructure"

"github.com/rabbitstack/fibratus/pkg/event"
"github.com/rabbitstack/fibratus/pkg/event/params"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html"
"strings"
)

// Severity is the type alias for alert's severity level.
Expand Down Expand Up @@ -58,6 +65,25 @@ func (s Severity) String() string {
}
}

// StringToSeverityDecodeHook converts severity string to integer.
func StringToSeverityDecodeHook() mapstructure.DecodeHookFuncType {
return func(
from reflect.Type,
to reflect.Type,
data any,
) (any, error) {
if from.Kind() != reflect.String {
return data, nil
}

if to != reflect.TypeOf(Severity(0)) {
return data, nil
}

return ParseSeverityFromString(data.(string)), nil
}
}

// ParseSeverityFromString parses the severity from the string representation.
func ParseSeverityFromString(sever string) Severity {
switch sever {
Expand Down Expand Up @@ -140,6 +166,182 @@ func (a *Alert) MDToHTML() error {
return nil
}

// MarshalJSON encodes the alert to JSON format.
func (a Alert) MarshalJSON() ([]byte, error) {
var msg = &struct {
ID string `json:"id"`
Title string `json:"title"`
Severity string `json:"severity"`
Text string `json:"text,omitempty"`
Description string `json:"description"`
Labels map[string]string `json:"labels,omitempty"`
Events []struct {
Name string `json:"name"`
Category string `json:"category"`
Timestamp time.Time `json:"timestamp"`
Params map[string]any `json:"params"`
Callstack []string `json:"callstack,omitempty"`
Proc *struct {
PID uint32 `json:"pid"`
TID uint32 `json:"tid"`
PPID uint32 `json:"ppid"`
Name string `json:"name"`
Exe string `json:"exe"`
Cmdline string `json:"cmdline,omitempty"`
Pname string `json:"parent_name,omitempty"`
Pcmdline string `json:"parent_cmdline,omitempty"`
Cwd string `json:"cwd,omitempty"`
SID string `json:"sid"`
Username string `json:"username"`
Domain string `json:"domain"`
SessionID uint32 `json:"session_id"`
IntegrityLevel string `json:"integrity_level"`
IsWOW64 bool `json:"is_wow64"`
IsPackaged bool `json:"is_packaged"`
IsProtected bool `json:"is_protected"`
Ancestors []string `json:"ancestors"`
} `json:"proc,omitempty"`
} `json:"events"`
}{
ID: a.ID,
Title: a.Title,
Severity: a.Severity.String(),
Text: a.Text,
Description: a.Description,
Labels: a.Labels,
}

events := make([]struct {
Name string `json:"name"`
Category string `json:"category"`
Timestamp time.Time `json:"timestamp"`
Params map[string]any `json:"params"`
Callstack []string `json:"callstack,omitempty"`
Proc *struct {
PID uint32 `json:"pid"`
TID uint32 `json:"tid"`
PPID uint32 `json:"ppid"`
Name string `json:"name"`
Exe string `json:"exe"`
Cmdline string `json:"cmdline,omitempty"`
Pname string `json:"parent_name,omitempty"`
Pcmdline string `json:"parent_cmdline,omitempty"`
Cwd string `json:"cwd,omitempty"`
SID string `json:"sid"`
Username string `json:"username"`
Domain string `json:"domain"`
SessionID uint32 `json:"session_id"`
IntegrityLevel string `json:"integrity_level"`
IsWOW64 bool `json:"is_wow64"`
IsPackaged bool `json:"is_packaged"`
IsProtected bool `json:"is_protected"`
Ancestors []string `json:"ancestors"`
} `json:"proc,omitempty"`
}, 0, len(a.Events))

for _, e := range a.Events {
var evt = struct {
Name string `json:"name"`
Category string `json:"category"`
Timestamp time.Time `json:"timestamp"`
Params map[string]any `json:"params"`
Callstack []string `json:"callstack,omitempty"`
Proc *struct {
PID uint32 `json:"pid"`
TID uint32 `json:"tid"`
PPID uint32 `json:"ppid"`
Name string `json:"name"`
Exe string `json:"exe"`
Cmdline string `json:"cmdline,omitempty"`
Pname string `json:"parent_name,omitempty"`
Pcmdline string `json:"parent_cmdline,omitempty"`
Cwd string `json:"cwd,omitempty"`
SID string `json:"sid"`
Username string `json:"username"`
Domain string `json:"domain"`
SessionID uint32 `json:"session_id"`
IntegrityLevel string `json:"integrity_level"`
IsWOW64 bool `json:"is_wow64"`
IsPackaged bool `json:"is_packaged"`
IsProtected bool `json:"is_protected"`
Ancestors []string `json:"ancestors"`
} `json:"proc,omitempty"`
}{
Name: e.Name,
Category: string(e.Category),
Timestamp: e.Timestamp,
Params: make(map[string]any),
Callstack: make([]string, 0, len(e.Callstack)),
}

// populate event parameters
for _, param := range e.Params {
if param.Type == params.Bool || param.Type == params.PID ||
param.Type == params.TID || param.Type == params.Port || param.IsNumber() {
evt.Params[param.Name] = param.Value
} else {
evt.Params[param.Name] = param.String()
}
}

// populate callstack
for i := range e.Callstack {
frame := e.Callstack[len(e.Callstack)-i-1]
evt.Callstack = append(evt.Callstack, fmt.Sprintf("%s %s!%s", frame.Addr, frame.Module, frame.Symbol))
}

ps := e.PS
if ps != nil {
evt.Proc = &struct {
PID uint32 `json:"pid"`
TID uint32 `json:"tid"`
PPID uint32 `json:"ppid"`
Name string `json:"name"`
Exe string `json:"exe"`
Cmdline string `json:"cmdline,omitempty"`
Pname string `json:"parent_name,omitempty"`
Pcmdline string `json:"parent_cmdline,omitempty"`
Cwd string `json:"cwd,omitempty"`
SID string `json:"sid"`
Username string `json:"username"`
Domain string `json:"domain"`
SessionID uint32 `json:"session_id"`
IntegrityLevel string `json:"integrity_level"`
IsWOW64 bool `json:"is_wow64"`
IsPackaged bool `json:"is_packaged"`
IsProtected bool `json:"is_protected"`
Ancestors []string `json:"ancestors"`
}{
PID: ps.PID,
TID: e.Tid,
PPID: ps.Ppid,
Name: ps.Name,
Exe: ps.Exe,
Cmdline: ps.Cmdline,
Cwd: ps.Cwd,
SID: ps.SID,
Username: ps.Username,
Domain: ps.Domain,
SessionID: ps.SessionID,
IntegrityLevel: ps.TokenIntegrityLevel,
IsWOW64: ps.IsWOW64,
IsPackaged: ps.IsPackaged,
IsProtected: ps.IsProtected,
Ancestors: ps.Ancestors(),
}
if ps.Parent != nil {
evt.Proc.Pname = ps.Parent.Name
evt.Proc.Pcmdline = ps.Parent.Cmdline
}
}

events = append(events, evt)
}
msg.Events = events

return json.Marshal(msg)
}

// NewAlert builds a new alert.
func NewAlert(title, text string, tags []string, severity Severity) Alert {
return Alert{Title: title, Text: text, Tags: tags, Severity: severity}
Expand Down
34 changes: 33 additions & 1 deletion pkg/alertsender/alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
package alertsender

import (
"encoding/json"
"testing"

"github.com/rabbitstack/fibratus/pkg/event"
"github.com/rabbitstack/fibratus/pkg/event/params"
pstypes "github.com/rabbitstack/fibratus/pkg/ps/types"
"github.com/stretchr/testify/require"
"testing"
)

func TestAlertString(t *testing.T) {
Expand Down Expand Up @@ -94,3 +96,33 @@ func TestAlertString(t *testing.T) {
})
}
}

func TestAlertJSON(t *testing.T) {
alert := NewAlertWithEvents("Credential discovery via VaultCmd.exe", "Suspicious vault enumeration via VaultCmd tool", nil, Normal, []*event.Event{{
Type: event.CreateProcess,
Category: event.Process,
Params: event.Params{
params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"},
params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}},
Name: "CreateProcess",
PID: 1023,
PS: &pstypes.PS{
Name: "svchost.exe",
Cmdline: "C:\\Windows\\System32\\svchost.exe",
Ppid: 345,
Username: "SYSTEM",
Domain: "NT AUTHORITY",
SID: "S-1-5-18",
TokenIntegrityLevel: "HIGH",
},
}})

alert.ID = "64af2e2e-2309-4079-9c0f-985f1dd930f5"

b, err := json.MarshalIndent(alert, "", " ")
require.NoError(t, err)

expectedJSON := "{\n \"id\": \"64af2e2e-2309-4079-9c0f-985f1dd930f5\",\n \"title\": \"Credential discovery via VaultCmd.exe\",\n \"severity\": \"low\",\n \"text\": \"Suspicious vault enumeration via VaultCmd tool\",\n \"description\": \"\",\n \"events\": [\n {\n \"name\": \"CreateProcess\",\n \"category\": \"process\",\n \"timestamp\": \"0001-01-01T00:00:00Z\",\n \"params\": {\n \"cmdline\": \"C:\\\\Windows\\\\system32\\\\svchost-fake.exe -k RPCSS\",\n \"name\": \"svchost-fake.exe\"\n },\n \"proc\": {\n \"pid\": 0,\n \"tid\": 0,\n \"ppid\": 345,\n \"name\": \"svchost.exe\",\n \"exe\": \"\",\n \"cmdline\": \"C:\\\\Windows\\\\System32\\\\svchost.exe\",\n \"sid\": \"S-1-5-18\",\n \"username\": \"SYSTEM\",\n \"domain\": \"NT AUTHORITY\",\n \"session_id\": 0,\n \"integrity_level\": \"HIGH\",\n \"is_wow64\": false,\n \"is_packaged\": false,\n \"is_protected\": false,\n \"ancestors\": []\n }\n }\n ]\n}"

require.Equal(t, expectedJSON, string(b))
}
8 changes: 8 additions & 0 deletions pkg/alertsender/eventlog/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import (
const (
enabled = "alertsenders.eventlog.enabled"
verbose = "alertsenders.eventlog.verbose"
format = "alertsenders.eventlog.format"

prettyFormat = "pretty"
jsonFormat = "json"
)

// Config defines the configuration for the eventlog sender.
Expand All @@ -35,10 +39,14 @@ type Config struct {
// event context, including parameters and the process
// state are included in the log message.
Verbose bool `mapstructure:"verbose"`
// Format specifies the eventlog record format for the alert.
// Can be pretty or json.
Format string `mapstructure:"format"`
}

// AddFlags registers persistent flags.
func AddFlags(flags *pflag.FlagSet) {
flags.Bool(enabled, true, "Indicates if eventlog alert sender is enabled")
flags.Bool(verbose, true, "Enables/disables the verbose mode. In verbose mode, the full event context, including all parameters and the process information are included in the log message")
flags.String(format, "pretty", "Specifies the eventlog record format for the alert. Can be pretty|json")
}
20 changes: 17 additions & 3 deletions pkg/alertsender/eventlog/eventlog.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
package eventlog

import (
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"strings"

"github.com/rabbitstack/fibratus/pkg/alertsender"
evlog "github.com/rabbitstack/fibratus/pkg/util/eventlog"
"golang.org/x/sys/windows"
"hash/crc32"
"strings"
)

const minIDChars = 12
Expand Down Expand Up @@ -83,7 +85,19 @@ func (s *eventlog) Send(alert alertsender.Alert) error {
code = uint16(h & 0xFFFF)
}

msg := alert.String(s.config.Verbose)
// build the eventlog event
var msg string
switch s.config.Format {
case prettyFormat:
msg = alert.String(s.config.Verbose)
case jsonFormat:
b, err := json.MarshalIndent(alert, "", " ")
if err != nil {
return err
}
msg = string(b)
}

// trim null characters to avoid
// UTF16PtrFromString complaints
msg = strings.ReplaceAll(msg, "\x00", "")
Expand Down
Loading
Loading