Skip to content

Commit

Permalink
WIP: ElastAlertEngine Tests
Browse files Browse the repository at this point in the history
Refactored the ElastAlertEngine to use a new interface for interacting with external resources (Disk and Network). This allows for mocking these resources in the new tests.

Also updated Suricata's tests to include the new thresholding config value used by overrides.
  • Loading branch information
coreyogburn committed Dec 22, 2023
1 parent 15fe72a commit 2a70301
Show file tree
Hide file tree
Showing 6 changed files with 555 additions and 93 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
require (
github.com/pkg/errors v0.9.1
github.com/samber/lo v1.39.0
go.uber.org/mock v0.3.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
Expand Down
75 changes: 58 additions & 17 deletions server/modules/elastalert/elastalert.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"encoding/json"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
Expand All @@ -38,6 +39,14 @@ var acceptedExtensions = map[string]bool{
".yaml": true,
}

type IOManager interface {
ReadFile(path string) ([]byte, error)
WriteFile(path string, contents []byte, perm fs.FileMode) error
DeleteFile(path string) error
ReadDir(path string) ([]os.DirEntry, error)
MakeRequest(*http.Request) (*http.Response, error)
}

type ElastAlertEngine struct {
srv *server.Server
communityRulesImportFrequencySeconds int
Expand All @@ -49,11 +58,13 @@ type ElastAlertEngine struct {
sigmaConversionTarget string
isRunning bool
thread *sync.WaitGroup
IOManager
}

func NewElastAlertEngine(srv *server.Server) *ElastAlertEngine {
return &ElastAlertEngine{
srv: srv,
srv: srv,
IOManager: &ResourceManager{},
}
}

Expand Down Expand Up @@ -156,7 +167,7 @@ func (e *ElastAlertEngine) SyncLocalDetections(ctx context.Context, detections [
}
}()

index, err := e.indexExistingRules()
index, err := e.IndexExistingRules()
if err != nil {
return nil, fmt.Errorf("unable to index existing rules: %w", err)
}
Expand All @@ -174,14 +185,14 @@ func (e *ElastAlertEngine) SyncLocalDetections(ctx context.Context, detections [
continue
}

err = os.WriteFile(path, []byte(eaRule), 0644)
err = e.WriteFile(path, []byte(eaRule), 0644)
if err != nil {
errMap[det.PublicID] = fmt.Sprintf("unable to write enabled detection file: %s", err)
continue
}
} else {
// was enabled, no longer is enabled: Disable
err = os.Remove(path)
err = e.DeleteFile(path)
if err != nil && !os.IsNotExist(err) {
errMap[det.PublicID] = fmt.Sprintf("unable to remove disabled detection file: %s", err)
continue
Expand Down Expand Up @@ -221,7 +232,7 @@ func (e *ElastAlertEngine) startCommunityRuleImport() {
zipHashes[pkg] = base64.StdEncoding.EncodeToString(h[:])
}

raw, err := os.ReadFile(e.rulesFingerprintFile)
raw, err := e.ReadFile(e.rulesFingerprintFile)
if err != nil && !os.IsNotExist(err) {
log.WithError(err).WithField("path", e.rulesFingerprintFile).Error("unable to read rules fingerprint file")
continue
Expand Down Expand Up @@ -273,7 +284,7 @@ func (e *ElastAlertEngine) startCommunityRuleImport() {
if err != nil {
log.WithError(err).Error("unable to marshal rules fingerprints")
} else {
err = os.WriteFile(e.rulesFingerprintFile, fingerprints, 0644)
err = e.WriteFile(e.rulesFingerprintFile, fingerprints, 0644)
if err != nil {
log.WithError(err).WithField("path", e.rulesFingerprintFile).Error("unable to write rules fingerprint file")
}
Expand Down Expand Up @@ -367,7 +378,7 @@ func (e *ElastAlertEngine) parseRules(pkgZips map[string][]byte) (detections []*
}

func (e *ElastAlertEngine) syncCommunityDetections(ctx context.Context, detections []*model.Detection) (errMap map[string]error, err error) {
existing, err := e.indexExistingRules()
existing, err := e.IndexExistingRules()
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -397,7 +408,6 @@ func (e *ElastAlertEngine) syncCommunityDetections(ctx context.Context, detectio

errMap = map[string]error{} // map[publicID]error

// detections = []*model.Detection{}
for _, det := range detections {
path, ok := index[det.PublicID]
if !ok {
Expand Down Expand Up @@ -448,14 +458,14 @@ func (e *ElastAlertEngine) syncCommunityDetections(ctx context.Context, detectio
path = filepath.Join(e.elastAlertRulesFolder, fmt.Sprintf("%s.yml", det.PublicID))
}

err = os.WriteFile(path, []byte(rule), 0644)
err = e.WriteFile(path, []byte(rule), 0644)
if err != nil {
errMap[det.PublicID] = fmt.Errorf("unable to write enabled detection file: %s", err)
continue
}
} else if path != "" {
// detection is disabled but a file exists, remove it
err = os.Remove(path)
err = e.DeleteFile(path)
if err != nil {
errMap[det.PublicID] = fmt.Errorf("unable to remove disabled detection file: %s", err)
continue
Expand Down Expand Up @@ -498,7 +508,13 @@ func (e *ElastAlertEngine) downloadSigmaPackages(ctx context.Context) (zipData m
for _, pkg := range e.sigmaRulePackages {
download := fmt.Sprintf(e.sigmaPackageDownloadTemplate, pkg)

resp, err := http.Get(download)
req, err := http.NewRequest(http.MethodGet, download, nil)
if err != nil {
errMap[pkg] = err
continue
}

resp, err := e.MakeRequest(req)
if err != nil {
errMap[pkg] = err
continue
Expand All @@ -523,12 +539,12 @@ func (e *ElastAlertEngine) downloadSigmaPackages(ctx context.Context) (zipData m
return zipData, errMap
}

// indexExistingRules maps the publicID of a detection to the path of the rule file.
// IndexExistingRules maps the publicID of a detection to the path of the rule file.
// Note that it indexes ALL rules and not just community rules.
func (e *ElastAlertEngine) indexExistingRules() (index map[string]string, err error) {
func (e *ElastAlertEngine) IndexExistingRules() (index map[string]string, err error) {
index = map[string]string{} // map[id | title]path

rules, err := os.ReadDir(e.elastAlertRulesFolder)
rules, err := e.ReadDir(e.elastAlertRulesFolder)
if err != nil {
return nil, fmt.Errorf("unable to read elastalert rules directory: %w", err)
}
Expand Down Expand Up @@ -582,7 +598,7 @@ func (e *ElastAlertEngine) sigmaToElastAlert(ctx context.Context, det *model.Det
req.Header.Add("Content-Type", "application/json")

// send request
resp, err := http.DefaultClient.Do(req)
resp, err := e.MakeRequest(req)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -763,7 +779,7 @@ func wrapRule(det *model.Detection, rule string) (string, error) {

// apply the first (should only have 1) CustomFilter override if any
for _, o := range det.Overrides {
if o.Type == model.OverrideTypeCustomFilter && o.CustomFilter != nil {
if o.IsEnabled && o.Type == model.OverrideTypeCustomFilter && o.CustomFilter != nil {
rule = fmt.Sprintf("(%s) and %s", rule, *o.CustomFilter)
break
}
Expand All @@ -775,7 +791,7 @@ func wrapRule(det *model.Detection, rule string) (string, error) {
EventModule: "elastalert",
EventDataset: "elastalert.alert",
EventSeverity: sevNum,
RuleCategory: "TBD",
RuleCategory: "", // TODO: what should this be?
SigmaLevel: string(det.Severity),
Alert: []string{"modules.so.playbook-es.PlaybookESAlerter"},
Index: ".ds-logs-*",
Expand All @@ -799,3 +815,28 @@ func wrapRule(det *model.Detection, rule string) (string, error) {

return string(rawYaml), nil
}

// go install go.uber.org/mock/mockgen@latest
//go:generate mockgen -destination mock/mock_iomanager.go -package mock . IOManager

type ResourceManager struct{}

func (_ *ResourceManager) ReadFile(path string) ([]byte, error) {
return os.ReadFile(path)
}

func (_ *ResourceManager) WriteFile(path string, contents []byte, perm fs.FileMode) error {
return os.WriteFile(path, contents, perm)
}

func (_ *ResourceManager) DeleteFile(path string) error {
return os.Remove(path)
}

func (_ *ResourceManager) ReadDir(path string) ([]os.DirEntry, error) {
return os.ReadDir(path)
}

func (_ *ResourceManager) MakeRequest(req *http.Request) (*http.Response, error) {
return http.DefaultClient.Do(req)
}
Loading

0 comments on commit 2a70301

Please sign in to comment.