diff --git a/README.md b/README.md index d453ade..9859548 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,38 @@ $ head -n 100000 huge.eve.json | scripts/makelpush | redis-cli > /dev/null FEVER can optionally inject in-band test data into downstream submissions, such as passive DNS observations, so allow automated checks that receiving components are updated correctly. +* For injecting test alerts into the forwarded stream, use the `heartbeat.alert-times` list to specify when an alert heartbeat should be injected. The approach is identical to the one for the general heartbeats: at each specified time, an alert like + ```json + { + "timestamp": "2021-12-09T09:49:35.641252+0000", + "event_type": "alert", + "src_ip": "192.0.2.1", + "src_port": 39106, + "dest_ip": "192.0.2.2", + "dest_port": 80, + "proto": "TCP", + "alert": { + "action": "allowed", + "gid": 0, + "signature_id": 0, + "rev": 0, + "signature": "DCSO FEVER TEST alert", + "category": "Not Suspicious Traffic", + "severity": 0 + }, + "http": { + "hostname": "test-2021-12-09.vast", + "url": "/just-visiting", + "http_user_agent": "FEVER", + "http_content_type": "text/html", + "http_method": "GET", + "protocol": "HTTP/1.1", + "status": 200, + "length": 42 + } + } + ``` + will be created and forwarded. * For passive DNS observation submissions, use the `pdns.test-domain` config item to insert a dummy entry for that domain, e.g. for `pdns.tests-domain` set to `heartbeat.fever-heartbeat`: ```json { diff --git a/cmd/fever/cmds/run.go b/cmd/fever/cmds/run.go index 16cb934..e82e0be 100644 --- a/cmd/fever/cmds/run.go +++ b/cmd/fever/cmds/run.go @@ -513,8 +513,9 @@ func mainfunc(cmd *cobra.Command, args []string) { // Heartbeat injector enableHeartbeat := viper.GetBool("heartbeat.enable") heartbeatTimes := viper.GetStringSlice("heartbeat.times") + heartbeatAlertTimes := viper.GetStringSlice("heartbeat.alert-times") if enableHeartbeat { - hi, err := processing.MakeHeartbeatInjector(forwardHandler, heartbeatTimes) + hi, err := processing.MakeHeartbeatInjector(forwardHandler, heartbeatTimes, heartbeatAlertTimes) if err != nil { log.Fatal(err) } diff --git a/fever.yaml b/fever.yaml index 1c7865c..e2b9ad5 100644 --- a/fever.yaml +++ b/fever.yaml @@ -121,12 +121,15 @@ stenosis: #add-fields: # sensor-id: foobar -# Send 'heartbeat' HTTP event +# Send 'heartbeat' HTTP or alert event heartbeat: enable: false - # 24h HH:MM strings with local times to send heartbeat + # 24h HH:MM strings with local times to send heartbeat as HTTP event times: - "00:01" + # 24h HH:MM strings with local times to send heartbeat as alert + #alert-times: + # - "00:02" # Configuration for detailed flow metadata submission. flowextract: diff --git a/processing/heartbeat_injector.go b/processing/heartbeat_injector.go index 2348625..0c5a186 100644 --- a/processing/heartbeat_injector.go +++ b/processing/heartbeat_injector.go @@ -1,7 +1,7 @@ package processing // DCSO FEVER -// Copyright (c) 2020, DCSO GmbH +// Copyright (c) 2020, 2021, DCSO GmbH import ( "encoding/json" @@ -29,13 +29,14 @@ var ( type HeartbeatInjector struct { SensorID string Times []string + AlertTimes []string CloseChan chan bool Logger *log.Entry ForwardHandler Handler } // MakeHeartbeatInjector creates a new HeartbeatInjector. -func MakeHeartbeatInjector(forwardHandler Handler, injectTimes []string) (*HeartbeatInjector, error) { +func MakeHeartbeatInjector(forwardHandler Handler, injectTimes []string, alertTimes []string) (*HeartbeatInjector, error) { sensorID, err := util.GetSensorID() if err != nil { return nil, err @@ -45,19 +46,25 @@ func MakeHeartbeatInjector(forwardHandler Handler, injectTimes []string) (*Heart return nil, fmt.Errorf("invalid time specification in heartbeat injector config: '%s'", v) } } + for _, v := range alertTimes { + if !injectTimeRegex.Match([]byte(v)) { + return nil, fmt.Errorf("invalid alert time specification in heartbeat injector config: '%s'", v) + } + } a := &HeartbeatInjector{ ForwardHandler: forwardHandler, Logger: log.WithFields(log.Fields{ "domain": "heartbeat_injector", }), - Times: injectTimes, - CloseChan: make(chan bool), - SensorID: sensorID, + Times: injectTimes, + AlertTimes: alertTimes, + CloseChan: make(chan bool), + SensorID: sensorID, } return a, nil } -func makeHeartbeatEvent() types.Entry { +func makeHeartbeatEvent(eventType string) types.Entry { now := time.Now() entry := types.Entry{ SrcIP: "192.0.2.1", @@ -65,7 +72,7 @@ func makeHeartbeatEvent() types.Entry { DestIP: "192.0.2.2", DestPort: 80, Timestamp: time.Now().Format(types.SuricataTimestampFormat), - EventType: "http", + EventType: eventType, Proto: "TCP", HTTPHost: fmt.Sprintf("test-%d-%02d-%02d.vast", now.Year(), now.Month(), now.Day()), @@ -93,6 +100,15 @@ func makeHeartbeatEvent() types.Entry { HTTPContentType: "text/html", }, } + if eventType == "alert" { + eve.Alert = &types.AlertEvent{ + Action: "allowed", + Category: "Not Suspicious Traffic", + Signature: "DCSO FEVER TEST alert", + } + entry.HTTPHost = "testalert.fever" + eve.HTTP.Hostname = entry.HTTPHost + } json, err := json.Marshal(eve) if err != nil { log.Warn(err) @@ -113,7 +129,15 @@ func (a *HeartbeatInjector) Run() { curTime := time.Now().Format("15:04") for _, timeVal := range a.Times { if curTime == timeVal { - ev := makeHeartbeatEvent() + ev := makeHeartbeatEvent("http") + a.Logger.Debugf("creating heartbeat event for %s: %s", + curTime, string(ev.JSONLine)) + a.ForwardHandler.Consume(&ev) + } + } + for _, timeVal := range a.AlertTimes { + if curTime == timeVal { + ev := makeHeartbeatEvent("alert") a.Logger.Debugf("creating heartbeat event for %s: %s", curTime, string(ev.JSONLine)) a.ForwardHandler.Consume(&ev) diff --git a/processing/heartbeat_injector_test.go b/processing/heartbeat_injector_test.go index dcbb172..b83e869 100644 --- a/processing/heartbeat_injector_test.go +++ b/processing/heartbeat_injector_test.go @@ -1,7 +1,7 @@ package processing // DCSO FEVER -// Copyright (c) 2020, DCSO GmbH +// Copyright (c) 2020, 2021, DCSO GmbH import ( "fmt" @@ -39,7 +39,18 @@ func TestHeartbeatInjectorInvalidTime(t *testing.T) { Entries: make([]types.Entry, 0), } - _, err := MakeHeartbeatInjector(&hbth, []string{"foo"}) + _, err := MakeHeartbeatInjector(&hbth, []string{"foo"}, []string{}) + if err == nil { + t.Fatal("invalid time not caught") + } +} + +func TestHeartbeatInjectorInvalidAlertTime(t *testing.T) { + hbth := HeartbeatTestFwdHandler{ + Entries: make([]types.Entry, 0), + } + + _, err := MakeHeartbeatInjector(&hbth, []string{}, []string{"foo"}) if err == nil { t.Fatal("invalid time not caught") } @@ -53,7 +64,7 @@ func TestHeartbeatInjector(t *testing.T) { now := time.Now() ctime := []string{now.Format("15:04")} - hbi, err := MakeHeartbeatInjector(&hbth, ctime) + hbi, err := MakeHeartbeatInjector(&hbth, ctime, []string{}) if err != nil { t.Fatal(err) } @@ -82,3 +93,39 @@ func TestHeartbeatInjector(t *testing.T) { t.Fatalf("wrong hostname for heartbeat: %s", seenHost) } } + +func TestHeartbeatAlertInjector(t *testing.T) { + hbth := HeartbeatTestFwdHandler{ + Entries: make([]types.Entry, 0), + } + + now := time.Now() + atime := []string{now.Format("15:04")} + + hbi, err := MakeHeartbeatInjector(&hbth, []string{}, atime) + if err != nil { + t.Fatal(err) + } + + hbi.Run() + for { + hbth.Lock.Lock() + if len(hbth.Entries) > 0 { + hbth.Lock.Unlock() + break + } + hbth.Lock.Unlock() + time.Sleep(100 * time.Millisecond) + } + hbi.Stop() + + hbJSON := hbth.Entries[0].JSONLine + + sig, err := jsonparser.GetString([]byte(hbJSON), "alert", "signature") + if err != nil { + t.Fatal(err) + } + if sig != "DCSO FEVER TEST alert" { + t.Fatalf("wrong signature for test alert: %s", sig) + } +}