Suricata alerts (#9322)
This commit is contained in:
parent
87c94e4ac3
commit
837eb31b3f
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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 {
|
if err != nil {
|
||||||
acc.AddError(err)
|
s.Log.Debugf("Flattening alert failed: %v", err)
|
||||||
return
|
// we skip this subitem as something did not parse correctly
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for presence of relevant stats
|
//threads field do not exist in alert output, always global
|
||||||
if _, ok := result["stats"]; !ok {
|
acc.AddFields("suricata_alert", totalmap, nil)
|
||||||
s.Log.Debug("Input does not contain necessary 'stats' sub-object")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"}}
|
||||||
Loading…
Reference in New Issue