Skip to content

Commit bd8608f

Browse files
committed
new(alert): Marshal alert to JSON
1 parent 803dfd9 commit bd8608f

2 files changed

Lines changed: 213 additions & 1 deletion

File tree

pkg/alertsender/alert.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ package alertsender
2020

2121
import (
2222
"bytes"
23+
"encoding/json"
2324
"fmt"
2425
"reflect"
2526
"strings"
27+
"time"
2628

2729
"github.com/mitchellh/mapstructure"
30+
2831
"github.com/rabbitstack/fibratus/pkg/event"
32+
"github.com/rabbitstack/fibratus/pkg/event/params"
2933
"github.com/yuin/goldmark"
3034
"github.com/yuin/goldmark/extension"
3135
"github.com/yuin/goldmark/renderer/html"
@@ -163,6 +167,182 @@ func (a *Alert) MDToHTML() error {
163167
return nil
164168
}
165169

170+
// MarshalJSON encodes the alert to JSON format.
171+
func (a Alert) MarshalJSON() ([]byte, error) {
172+
var msg = &struct {
173+
ID string `json:"id"`
174+
Title string `json:"title"`
175+
Severity string `json:"severity"`
176+
Text string `json:"text,omitempty"`
177+
Description string `json:"description"`
178+
Labels map[string]string `json:"labels,omitempty"`
179+
Events []struct {
180+
Name string `json:"name"`
181+
Category string `json:"category"`
182+
Timestamp time.Time `json:"timestamp"`
183+
Params map[string]any `json:"params"`
184+
Callstack []string `json:"callstack,omitempty"`
185+
Proc *struct {
186+
PID uint32 `json:"pid"`
187+
TID uint32 `json:"tid"`
188+
PPID uint32 `json:"ppid"`
189+
Name string `json:"name"`
190+
Exe string `json:"exe"`
191+
Cmdline string `json:"cmdline,omitempty"`
192+
Pname string `json:"parent_name,omitempty"`
193+
Pcmdline string `json:"parent_cmdline,omitempty"`
194+
Cwd string `json:"cwd,omitempty"`
195+
SID string `json:"sid"`
196+
Username string `json:"username"`
197+
Domain string `json:"domain"`
198+
SessionID uint32 `json:"session_id"`
199+
IntegrityLevel string `json:"integrity_level"`
200+
IsWOW64 bool `json:"is_wow64"`
201+
IsPackaged bool `json:"is_packaged"`
202+
IsProtected bool `json:"is_protected"`
203+
Ancestors []string `json:"ancestors"`
204+
} `json:"proc,omitempty"`
205+
} `json:"events"`
206+
}{
207+
ID: a.ID,
208+
Title: a.Title,
209+
Severity: a.Severity.String(),
210+
Text: a.Text,
211+
Description: a.Description,
212+
Labels: a.Labels,
213+
}
214+
215+
events := make([]struct {
216+
Name string `json:"name"`
217+
Category string `json:"category"`
218+
Timestamp time.Time `json:"timestamp"`
219+
Params map[string]any `json:"params"`
220+
Callstack []string `json:"callstack,omitempty"`
221+
Proc *struct {
222+
PID uint32 `json:"pid"`
223+
TID uint32 `json:"tid"`
224+
PPID uint32 `json:"ppid"`
225+
Name string `json:"name"`
226+
Exe string `json:"exe"`
227+
Cmdline string `json:"cmdline,omitempty"`
228+
Pname string `json:"parent_name,omitempty"`
229+
Pcmdline string `json:"parent_cmdline,omitempty"`
230+
Cwd string `json:"cwd,omitempty"`
231+
SID string `json:"sid"`
232+
Username string `json:"username"`
233+
Domain string `json:"domain"`
234+
SessionID uint32 `json:"session_id"`
235+
IntegrityLevel string `json:"integrity_level"`
236+
IsWOW64 bool `json:"is_wow64"`
237+
IsPackaged bool `json:"is_packaged"`
238+
IsProtected bool `json:"is_protected"`
239+
Ancestors []string `json:"ancestors"`
240+
} `json:"proc,omitempty"`
241+
}, 0, len(a.Events))
242+
243+
for _, e := range a.Events {
244+
var evt = struct {
245+
Name string `json:"name"`
246+
Category string `json:"category"`
247+
Timestamp time.Time `json:"timestamp"`
248+
Params map[string]any `json:"params"`
249+
Callstack []string `json:"callstack,omitempty"`
250+
Proc *struct {
251+
PID uint32 `json:"pid"`
252+
TID uint32 `json:"tid"`
253+
PPID uint32 `json:"ppid"`
254+
Name string `json:"name"`
255+
Exe string `json:"exe"`
256+
Cmdline string `json:"cmdline,omitempty"`
257+
Pname string `json:"parent_name,omitempty"`
258+
Pcmdline string `json:"parent_cmdline,omitempty"`
259+
Cwd string `json:"cwd,omitempty"`
260+
SID string `json:"sid"`
261+
Username string `json:"username"`
262+
Domain string `json:"domain"`
263+
SessionID uint32 `json:"session_id"`
264+
IntegrityLevel string `json:"integrity_level"`
265+
IsWOW64 bool `json:"is_wow64"`
266+
IsPackaged bool `json:"is_packaged"`
267+
IsProtected bool `json:"is_protected"`
268+
Ancestors []string `json:"ancestors"`
269+
} `json:"proc,omitempty"`
270+
}{
271+
Name: e.Name,
272+
Category: string(e.Category),
273+
Timestamp: e.Timestamp,
274+
Params: make(map[string]any),
275+
Callstack: make([]string, 0, len(e.Callstack)),
276+
}
277+
278+
// populate event parameters
279+
for _, param := range e.Params {
280+
if param.Type == params.Bool || param.Type == params.PID ||
281+
param.Type == params.TID || param.Type == params.Port || param.IsNumber() {
282+
evt.Params[param.Name] = param.Value
283+
} else {
284+
evt.Params[param.Name] = param.String()
285+
}
286+
}
287+
288+
// populate callstack
289+
for i := range e.Callstack {
290+
frame := e.Callstack[len(e.Callstack)-i-1]
291+
evt.Callstack = append(evt.Callstack, fmt.Sprintf("%s %s!%s", frame.Addr, frame.Module, frame.Symbol))
292+
}
293+
294+
ps := e.PS
295+
if ps != nil {
296+
evt.Proc = &struct {
297+
PID uint32 `json:"pid"`
298+
TID uint32 `json:"tid"`
299+
PPID uint32 `json:"ppid"`
300+
Name string `json:"name"`
301+
Exe string `json:"exe"`
302+
Cmdline string `json:"cmdline,omitempty"`
303+
Pname string `json:"parent_name,omitempty"`
304+
Pcmdline string `json:"parent_cmdline,omitempty"`
305+
Cwd string `json:"cwd,omitempty"`
306+
SID string `json:"sid"`
307+
Username string `json:"username"`
308+
Domain string `json:"domain"`
309+
SessionID uint32 `json:"session_id"`
310+
IntegrityLevel string `json:"integrity_level"`
311+
IsWOW64 bool `json:"is_wow64"`
312+
IsPackaged bool `json:"is_packaged"`
313+
IsProtected bool `json:"is_protected"`
314+
Ancestors []string `json:"ancestors"`
315+
}{
316+
PID: ps.PID,
317+
TID: e.Tid,
318+
PPID: ps.Ppid,
319+
Name: ps.Name,
320+
Exe: ps.Exe,
321+
Cmdline: ps.Cmdline,
322+
Cwd: ps.Cwd,
323+
SID: ps.SID,
324+
Username: ps.Username,
325+
Domain: ps.Domain,
326+
SessionID: ps.SessionID,
327+
IntegrityLevel: ps.TokenIntegrityLevel,
328+
IsWOW64: ps.IsWOW64,
329+
IsPackaged: ps.IsPackaged,
330+
IsProtected: ps.IsProtected,
331+
Ancestors: ps.Ancestors(),
332+
}
333+
if ps.Parent != nil {
334+
evt.Proc.Pname = ps.Parent.Name
335+
evt.Proc.Pcmdline = ps.Parent.Cmdline
336+
}
337+
}
338+
339+
events = append(events, evt)
340+
}
341+
msg.Events = events
342+
343+
return json.Marshal(msg)
344+
}
345+
166346
// NewAlert builds a new alert.
167347
func NewAlert(title, text string, tags []string, severity Severity) Alert {
168348
return Alert{Title: title, Text: text, Tags: tags, Severity: severity}

pkg/alertsender/alert_test.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
package alertsender
2020

2121
import (
22+
"encoding/json"
23+
"testing"
24+
2225
"github.com/rabbitstack/fibratus/pkg/event"
2326
"github.com/rabbitstack/fibratus/pkg/event/params"
2427
pstypes "github.com/rabbitstack/fibratus/pkg/ps/types"
2528
"github.com/stretchr/testify/require"
26-
"testing"
2729
)
2830

2931
func TestAlertString(t *testing.T) {
@@ -94,3 +96,33 @@ func TestAlertString(t *testing.T) {
9496
})
9597
}
9698
}
99+
100+
func TestAlertJSON(t *testing.T) {
101+
alert := NewAlertWithEvents("Credential discovery via VaultCmd.exe", "Suspicious vault enumeration via VaultCmd tool", nil, Normal, []*event.Event{{
102+
Type: event.CreateProcess,
103+
Category: event.Process,
104+
Params: event.Params{
105+
params.Cmdline: {Name: params.Cmdline, Type: params.UnicodeString, Value: "C:\\Windows\\system32\\svchost-fake.exe -k RPCSS"},
106+
params.ProcessName: {Name: params.ProcessName, Type: params.AnsiString, Value: "svchost-fake.exe"}},
107+
Name: "CreateProcess",
108+
PID: 1023,
109+
PS: &pstypes.PS{
110+
Name: "svchost.exe",
111+
Cmdline: "C:\\Windows\\System32\\svchost.exe",
112+
Ppid: 345,
113+
Username: "SYSTEM",
114+
Domain: "NT AUTHORITY",
115+
SID: "S-1-5-18",
116+
TokenIntegrityLevel: "HIGH",
117+
},
118+
}})
119+
120+
alert.ID = "64af2e2e-2309-4079-9c0f-985f1dd930f5"
121+
122+
b, err := json.MarshalIndent(alert, "", " ")
123+
require.NoError(t, err)
124+
125+
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}"
126+
127+
require.Equal(t, expectedJSON, string(b))
128+
}

0 commit comments

Comments
 (0)