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,
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.
It can also report for triggered Suricata IDS/IPS alerts.
### 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"
# becomes "detect_alert" when delimiter is "_".
delimiter = "_"
# Detect alert logs
alerts = false
```
### Metrics
@ -26,7 +30,7 @@ stats output.
See http://suricata.readthedocs.io/en/latest/performance/statistics.html for
more information.
All fields are numeric.
All fields for Suricata stats are numeric.
- suricata
- tags:
- 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
- ...
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

View File

@ -25,6 +25,7 @@ const (
type Suricata struct {
Source string `toml:"source"`
Delimiter string `toml:"delimiter"`
Alerts bool `toml:"alerts"`
inputListener *net.UnixListener
cancel context.CancelFunc
@ -36,11 +37,11 @@ type Suricata struct {
// Description returns the plugin description.
func (s *Suricata) Description() string {
return "Suricata stats plugin"
return "Suricata stats and alerts plugin"
}
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
# unix socket to be created for listening.
source = "/var/run/suricata-stats.sock"
@ -48,6 +49,9 @@ const sampleConfig = `
# Delimiter for flattening field keys, e.g. subitem "alert" of "detect"
# becomes "detect_alert" when delimiter is "_".
delimiter = "_"
## Detect alert logs
# alerts = false
`
// 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')
if rerr != nil {
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:
outmap[field] = v
case float64:
outmap[field] = v.(float64)
outmap[field] = t
default:
return fmt.Errorf("unsupported type %T encountered", t)
}
return nil
}
func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) {
// initial parsing
var result map[string]interface{}
err := json.Unmarshal(sjson, &result)
if err != nil {
acc.AddError(err)
func (s *Suricata) parseAlert(acc telegraf.Accumulator, result map[string]interface{}) {
if _, ok := result["alert"].(map[string]interface{}); !ok {
s.Log.Debug("'alert' sub-object does not have required structure")
return
}
// check for presence of relevant stats
if _, ok := result["stats"]; !ok {
s.Log.Debug("Input does not contain necessary 'stats' sub-object")
return
totalmap := make(map[string]interface{})
for k, v := range result["alert"].(map[string]interface{}) {
//source and target fields are maps
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 {
s.Log.Debug("The 'stats' sub-object does not have required structure")
return
@ -193,9 +208,9 @@ func (s *Suricata) parse(acc telegraf.Accumulator, sjson []byte) {
for k, t := range v {
outmap := make(map[string]interface{})
if threadStruct, ok := t.(map[string]interface{}); ok {
err = flexFlatten(outmap, "", threadStruct, s.Delimiter)
err := flexFlatten(outmap, "", threadStruct, s.Delimiter)
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
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")
}
} else {
err = flexFlatten(totalmap, k, v, s.Delimiter)
err := flexFlatten(totalmap, k, v, s.Delimiter)
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
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.
// Not used here, submission is completely input-driven.
func (s *Suricata) Gather(_ telegraf.Accumulator) error {

View File

@ -29,6 +29,7 @@ func TestSuricataLarge(t *testing.T) {
s := Suricata{
Source: tmpfn,
Delimiter: ".",
Alerts: true,
Log: testutil.Logger{
Name: "inputs.suricata",
},
@ -46,11 +47,74 @@ func TestSuricataLarge(t *testing.T) {
require.NoError(t, err)
_, err = c.Write([]byte("\n"))
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())
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) {
dir, err := ioutil.TempDir("", "test")
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"}}