From 0412e7ebcde871923678d952a77fa7e31c616d55 Mon Sep 17 00:00:00 2001 From: davidnewhall2 Date: Thu, 25 Jun 2020 02:04:39 -0700 Subject: [PATCH] add alarms and anomalies --- core/unifi/alarms.go | 120 +++++++++++++++++++++++++++++++++++++ core/unifi/anomalies.go | 130 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 core/unifi/alarms.go create mode 100644 core/unifi/anomalies.go diff --git a/core/unifi/alarms.go b/core/unifi/alarms.go new file mode 100644 index 00000000..63c558e9 --- /dev/null +++ b/core/unifi/alarms.go @@ -0,0 +1,120 @@ +package unifi + +import ( + "fmt" + "sort" + "time" +) + +type Alarm struct { + Archived FlexBool `json:"archived"` + DestPort int `json:"dest_port"` + SrcPort int `json:"src_port"` + FlowID int64 `json:"flow_id"` + InnerAlertGID int64 `json:"inner_alert_gid"` + InnerAlertRev int64 `json:"inner_alert_rev"` + InnerAlertSeverity int64 `json:"inner_alert_severity"` + InnerAlertSignatureID int64 `json:"inner_alert_signature_id"` + Time int64 `json:"time"` + Timestamp int64 `json:"timestamp"` + Datetime time.Time `json:"datetime"` + HandledTime time.Time `json:"handled_time,omitempty"` + AppProto string `json:"app_proto,omitempty"` + Catname string `json:"catname"` + DestIP string `json:"dest_ip"` + DstMAC string `json:"dst_mac"` + DstIPASN string `json:"dstipASN,omitempty"` + DstIPCountry string `json:"dstipCountry,omitempty"` + EventType string `json:"event_type"` + HandledAdminID string `json:"handled_admin_id,omitempty"` + Host string `json:"host"` + ID string `json:"_id"` + InIface string `json:"in_iface"` + InnerAlertAction string `json:"inner_alert_action"` + InnerAlertCategory string `json:"inner_alert_category"` + InnerAlertSignature string `json:"inner_alert_signature"` + Key string `json:"key"` + Msg string `json:"msg"` + Proto string `json:"proto"` + SiteID string `json:"site_id"` + SiteName string `json:"-"` + SourceName string `json:"-"` + SrcIP string `json:"src_ip"` + SrcIPASN string `json:"srcipASN,omitempty"` + SrcIPCountry string `json:"srcipCountry,omitempty"` + SrcMAC string `json:"src_mac"` + Subsystem string `json:"subsystem"` + UniqueAlertID string `json:"unique_alertid"` + USGIP string `json:"usgip"` + USGIPASN string `json:"usgipASN"` + USGIPCountry string `json:"usgipCountry"` + TxID FlexInt `json:"tx_id,omitempty"` + DestIPGeo IPGeo `json:"dstipGeo"` + SourceIPGeo IPGeo `json:"usgipGeo"` + USGIPGeo IPGeo `json:"srcipGeo,omitempty"` +} + +// GetAlarms returns Alarms for a list of Sites. +func (u *Unifi) GetAlarms(sites []*Site) ([]*Alarm, error) { + data := []*Alarm{} + + for _, site := range sites { + response, err := u.GetAlarmsSite(site) + if err != nil { + return data, err + } + + data = append(data, response...) + } + + return data, nil +} + +// GetAlarmsSite retreives the Alarms for a single Site. +func (u *Unifi) GetAlarmsSite(site *Site) ([]*Alarm, error) { + if site == nil || site.Name == "" { + return nil, errNoSiteProvided + } + + u.DebugLog("Polling Controller for Alarms, site %s (%s)", site.Name, site.Desc) + + var ( + path = fmt.Sprintf(APIEventPathAlarms, site.Name) + alarms struct { + Data alarms `json:"data"` + } + ) + + if err := u.GetData(path, &alarms, ""); err != nil { + return alarms.Data, err + } + + for i := range alarms.Data { + // Add special SourceName value. + alarms.Data[i].SourceName = u.URL + // Add the special "Site Name" to each event. This becomes a Grafana filter somewhere. + alarms.Data[i].SiteName = site.Desc + " (" + site.Name + ")" + } + + sort.Sort(alarms.Data) + + return alarms.Data, nil +} + +// alarms satisfies the sort.Sort Interface. +type alarms []*Alarm + +// Len satisfies sort.Interface. +func (a alarms) Len() int { + return len(a) +} + +// Swap satisfies sort.Interface. +func (a alarms) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +// Less satisfies sort.Interface. Sort our list by Datetime. +func (a alarms) Less(i, j int) bool { + return a[i].Datetime.Before(a[j].Datetime) +} diff --git a/core/unifi/anomalies.go b/core/unifi/anomalies.go new file mode 100644 index 00000000..9f7de0e2 --- /dev/null +++ b/core/unifi/anomalies.go @@ -0,0 +1,130 @@ +package unifi + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" +) + +// anomaly is the type UniFi returns, but not the type this library returns. +type anomaly struct { + Anomaly string `json:"anomaly"` + MAC string `json:"mac"` + Timestamps []int64 `json:"timestamps"` +} + +// Anomaly is the reformatted data type that this library returns. +type Anomaly struct { + Datetime time.Time + SourceName string + SiteName string + Anomaly string + DeviceMAC string + // DeviceName string // we do not have this.... +} + +// GetAnomalies returns Anomalies for a list of Sites. +func (u *Unifi) GetAnomalies(sites []*Site, timeRange ...time.Time) ([]*Anomaly, error) { + data := []*Anomaly{} + + for _, site := range sites { + response, err := u.GetAnomaliesSite(site, timeRange...) + if err != nil { + return data, err + } + + data = append(data, response...) + } + + return data, nil +} + +// GetAnomaliesSite retreives the Anomalies for a single Site. +func (u *Unifi) GetAnomaliesSite(site *Site, timeRange ...time.Time) ([]*Anomaly, error) { + if site == nil || site.Name == "" { + return nil, errNoSiteProvided + } + + u.DebugLog("Polling Controller for Anomalies, site %s (%s)", site.Name, site.Desc) + + var ( + path = fmt.Sprintf(APIAnomaliesPath, site.Name) + anomalies = anomalies{} + data struct { + Data []*anomaly `json:"data"` + } + ) + + if params, err := makeAnomalyParams("hourly", timeRange...); err != nil { + return anomalies, err + } else if err := u.GetData(path+params, &data, ""); err != nil { + return anomalies, err + } + + for _, d := range data.Data { + for _, ts := range d.Timestamps { + anomalies = append(anomalies, &Anomaly{ + Datetime: time.Unix(ts/int64(time.Microsecond), 0), + SourceName: u.URL, + SiteName: site.Desc + " (" + site.Name + ")", + Anomaly: d.Anomaly, + DeviceMAC: d.MAC, + // DeviceName: d.Anomaly, + }) + } + } + + sort.Sort(anomalies) + + return anomalies, nil +} + +// anomalies satisfies the sort.Sort interface. +type anomalies []*Anomaly + +// Len satisfies sort.Interface. +func (a anomalies) Len() int { + return len(a) +} + +// Swap satisfies sort.Interface. +func (a anomalies) Swap(i, j int) { + a[i], a[j] = a[j], a[i] +} + +// Less satisfies sort.Interface. Sort our list by Datetime. +func (a anomalies) Less(i, j int) bool { + return a[i].Datetime.Before(a[j].Datetime) +} + +func makeAnomalyParams(scale string, timeRange ...time.Time) (string, error) { + out := []string{} + + if scale != "" { + out = append(out, "scale="+scale) + } + + switch len(timeRange) { + case 0: + end := time.Now().Unix() * int64(time.Microsecond) + out = append(out, "end="+strconv.FormatInt(end, 10)) + case 1: + start := timeRange[0].Unix() * int64(time.Microsecond) + end := time.Now().Unix() * int64(time.Microsecond) + out = append(out, "end="+strconv.FormatInt(end, 10), "start="+strconv.FormatInt(start, 10)) + case 2: // nolint: gomnd + start := timeRange[0].Unix() * int64(time.Microsecond) + end := timeRange[1].Unix() * int64(time.Microsecond) + out = append(out, "end="+strconv.FormatInt(end, 10), "start="+strconv.FormatInt(start, 10)) + default: + return "", errInvalidTimeRange + } + + if len(out) == 0 { + return "", nil + } + + return "?" + strings.Join(out, "&"), nil +}