-
Notifications
You must be signed in to change notification settings - Fork 31
/
detector.go
135 lines (120 loc) · 3.91 KB
/
detector.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package morgoth
import (
"log"
"sync"
"github.com/nathanielc/morgoth/counter"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
)
type Detector struct {
mu sync.RWMutex
consensus float64
minSupport float64
errorTolerance float64
counters []fingerprinterCounter
metrics *DetectorMetrics
}
type DetectorMetrics struct {
WindowCount prometheus.Counter
PointCount prometheus.Counter
AnomalousCount prometheus.Counter
FingerprinterMetrics []*counter.Metrics
}
func (m *DetectorMetrics) Register() error {
if err := prometheus.Register(m.WindowCount); err != nil {
return errors.Wrap(err, "window count metric")
}
if err := prometheus.Register(m.PointCount); err != nil {
return errors.Wrap(err, "point count metric")
}
if err := prometheus.Register(m.AnomalousCount); err != nil {
return errors.Wrap(err, "anomalous count metric")
}
for i, f := range m.FingerprinterMetrics {
if err := f.Register(); err != nil {
return errors.Wrapf(err, "fingerprinter %d", i)
}
}
return nil
}
func (m *DetectorMetrics) Unregister() {
prometheus.Unregister(m.WindowCount)
prometheus.Unregister(m.PointCount)
prometheus.Unregister(m.AnomalousCount)
for _, f := range m.FingerprinterMetrics {
f.Unregister()
}
}
// Pair of fingerprinter and counter
type fingerprinterCounter struct {
Fingerprinter
counter.Counter
}
// Create a new Lossy couting based detector
// The consensus is a percentage of the fingerprinters that must agree in order to flag a window as anomalous.
// If the consensus is -1 then the average support from each fingerprinter is compared to minSupport instead of using a consensus.
// The minSupport defines a minimum frequency as a percentage for a window to be considered normal.
// The errorTolerance defines a frequency as a precentage for the smallest frequency that will be retained in memory.
// The errorTolerance must be less than the minSupport.
func NewDetector(metrics *DetectorMetrics, consensus, minSupport, errorTolerance float64, fingerprinters []Fingerprinter) (*Detector, error) {
if (consensus != -1 && consensus < 0) || consensus > 1 {
return nil, errors.New("consensus must be in the range [0,1) or equal to -1")
}
if minSupport <= errorTolerance {
return nil, errors.New("minSupport must be greater than errorTolerance")
}
if len(metrics.FingerprinterMetrics) != len(fingerprinters) {
return nil, errors.New("must provide the same number of fingerprinter metrics as fingerprinters")
}
counters := make([]fingerprinterCounter, len(fingerprinters))
for i, fingerprinter := range fingerprinters {
counters[i] = fingerprinterCounter{
Fingerprinter: fingerprinter,
Counter: counter.NewLossyCounter(metrics.FingerprinterMetrics[i], errorTolerance),
}
}
return &Detector{
metrics: metrics,
consensus: consensus,
minSupport: minSupport,
errorTolerance: errorTolerance,
counters: counters,
}, nil
}
// Determine if the window is anomalous
func (self *Detector) IsAnomalous(window *Window) (bool, float64) {
self.mu.Lock()
defer self.mu.Unlock()
self.metrics.WindowCount.Inc()
self.metrics.PointCount.Add(float64(len(window.Data)))
vote := 0.0
avgSupport := 0.0
n := 0.0
for _, fc := range self.counters {
fingerprint := fc.Fingerprint(window.Copy())
support := fc.Count(fingerprint)
anomalous := support <= self.minSupport
if anomalous {
vote++
}
log.Printf("D! %T anomalous? %v support: %f", fc.Fingerprinter, anomalous, support)
avgSupport = ((avgSupport * n) + support) / (n + 1)
n++
}
anomalous := false
if self.consensus != -1 {
// Use voting consensus
vote /= float64(len(self.counters))
anomalous = vote >= self.consensus
} else {
// Use average suppport
anomalous = avgSupport <= self.minSupport
}
if anomalous {
self.metrics.AnomalousCount.Inc()
}
return anomalous, avgSupport
}
func (self *Detector) Close() {
self.metrics.Unregister()
}