Skip to content

Commit 3aa6999

Browse files
authored
feat(alertsender): Systray alert sender (#237)
1 parent 0bf0761 commit 3aa6999

23 files changed

Lines changed: 826 additions & 13 deletions

File tree

configs/fibratus.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ aggregator:
1616

1717
# Alert senders deal with emitting alerts via different channels.
1818
alertsenders:
19+
# Systray sender sends alerts as notifications to the taskbar status area.
20+
systray:
21+
# Enables/disables systray alert sender
22+
enabled: true
23+
24+
# Indicates if the associated sound is played when the balloon notification is shown
25+
sound: true
26+
27+
# Instructs not to display the balloon notification if the current user is in quiet time.
28+
# During this time, most notifications should not be sent or shown. This lets a user become
29+
# accustomed to a new computer system without those distractions. Quiet time also occurs for
30+
# each user after an operating system upgrade or clean installation.
31+
quiet-mode: false
32+
1933
# Mail sender transports the alerts via SMTP protocol.
2034
mail:
2135
# Enables/disables mail alert sender
@@ -27,7 +41,7 @@ alertsenders:
2741
# Represents the port of the SMTP server
2842
#port: 587
2943

30-
# Specifies the user name when authenticating to the SMTP server
44+
# Specifies the username when authenticating to the SMTP server
3145
#user:
3246

3347
# Specifies the password when authenticating to the SMTP server

internal/bootstrap/bootstrap.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ func (f *App) Shutdown() error {
423423
if err := api.CloseServer(); err != nil {
424424
errs = append(errs, err)
425425
}
426+
if err := alertsender.ShutdownAll(); err != nil {
427+
errs = append(errs, err)
428+
}
426429
return multierror.Wrap(errs...)
427430
}
428431

pkg/aggregator/aggregator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
// initialize alert senders
4141
_ "github.com/rabbitstack/fibratus/pkg/alertsender/mail"
4242
_ "github.com/rabbitstack/fibratus/pkg/alertsender/slack"
43+
_ "github.com/rabbitstack/fibratus/pkg/alertsender/systray"
4344

4445
// initialize transformers
4546
_ "github.com/rabbitstack/fibratus/pkg/aggregator/transformers/remove"

pkg/alertsender/mail/mail.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ func (s mail) Send(alert alertsender.Alert) error {
5252
}
5353

5454
func (s mail) Type() alertsender.Type { return alertsender.Mail }
55+
func (s mail) Shutdown() error { return nil }
56+
func (s mail) SupportsMarkdown() bool { return true }
5557

5658
func (s mail) composeMessage(from string, to []string, alert alertsender.Alert) *gomail.Message {
5759
msg := gomail.NewMessage()

pkg/alertsender/sender.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@
1818

1919
package alertsender
2020

21-
import "fmt"
21+
import (
22+
"fmt"
23+
"github.com/rabbitstack/fibratus/pkg/util/multierror"
24+
)
2225

2326
// ErrInvalidConfig signals an invalid sender config
2427
var ErrInvalidConfig = func(name Type) error { return fmt.Errorf("invalid config for %q sender", name) }
@@ -39,6 +42,8 @@ const (
3942
Slack
4043
// Noop is a noop alert sender. Useful for testing.
4144
Noop
45+
// Systray designates the systray notification alert sender
46+
Systray
4247
// None is the type for unknown alert sender
4348
None
4449
)
@@ -52,6 +57,8 @@ func (s Type) String() string {
5257
return "slack"
5358
case Noop:
5459
return "noop"
60+
case Systray:
61+
return "systray"
5562
default:
5663
return "none"
5764
}
@@ -63,6 +70,12 @@ type Sender interface {
6370
Send(Alert) error
6471
// Type returns the type that identifies a particular sender.
6572
Type() Type
73+
// Shutdown performs cleanup tasks possibly disposing any resources
74+
// allocated by the sender.
75+
Shutdown() error
76+
// SupportsMarkdown indicates if the sender supports Markdown
77+
// rendering in alert text string.
78+
SupportsMarkdown() bool
6679
}
6780

6881
// ToType converts the string representation of the alert sender to its corresponding type.
@@ -74,6 +87,8 @@ func ToType(s string) Type {
7487
return Slack
7588
case "noop":
7689
return Noop
90+
case "systray":
91+
return Systray
7792
default:
7893
return None
7994
}
@@ -101,6 +116,18 @@ func FindAll() []Sender {
101116
return senders
102117
}
103118

119+
// ShutdownAll shutdowns all registered senders.
120+
func ShutdownAll() error {
121+
errs := make([]error, 0)
122+
for _, s := range alertsenders {
123+
err := s.Shutdown()
124+
if err != nil {
125+
errs = append(errs, err)
126+
}
127+
}
128+
return multierror.Wrap(errs...)
129+
}
130+
104131
// Load loads an alert sender from the registry.
105132
func Load(config Config) (Sender, error) {
106133
typ := config.Type

pkg/alertsender/slack/slack.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,5 @@ func (s slack) Send(alert alertsender.Alert) error {
128128
}
129129

130130
func (s slack) Type() alertsender.Type { return alertsender.Slack }
131+
func (s slack) Shutdown() error { return nil }
132+
func (s slack) SupportsMarkdown() bool { return true }
28.3 KB
Binary file not shown.

pkg/alertsender/systray/config.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2021-2022 by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package systray
20+
21+
import "github.com/spf13/pflag"
22+
23+
const (
24+
enabled = "alertsenders.systray.enabled"
25+
sound = "alertsenders.systray.sound"
26+
quietMode = "alertsenders.systray.quiet-mode"
27+
)
28+
29+
// Config contains the configuration for the systray alert sender.
30+
type Config struct {
31+
// Enabled indicates whether systray alert sender is enabled.
32+
Enabled bool `mapstructure:"enabled"`
33+
// Sound indicates if the associated sound is played
34+
// when the balloon notification is shown.
35+
Sound bool `mapstructure:"sound"`
36+
// QuietMode instructs not to display the balloon notification
37+
// if the current user is in quiet time.
38+
QuietMode bool `mapstructure:"quiet"`
39+
}
40+
41+
// AddFlags registers persistent flags.
42+
func AddFlags(flags *pflag.FlagSet) {
43+
flags.Bool(enabled, true, "Determines whether systray alert sender is enabled")
44+
flags.Bool(sound, true, "Indicates if the associated sound is played when the balloon notification is shown")
45+
flags.Bool(quietMode, false, "Instructs not to display the balloon notification if the current user is in quiet time")
46+
}

pkg/alertsender/systray/systray.go

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2021-2022 by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package systray
20+
21+
import (
22+
"errors"
23+
"fmt"
24+
"github.com/rabbitstack/fibratus/pkg/alertsender"
25+
"github.com/rabbitstack/fibratus/pkg/sys"
26+
"golang.org/x/sys/windows"
27+
"os"
28+
"path/filepath"
29+
"unsafe"
30+
)
31+
32+
var (
33+
// ErrSystrayIconRegisterClass is raised when the systray window class fails o register
34+
ErrSystrayIconRegisterClass = errors.New("unable to register systray icon window class")
35+
// ErrSystrayIconWindow is raised when the systray icon window can't be created
36+
ErrSystrayIconWindow = errors.New("unable to create systray icon window")
37+
38+
className = windows.StringToUTF16Ptr("fibratus")
39+
)
40+
41+
// systray interops with the status area
42+
// to show balloon notifications with the
43+
// desired title and text. Both, regular
44+
// and balloon icons are also rendered when
45+
// displaying the notification message.
46+
type systray struct {
47+
wnd sys.Hwnd // systray icon window handle
48+
systrayIcon *sys.SystrayIcon
49+
config Config
50+
}
51+
52+
func init() {
53+
alertsender.Register(alertsender.Systray, makeSender)
54+
}
55+
56+
// makeSender constructs a new instance of the systray alert sender.
57+
func makeSender(config alertsender.Config) (alertsender.Sender, error) {
58+
c, ok := config.Sender.(Config)
59+
if !ok {
60+
return nil, alertsender.ErrInvalidConfig(alertsender.Systray)
61+
}
62+
63+
if !c.Enabled {
64+
return &systray{}, nil
65+
}
66+
67+
var mod windows.Handle
68+
err := windows.GetModuleHandleEx(0, nil, &mod)
69+
if err != nil {
70+
return nil, err
71+
}
72+
// register notification icon window class
73+
var wc sys.WndClassEx
74+
wc.Size = uint32(unsafe.Sizeof(wc))
75+
wc.Instance = mod
76+
wc.WndProc = windows.NewCallback(wndProc)
77+
wc.ClassName = className
78+
err = sys.RegisterClass(&wc)
79+
if err != nil {
80+
return nil, errors.Join(ErrSystrayIconRegisterClass, err)
81+
}
82+
// create the notification icon window
83+
hwnd, err := sys.CreateWindowEx(
84+
0,
85+
className,
86+
className,
87+
sys.WindowStyleOverlapped,
88+
sys.CwUseDefault,
89+
sys.CwUseDefault,
90+
100,
91+
100,
92+
0,
93+
0,
94+
mod,
95+
0,
96+
)
97+
if err != nil {
98+
return nil, errors.Join(ErrSystrayIconWindow, err)
99+
}
100+
101+
systrayIcon, err := sys.NewSystrayIcon(hwnd)
102+
if err != nil {
103+
return nil, err
104+
}
105+
// find the icon in the same directory where the binary is loaded
106+
exe, err := os.Executable()
107+
if err != nil {
108+
return nil, err
109+
}
110+
ico, err := sys.LoadImage(
111+
mod,
112+
windows.StringToUTF16Ptr(filepath.Join(filepath.Dir(exe), "fibratus.ico")),
113+
1, // load icon
114+
0,
115+
0,
116+
sys.LoadResourceDefaultSize|sys.LoadResourceFromFile,
117+
)
118+
if err != nil {
119+
// load stock informational system icon
120+
var icon sys.ShStockIcon
121+
icon.Size = uint32(unsafe.Sizeof(icon))
122+
err := sys.SHGetStockIconInfo(79, 0x000000100, &icon)
123+
if err != nil {
124+
return nil, fmt.Errorf("unable to load systray icon resource: %v", err)
125+
}
126+
ico = windows.Handle(icon.Icon)
127+
}
128+
129+
// set systray icon and tooltip
130+
if err := systrayIcon.SetIcon(sys.Hicon(ico)); err != nil {
131+
return nil, fmt.Errorf("unable to set systray icon: %v", err)
132+
}
133+
if err := systrayIcon.SetTooltip("Fibratus"); err != nil {
134+
return nil, fmt.Errorf("unable to set systray icon tooltip: %v", err)
135+
}
136+
s := &systray{
137+
wnd: hwnd,
138+
systrayIcon: systrayIcon,
139+
config: c,
140+
}
141+
return s, nil
142+
}
143+
144+
func (s systray) Send(alert alertsender.Alert) error {
145+
if s.systrayIcon == nil {
146+
return nil
147+
}
148+
return s.systrayIcon.ShowBalloonNotification(alert.Title, alert.Text, s.config.Sound, s.config.QuietMode)
149+
}
150+
151+
func (s systray) Type() alertsender.Type { return alertsender.Systray }
152+
func (s systray) SupportsMarkdown() bool { return false }
153+
154+
func (s systray) Shutdown() error {
155+
if s.wnd.IsValid() {
156+
s.wnd.Destroy()
157+
}
158+
if s.systrayIcon != nil {
159+
return s.systrayIcon.Delete()
160+
}
161+
return nil
162+
}
163+
164+
func wndProc(hwnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
165+
return sys.DefWindowProc(hwnd, msg, wparam, lparam)
166+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2021-2022 by Nedim Sabic Sabic
3+
* https://www.fibratus.io
4+
* All Rights Reserved.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package systray
20+
21+
import (
22+
"github.com/rabbitstack/fibratus/pkg/alertsender"
23+
"github.com/stretchr/testify/require"
24+
"testing"
25+
)
26+
27+
func TestSystraySender(t *testing.T) {
28+
s, err := alertsender.Load(alertsender.Config{Type: alertsender.Systray, Sender: Config{Enabled: true, Sound: false, QuietMode: false}})
29+
require.NoError(t, err)
30+
require.NotNil(t, s)
31+
require.NoError(t, s.Send(alertsender.Alert{
32+
Title: "LSASS memory dumping via legitimate or offensive tools",
33+
Text: `Detected an attempt by mimikatz.exe process to access and read
34+
the memory of the Local Security And Authority Subsystem Service
35+
and subsequently write the C:\\temp\lsass.dmp dump file to the disk device`}))
36+
}

0 commit comments

Comments
 (0)