Skip to content

Commit 1cab108

Browse files
committed
refactor(alertsenders/mail): Move template rendering to sender
HTML templates are more likely to exclusively be used by the mail sender. For this purpose, a new config flag is defined that determines if the alert is sent with HTML template or plain text. Alert structure is augmented with labels and description fields to make it compatible with the same fields available in the filter config.
1 parent ad736c5 commit 1cab108

File tree

9 files changed

+78
-57
lines changed

9 files changed

+78
-57
lines changed

configs/fibratus.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ alertsenders:
5656
# Specifies the email body content type
5757
#content-type: text/html
5858

59+
# Indicates if the alert is rendered with HTML template
60+
#use-template: true
61+
5962
# Slack sender transports the alerts to the Slack workspace.
6063
slack:
6164
# Enables/disables Slack alert sender

pkg/alertsender/alert.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ type Alert struct {
8686
Text string
8787
// Tags contains a sequence of tags for categorizing the alerts.
8888
Tags []string
89+
// Labels is an arbitrary collection of key-value pairs.
90+
Labels map[string]string
91+
// Description represents a longer explanation of the alert. It is
92+
// typically a description of adversary tactics, techniques or any
93+
// information valuable to the analyst.
94+
Description string
8995
// Severity determines the severity of this alert.
9096
Severity Severity
9197
// Events contains a list of events that trigger the alert.

pkg/alertsender/mail/config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
to = "alertsenders.mail.to"
3030
enabled = "alertsenders.mail.enabled"
3131
contentType = "alertsenders.mail.content-type"
32+
useTemplate = "alertsenders.mail.use-template"
3233
)
3334

3435
// Config contains the configuration for the mail alert sender.
@@ -37,7 +38,7 @@ type Config struct {
3738
Host string `mapstructure:"host"`
3839
// Port is the port of the SMTP server.
3940
Port int `mapstructure:"port"`
40-
// User specifies the user name when authenticating to the SMTP server.
41+
// User specifies the username when authenticating to the SMTP server.
4142
User string `mapstructure:"user"`
4243
// Pass specifies the password when authenticating to the SMTP server.
4344
Pass string `mapstructure:"password"`
@@ -49,6 +50,9 @@ type Config struct {
4950
Enabled bool `mapstructure:"enabled"`
5051
// ContentType represents the email body content type.
5152
ContentType string `mapstructure:"content-type"`
53+
// UseTemplate indicates if the alert is rendered with HTML template.
54+
// If set to false, the plain text email is sent instead.
55+
UseTemplate bool `mapstructure:"use-template"`
5256
}
5357

5458
// AddFlags registers persistent flags.
@@ -61,4 +65,5 @@ func AddFlags(flags *pflag.FlagSet) {
6165
flags.StringSlice(to, []string{}, "Specifies all the recipients that'll receive the alert")
6266
flags.Bool(enabled, false, "Indicates whether mail alert sender is enabled")
6367
flags.String(contentType, "text/html", "Represents the email body content type")
68+
flags.Bool(useTemplate, true, "Indicates if the alert is rendered with HTML template")
6469
}

pkg/alertsender/mail/mail.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,34 @@ func (s mail) Send(alert alertsender.Alert) error {
4848
return err
4949
}
5050
defer sender.Close()
51-
return gomail.Send(sender, s.composeMessage(s.c.From, s.c.To, alert))
51+
msg, err := s.composeMessage(s.c.From, s.c.To, alert)
52+
if err != nil {
53+
return err
54+
}
55+
return gomail.Send(sender, msg)
5256
}
5357

5458
func (s mail) Type() alertsender.Type { return alertsender.Mail }
5559
func (s mail) Shutdown() error { return nil }
5660
func (s mail) SupportsMarkdown() bool { return true }
5761

58-
func (s mail) composeMessage(from string, to []string, alert alertsender.Alert) *gomail.Message {
62+
func (s mail) composeMessage(from string, to []string, alert alertsender.Alert) (*gomail.Message, error) {
5963
msg := gomail.NewMessage()
6064
msg.SetHeader("From", from)
6165
msg.SetHeader("To", to...)
6266
msg.SetHeader("Subject", alert.Title)
63-
msg.SetBody(s.c.ContentType, alert.Text)
64-
return msg
67+
68+
body := alert.Text
69+
70+
var err error
71+
if s.c.UseTemplate {
72+
body, err = renderHTMLTemplate(alert)
73+
}
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
msg.SetBody(s.c.ContentType, body)
79+
80+
return msg, nil
6581
}

pkg/alertsender/renderer/renderer.go renamed to pkg/alertsender/mail/renderer.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,36 +16,34 @@
1616
* limitations under the License.
1717
*/
1818

19-
package renderer
19+
package mail
2020

2121
import (
2222
"bytes"
2323
"github.com/Masterminds/sprig/v3"
2424
"github.com/rabbitstack/fibratus/pkg/alertsender"
25-
"github.com/rabbitstack/fibratus/pkg/config"
2625
"github.com/rabbitstack/fibratus/pkg/util/hostname"
2726
"github.com/rabbitstack/fibratus/pkg/util/version"
2827
"text/template"
2928
"time"
3029
)
3130

32-
// RenderHTMLRuleAlert produces HTML template for rule alerts. This function generates
33-
// inlined CSS to maximize the compatibility between email clients when the alert is
34-
// transported via email sender or other senders that may render HTML content.
35-
func RenderHTMLRuleAlert(ctx *config.ActionContext, alert alertsender.Alert) (string, error) {
31+
// renderHTMLTemplate produces HTML template for the alert.
32+
// This function generates inlined CSS to maximize the compatibility
33+
// across email clients.
34+
func renderHTMLTemplate(alert alertsender.Alert) (string, error) {
3635
data := struct {
37-
*config.ActionContext
3836
Alert alertsender.Alert
3937
TriggeredAt time.Time
4038
Hostname string
4139
Version string
4240
}{
43-
ctx,
4441
alert,
4542
time.Now(),
4643
hostname.Get(),
4744
version.Get(),
4845
}
46+
4947
_ = data.Alert.MDToHTML()
5048
funcmap := sprig.TxtFuncMap()
5149

@@ -56,7 +54,7 @@ func RenderHTMLRuleAlert(ctx *config.ActionContext, alert alertsender.Alert) (st
5654
}
5755
return false
5856
}
59-
tmpl, err := template.New("rule-alert").Funcs(funcmap).Parse(ruleAlertHTMLTemplate)
57+
tmpl, err := template.New("alert").Funcs(funcmap).Parse(htmlTemplate)
6058
if err != nil {
6159
return "", err
6260
}

pkg/alertsender/renderer/renderer_test.go renamed to pkg/alertsender/mail/renderer_test.go

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
* limitations under the License.
1717
*/
1818

19-
package renderer
19+
package mail
2020

2121
import (
2222
"github.com/antchfx/htmlquery"
2323
"github.com/rabbitstack/fibratus/pkg/alertsender"
24-
"github.com/rabbitstack/fibratus/pkg/config"
2524
htypes "github.com/rabbitstack/fibratus/pkg/handle/types"
2625
"github.com/rabbitstack/fibratus/pkg/kevent"
2726
"github.com/rabbitstack/fibratus/pkg/kevent/kparams"
@@ -37,17 +36,19 @@ import (
3736
"time"
3837
)
3938

40-
func TestHTMLFormatterRuleAlert(t *testing.T) {
41-
out, err := RenderHTMLRuleAlert(&config.ActionContext{
42-
Filter: &config.FilterConfig{
43-
Labels: map[string]string{
44-
"tactic.name": "Credential Access",
45-
"tactic.ref": "https://attack.mitre.org/tactics/TA0006/",
46-
"technique.name": "Credentials from Password Stores",
47-
"technique.ref": "https://attack.mitre.org/techniques/T1555/",
48-
"subtechnique.name": "Windows Credential Manager",
49-
"subtechnique.ref": "https://attack.mitre.org/techniques/T1555/004/",
50-
},
39+
func TestRenderHTMLTemplate(t *testing.T) {
40+
out, err := renderHTMLTemplate(alertsender.Alert{
41+
Title: "Suspicious access to Windows Vault files",
42+
Text: "`cmd.exe` attempted to access Windows Vault files which was considered as a suspicious activity",
43+
Severity: alertsender.Critical,
44+
Description: "Identifies attempts from adversaries to acquire credentials from Vault files",
45+
Labels: map[string]string{
46+
"tactic.name": "Credential Access",
47+
"tactic.ref": "https://attack.mitre.org/tactics/TA0006/",
48+
"technique.name": "Credentials from Password Stores",
49+
"technique.ref": "https://attack.mitre.org/techniques/T1555/",
50+
"subtechnique.name": "Windows Credential Manager",
51+
"subtechnique.ref": "https://attack.mitre.org/techniques/T1555/004/",
5152
},
5253
Events: []*kevent.Kevent{
5354
{
@@ -240,11 +241,8 @@ func TestHTMLFormatterRuleAlert(t *testing.T) {
240241
},
241242
},
242243
},
243-
},
244-
alertsender.Alert{
245-
Title: "Suspicious access to Windows Vault files",
246-
Text: "`cmd.exe` attempted to access Windows Vault files which was considered as a suspicious activity",
247-
Severity: alertsender.Critical})
244+
})
245+
248246
require.NoError(t, err)
249247
doc, err := htmlquery.Parse(strings.NewReader(out))
250248
require.NoError(t, err)

pkg/alertsender/renderer/template.go renamed to pkg/alertsender/mail/template.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
* limitations under the License.
1717
*/
1818

19-
package renderer
19+
package mail
2020

21-
var ruleAlertHTMLTemplate = `
21+
var htmlTemplate = `
2222
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2323
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2424
<html xmlns="http://www.w3.org/1999/xhtml">
@@ -75,28 +75,26 @@ var ruleAlertHTMLTemplate = `
7575
{{ $text := (regexReplaceAll "<code>" .Alert.Text "<code style='border-radius: 5px; color: #404243; font-size: .8rem; margin: 0 2px; padding: 3px 5px; line-height: 1.7rem; white-space: pre-wrap; font-weight: 600; font-family: Consolas, Roboto, monaco, monospace; background-color: #e1e3e4;'>") }}
7676
<p style="font-size: .8rem; margin: 0 0 4px 0px; padding: 3px 0px; white-space: pre-wrap; line-height: 1.5em;">{{ regexReplaceAll "\\s+" $text " " }}</p>
7777
{{- end }}
78-
{{ if hasKey .Filter.Labels "tactic.name" }}
79-
<div class="tag" style="display: inline-block; border-radius: 5px; color: #404243; font-size: .8rem; margin: 2px 2px; padding: 3px 5px; white-space: pre-wrap; font-weight: 600; background-color: #bad1fb;"><a style="text-decoration: none; color: inherit;" href="{{ index .Filter.Labels "tactic.ref"}}">{{ index .Filter.Labels "tactic.name"}}</a></div>
78+
{{ if hasKey .Alert.Labels "tactic.name" }}
79+
<div class="tag" style="display: inline-block; border-radius: 5px; color: #404243; font-size: .8rem; margin: 2px 2px; padding: 3px 5px; white-space: pre-wrap; font-weight: 600; background-color: #bad1fb;"><a style="text-decoration: none; color: inherit;" href="{{ index .Alert.Labels "tactic.ref"}}">{{ index .Alert.Labels "tactic.name"}}</a></div>
8080
{{ end }}
81-
{{ if hasKey .Filter.Labels "technique.name" }}
82-
<div class="tag" style="display: inline-block; border-radius: 5px; color: #404243; font-size: .8rem; margin: 2px 2px; padding: 3px 5px; white-space: pre-wrap; font-weight: 600; background-color: #84cad7;"><a style="text-decoration: none; color: inherit;" href="{{ index .Filter.Labels "technique.ref"}}">{{ index .Filter.Labels "technique.name"}}</a></div>
81+
{{ if hasKey .Alert.Labels "technique.name" }}
82+
<div class="tag" style="display: inline-block; border-radius: 5px; color: #404243; font-size: .8rem; margin: 2px 2px; padding: 3px 5px; white-space: pre-wrap; font-weight: 600; background-color: #84cad7;"><a style="text-decoration: none; color: inherit;" href="{{ index .Alert.Labels "technique.ref"}}">{{ index .Alert.Labels "technique.name"}}</a></div>
8383
{{ end }}
84-
{{ if hasKey .Filter.Labels "subtechnique.name" }}
85-
<div class="tag" style="display: inline-block; border-radius: 5px; color: #404243; font-size: .8rem; margin: 2px 2px; padding: 3px 5px; white-space: pre-wrap; font-weight: 600; background-color: #fcbcba;"><a style="text-decoration: none; color: inherit;" href="{{ index .Filter.Labels "subtechnique.ref"}}">{{ index .Filter.Labels "subtechnique.name"}}</a></div>
84+
{{ if hasKey .Alert.Labels "subtechnique.name" }}
85+
<div class="tag" style="display: inline-block; border-radius: 5px; color: #404243; font-size: .8rem; margin: 2px 2px; padding: 3px 5px; white-space: pre-wrap; font-weight: 600; background-color: #fcbcba;"><a style="text-decoration: none; color: inherit;" href="{{ index .Alert.Labels "subtechnique.ref"}}">{{ index .Alert.Labels "subtechnique.name"}}</a></div>
8686
{{ end }}
8787
</td>
8888
</tr>
89+
{{- if .Alert.Description }}
8990
<tr>
9091
<td style="padding: 5px 0px 0px 15px;">
9192
<p style="background: #efefef; display: inline-block; border-radius: 5px; font-size: .8rem; margin: 4px 4px 25px 0px; padding: 3px 5px; white-space: pre-wrap; line-height: 1.5em;">
92-
{{- if .Filter -}}
93-
{{- .Filter.Description | trimSuffix "." }}
94-
{{- else }}
95-
{{- .Group.Description | trimSuffix "." }}
96-
{{- end -}}
93+
{{- .Alert.Description | trimSuffix "." }}
9794
</p>
9895
</td>
9996
</tr>
97+
{{ end }}
10098
</table>
10199
</td>
102100
</tr>
@@ -109,7 +107,7 @@ var ruleAlertHTMLTemplate = `
109107
<h1 style="font-weight: bold; margin-bottom: 22px; margin-top: 0px; font-size: 16px;">
110108
Security events involved in this incident
111109
</h1>
112-
{{- range $i, $evt := .Events }}
110+
{{- range $i, $evt := .Alert.Events }}
113111
{{ with $evt }}
114112
<table style="width: 100%; margin: 0; padding: 35px 0; border-collapse: collapse;" width="100%" cellpadding="0" cellspacing="0">
115113
<tr>

pkg/config/schema_windows.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ var schema = `
5252
"password": {"type": "string"},
5353
"from": {"type": "string"},
5454
"to": {"type": "array", "items": {"type": "string", "format": "email"}},
55-
"content-type": {"type": "string"}
55+
"content-type": {"type": "string"},
56+
"use-template": {"type": "boolean"}
5657
},
5758
"if": {
5859
"properties": {"enabled": { "const": true }}

pkg/filter/action/emit.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ package action
2121
import (
2222
"fmt"
2323
"github.com/rabbitstack/fibratus/pkg/alertsender"
24-
"github.com/rabbitstack/fibratus/pkg/alertsender/renderer"
2524
"github.com/rabbitstack/fibratus/pkg/config"
2625
"github.com/rabbitstack/fibratus/pkg/util/markdown"
2726
log "github.com/sirupsen/logrus"
@@ -49,25 +48,22 @@ func Emit(ctx *config.ActionContext, title string, text string, severity string,
4948
tags,
5049
alertsender.ParseSeverityFromString(severity),
5150
)
51+
5252
alert.ID = ctx.Filter.ID
5353
alert.Events = ctx.Events
54+
alert.Labels = ctx.Filter.Labels
55+
alert.Description = ctx.Filter.Description
5456

55-
// strip markdown
57+
// strip markdown if not supported by the sender
5658
if !sender.SupportsMarkdown() {
5759
alert.Text = markdown.Strip(alert.Text)
5860
}
59-
// produce HTML rule alert text for email sender
60-
if sender.Type() == alertsender.Mail {
61-
var err error
62-
alert.Text, err = renderer.RenderHTMLRuleAlert(ctx, alert)
63-
if err != nil {
64-
return err
65-
}
66-
}
61+
6762
err := sender.Send(alert)
6863
if err != nil {
6964
return fmt.Errorf("unable to emit alert from rule via [%s] sender: %v", sender.Type(), err)
7065
}
7166
}
67+
7268
return nil
7369
}

0 commit comments

Comments
 (0)