Suricata alerts (#9322)

This commit is contained in:
Roger Coll Aumatell 2021-07-27 23:31:24 +02:00 committed by GitHub
parent 87c94e4ac3
commit 837eb31b3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 140 additions and 20 deletions

View File

@ -4,6 +4,7 @@ This plugin reports internal performance counters of the Suricata IDS/IPS
engine, such as captured traffic volume, memory usage, uptime, flow counters, engine, such as captured traffic volume, memory usage, uptime, flow counters,
and much more. It provides a socket for the Suricata log output to write JSON and much more. It provides a socket for the Suricata log output to write JSON
stats output to, and processes the incoming data to fit Telegraf's format. stats output to, and processes the incoming data to fit Telegraf's format.
It can also report for triggered Suricata IDS/IPS alerts.
### Configuration ### Configuration
@ -17,6 +18,9 @@ stats output to, and processes the incoming data to fit Telegraf's format.
# Delimiter for flattening field keys, e.g. subitem "alert" of "detect" # Delimiter for flattening field keys, e.g. subitem "alert" of "detect"
# becomes "detect_alert" when delimiter is "_". # becomes "detect_alert" when delimiter is "_".
delimiter = "_" delimiter = "_"
# Detect alert logs
alerts = false
``` ```
### Metrics ### Metrics
@ -26,7 +30,7 @@ stats output.
See http://suricata.readthedocs.io/en/latest/performance/statistics.html for See http://suricata.readthedocs.io/en/latest/performance/statistics.html for
more information. more information.
All fields are numeric. All fields for Suricata stats are numeric.
- suricata - suricata
- tags: - tags:
- thread: `Global` for global statistics (if enabled), thread IDs (e.g. `W#03-enp0s31f6`) for thread-specific statistics - thread: `Global` for global statistics (if enabled), thread IDs (e.g. `W#03-enp0s31f6`) for thread-specific statistics
@ -94,6 +98,19 @@ All fields are numeric.
- tcp_synack - tcp_synack
- ... - ...
Some fields of the Suricata alerts are strings, for example the signatures. See https://suricata.readthedocs.io/en/suricata-6.0.0/output/eve/eve-json-format.html?highlight=priority#event-type-alert for more information.
- suricata_alert
- fields:
- action
- gid
- severity
- signature
- source_ip
- source_port
- target_port
- target_port
- ...
#### Suricata configuration #### Suricata configuration

View File

@ -25,6 +25,7 @@ const (
type Suricata struct { type Suricata struct {
Source string `toml:"source"` Source string `toml:"source"`
Delimiter string `toml:"delimiter"` Delimiter string `toml:"delimiter"`
Alerts bool `toml:"alerts"`
inputListener *net.UnixListener inputListener *net.UnixListener
cancel context.CancelFunc cancel context.CancelFunc
@ -36,11 +37,11 @@ type Suricata struct {
// Description returns the plugin description. // Description returns the plugin description.
func (s *Suricata) Description() string { func (s *Suricata) Description() string {
return "Suricata stats plugin" return "Suricata stats and alerts plugin"
} }
const sampleConfig = ` const sampleConfig = `
## Data sink for Suricata stats log ## Data sink for Suricata stats and alerts logs
# This is expected to be a filename of a # This is expected to be a filename of a
# unix socket to be created for listening. # unix socket to be created for listening.
source = "/var/run/suricata-stats.sock" source = "/var/run/suricata-stats.sock"
@ -48,6 +49,9 @@ const sampleConfig = `
# Delimiter for flattening field keys, e.g. subitem "alert" of "detect" # Delimiter for flattening field keys, e.g. subitem "alert" of "detect"
# becomes "detect_alert" when delimiter is "_". # becomes "detect_alert" when delimiter is "_".
delimiter = "_" delimiter = "_"
## Detect alert logs
# alerts = false
` `
// SampleConfig returns a sample TOML section to illustrate configuration // SampleConfig returns a sample TOML section to illustrate configuration
@ -100,8 +104,12 @@ func (s *Suricata) readInput(ctx context.Context, acc telegraf.Accumulator, conn
line, rerr := reader.ReadBytes('\n') line, rerr := reader.ReadBytes('\n')
if rerr != nil { if rerr != nil {
return rerr return rerr
} else if len(line) > 0 { }
s.parse(acc, line) if len(line) > 0 {
err := s.parse(acc, line)
if err != nil {
acc.AddError(err)
}
} }
} }
} }
@ -158,28 +166,35 @@ func flexFlatten(outmap map[string]interface{}, field string, v interface{}, del
case string: case string:
outmap[field] = v outmap[field] = v
case float64: case float64:
outmap[field] = v.(float64) outmap[field] = t
default: default:
return fmt.Errorf("unsupported type %T encountered", t) return fmt.Errorf("unsupported type %T encountered", t)
} }
return nil return nil
} }
func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) { func (s *Suricata) parseAlert(acc telegraf.Accumulator, result map[string]interface{}) {
// initial parsing if _, ok := result["alert"].(map[string]interface{}); !ok {
var result map[string]interface{} s.Log.Debug("'alert' sub-object does not have required structure")
err := json.Unmarshal(sjson, &result)
if err != nil {
acc.AddError(err)
return return
} }
// check for presence of relevant stats totalmap := make(map[string]interface{})
if _, ok := result["stats"]; !ok { for k, v := range result["alert"].(map[string]interface{}) {
s.Log.Debug("Input does not contain necessary 'stats' sub-object") //source and target fields are maps
return err := flexFlatten(totalmap, k, v, s.Delimiter)
if err != nil {
s.Log.Debugf("Flattening alert failed: %v", err)
// we skip this subitem as something did not parse correctly
continue
}
} }
//threads field do not exist in alert output, always global
acc.AddFields("suricata_alert", totalmap, nil)
}
func (s *Suricata) parseStats(acc telegraf.Accumulator, result map[string]interface{}) {
if _, ok := result["stats"].(map[string]interface{}); !ok { if _, ok := result["stats"].(map[string]interface{}); !ok {
s.Log.Debug("The 'stats' sub-object does not have required structure") s.Log.Debug("The 'stats' sub-object does not have required structure")
return return
@ -193,9 +208,9 @@ func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) {
for k, t := range v { for k, t := range v {
outmap := make(map[string]interface{}) outmap := make(map[string]interface{})
if threadStruct, ok := t.(map[string]interface{}); ok { if threadStruct, ok := t.(map[string]interface{}); ok {
err = flexFlatten(outmap, "", threadStruct, s.Delimiter) err := flexFlatten(outmap, "", threadStruct, s.Delimiter)
if err != nil { if err != nil {
s.Log.Debug(err) s.Log.Debugf("Flattening alert failed: %v", err)
// we skip this thread as something did not parse correctly // we skip this thread as something did not parse correctly
continue continue
} }
@ -206,10 +221,11 @@ func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) {
s.Log.Debug("The 'threads' sub-object does not have required structure") s.Log.Debug("The 'threads' sub-object does not have required structure")
} }
} else { } else {
err = flexFlatten(totalmap, k, v, s.Delimiter) err := flexFlatten(totalmap, k, v, s.Delimiter)
if err != nil { if err != nil {
s.Log.Debug(err.Error()) s.Log.Debugf("Flattening alert failed: %v", err)
// we skip this subitem as something did not parse correctly // we skip this subitem as something did not parse correctly
continue
} }
} }
} }
@ -224,6 +240,28 @@ func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) {
} }
} }
func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) error {
// initial parsing
var result map[string]interface{}
err := json.Unmarshal(sjson, &result)
if err != nil {
return err
}
// check for presence of relevant stats or alert
_, ok := result["stats"]
_, ok2 := result["alert"]
if !ok && !ok2 {
s.Log.Debugf("Invalid input without 'stats' or 'alert' object: %v", result)
return fmt.Errorf("input does not contain 'stats' or 'alert' object")
}
if ok {
s.parseStats(acc, result)
} else if ok2 && s.Alerts {
s.parseAlert(acc, result)
}
return nil
}
// Gather measures and submits one full set of telemetry to Telegraf. // Gather measures and submits one full set of telemetry to Telegraf.
// Not used here, submission is completely input-driven. // Not used here, submission is completely input-driven.
func (s *Suricata) Gather(_ telegraf.Accumulator) error { func (s *Suricata) Gather(_ telegraf.Accumulator) error {

View File

@ -29,6 +29,7 @@ func TestSuricataLarge(t *testing.T) {
s := Suricata{ s := Suricata{
Source: tmpfn, Source: tmpfn,
Delimiter: ".", Delimiter: ".",
Alerts: true,
Log: testutil.Logger{ Log: testutil.Logger{
Name: "inputs.suricata", Name: "inputs.suricata",
}, },
@ -46,11 +47,74 @@ func TestSuricataLarge(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
_, err = c.Write([]byte("\n")) _, err = c.Write([]byte("\n"))
require.NoError(t, err) require.NoError(t, err)
//test suricata alerts
data2, err := ioutil.ReadFile("testdata/test2.json")
require.NoError(t, err)
_, err = c.Write(data2)
require.NoError(t, err)
_, err = c.Write([]byte("\n"))
require.NoError(t, err)
require.NoError(t, c.Close()) require.NoError(t, c.Close())
acc.Wait(1) acc.Wait(1)
} }
func TestSuricataAlerts(t *testing.T) {
dir, err := ioutil.TempDir("", "test")
require.NoError(t, err)
defer os.RemoveAll(dir)
tmpfn := filepath.Join(dir, fmt.Sprintf("t%d", rand.Int63()))
s := Suricata{
Source: tmpfn,
Delimiter: ".",
Alerts: true,
Log: testutil.Logger{
Name: "inputs.suricata",
},
}
acc := testutil.Accumulator{}
require.NoError(t, s.Start(&acc))
defer s.Stop()
data, err := ioutil.ReadFile("testdata/test3.json")
require.NoError(t, err)
c, err := net.Dial("unix", tmpfn)
require.NoError(t, err)
_, err = c.Write(data)
require.NoError(t, err)
_, err = c.Write([]byte("\n"))
require.NoError(t, err)
require.NoError(t, c.Close())
acc.Wait(1)
expected := []telegraf.Metric{
testutil.MustMetric(
"suricata_alert",
map[string]string{},
map[string]interface{}{
"action": "allowed",
"category": "Misc activity",
"gid": float64(1),
"rev": float64(0),
"signature": "Corrupted HTTP body",
"signature_id": float64(6),
"severity": float64(3),
"source.ip": "10.0.0.5",
"target.ip": "179.60.192.3",
"source.port": float64(18715),
"target.port": float64(80),
},
time.Unix(0, 0),
),
}
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
}
func TestSuricata(t *testing.T) { func TestSuricata(t *testing.T) {
dir, err := ioutil.TempDir("", "test") dir, err := ioutil.TempDir("", "test")
require.NoError(t, err) require.NoError(t, err)

View File

@ -0,0 +1 @@
{"timestamp":"2021-05-30T20:07:13.208777+0200","flow_id":1696236471136137,"in_iface":"s1-suricata","event_type":"alert","src_ip":"10.0.0.5","src_port":18715,"dest_ip":"179.60.192.3","dest_port":80,"proto":"TCP","alert":{"action":"allowed","gid":1,"source":{"ip":"10.0.0.5","port":18715},"target":{"ip":"179.60.192.3","port":80},"signature_id":6,"rev":0,"signature":"Corrupted HTTP body","severity": 3,"category":"Misc activity","severity":3},"flow":{"pkts_toserver":1,"pkts_toclient":0,"bytes_toserver":174,"bytes_toclient":0,"start":"2021-05-30T20:07:13.208777+0200"}}