From f7da5c68a8de972b9fa5e086a770c2b924159144 Mon Sep 17 00:00:00 2001 From: davidnewhall2 Date: Fri, 19 Jun 2020 02:20:06 -0700 Subject: [PATCH] Updates for Events --- core/unifi/clients.go | 7 +- core/unifi/devices.go | 2 +- core/unifi/events.go | 175 +++++++++++++++++++++++++++++++++++++++++ core/unifi/ids.go | 179 +++++++++++++++--------------------------- core/unifi/site.go | 7 +- core/unifi/types.go | 6 +- 6 files changed, 249 insertions(+), 127 deletions(-) create mode 100644 core/unifi/events.go diff --git a/core/unifi/clients.go b/core/unifi/clients.go index a5a76019..193e502a 100644 --- a/core/unifi/clients.go +++ b/core/unifi/clients.go @@ -6,7 +6,7 @@ import ( ) // GetClients returns a response full of clients' data from the UniFi Controller. -func (u *Unifi) GetClients(sites Sites) (Clients, error) { +func (u *Unifi) GetClients(sites []*Site) ([]*Client, error) { data := make([]*Client, 0) for _, site := range sites { @@ -38,7 +38,7 @@ func (u *Unifi) GetClients(sites Sites) (Clients, error) { } // GetClientsDPI garners dpi data for clients. -func (u *Unifi) GetClientsDPI(sites Sites) ([]*DPITable, error) { +func (u *Unifi) GetClientsDPI(sites []*Site) ([]*DPITable, error) { var data []*DPITable for _, site := range sites { @@ -63,9 +63,6 @@ func (u *Unifi) GetClientsDPI(sites Sites) ([]*DPITable, error) { return data, nil } -// Clients contains a list that contains all of the unifi clients from a controller. -type Clients []*Client - // Client defines all the data a connected-network client contains. type Client struct { SourceName string `json:"-"` diff --git a/core/unifi/devices.go b/core/unifi/devices.go index 9ddfefe6..045e43d7 100644 --- a/core/unifi/devices.go +++ b/core/unifi/devices.go @@ -7,7 +7,7 @@ import ( ) // GetDevices returns a response full of devices' data from the UniFi Controller. -func (u *Unifi) GetDevices(sites Sites) (*Devices, error) { +func (u *Unifi) GetDevices(sites []*Site) (*Devices, error) { devices := new(Devices) for _, site := range sites { diff --git a/core/unifi/events.go b/core/unifi/events.go new file mode 100644 index 00000000..d1982e38 --- /dev/null +++ b/core/unifi/events.go @@ -0,0 +1,175 @@ +package unifi + +import ( + "encoding/json" + "fmt" + "time" +) + +var ( + errNoSiteProvided = fmt.Errorf("site must not be nil or empty") + errInvalidTimeRange = fmt.Errorf("only 0, 1 or 2 times may be provided to timeRange") +) + +const ( + eventLimit = 50000 +) + +// GetEvents returns a response full of UniFi Events from multiple sites. +// timeRange may have a length of 0, 1 or 2. The first time is Start, the second is End. +// Events between start and end are returned. End defaults to time.Now(). +func (u *Unifi) GetEvents(sites []*Site, timeRange ...time.Time) ([]*Event, error) { + data := make([]*Event, 0) + + for _, site := range sites { + response, err := u.GetSiteEvents(site, timeRange...) + if err != nil { + return data, err + } + + data = append(data, response...) + } + + return data, nil +} + +// GetSiteEvents retreives the events from a single site. +// timeRange may have a length of 0, 1 or 2. The first time is Start, the second is End. +// Events between start and end are returned. End defaults to time.Now(). +func (u *Unifi) GetSiteEvents(site *Site, timeRange ...time.Time) ([]*Event, error) { // nolint: dupl + if site == nil || site.Name == "" { + return nil, errNoSiteProvided + } + + u.DebugLog("Polling Controller, retreiving UniFi Events, site %s (%s)", site.Name, site.Desc) + + var ( + path = fmt.Sprintf(APIEventPath, site.Name) + event struct { + Data []*Event `json:"data"` + } + ) + + if params, err := makeEventParams(timeRange...); err != nil { + return event.Data, err + } else if err = u.GetData(path, &event, params); err != nil { + return event.Data, err + } + + for i := range event.Data { + // Add special SourceName value. + event.Data[i].SourceName = u.URL + // Add the special "Site Name" to each event. This becomes a Grafana filter somewhere. + event.Data[i].SiteName = site.Desc + " (" + site.Name + ")" + } + + return event.Data, nil +} + +func makeEventParams(timeRange ...time.Time) (string, error) { + type eventReq struct { + Start int64 `json:"start,omitempty"` + End int64 `json:"end,omitempty"` + Limit int `json:"_limit,omitempty"` + } + + rp := eventReq{Limit: eventLimit} + + switch len(timeRange) { + case 0: + rp.End = time.Now().Unix() * int64(time.Microsecond) + case 1: + rp.Start = timeRange[0].Unix() * int64(time.Microsecond) + rp.End = time.Now().Unix() * int64(time.Microsecond) + case 2: // nolint: gomnd + rp.Start = timeRange[0].Unix() * int64(time.Microsecond) + rp.End = timeRange[1].Unix() * int64(time.Microsecond) + default: + return "", errInvalidTimeRange + } + + params, err := json.Marshal(&rp) + + return string(params), err +} + +// Event describes a UniFi Event. +// API Path: /api/s/default/stat/event. +type Event struct { + IsAdmin FlexBool `json:"is_admin"` + DestPort int `json:"dest_port"` + SrcPort int `json:"src_port"` + Bytes int64 `json:"bytes"` + Duration int64 `json:"duration"` + 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"` + Admin string `json:"admin"` + Ap string `json:"ap"` + ApFrom string `json:"ap_from"` + ApName string `json:"ap_name"` + ApTo string `json:"ap_to"` + AppProto string `json:"app_proto"` + Catname string `json:"catname"` + Channel string `json:"channel"` + ChannelFrom string `json:"channel_from"` + ChannelTo string `json:"channel_to"` + DestIP string `json:"dest_ip"` + DstMac string `json:"dst_mac"` + EventType string `json:"event_type"` + Guest string `json:"guest"` + Gw string `json:"gw"` + GwName string `json:"gw_name"` + Host string `json:"host"` + Hostname string `json:"hostname"` + ID string `json:"_id"` + IP string `json:"ip"` + 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"` + Network string `json:"network"` + Proto string `json:"proto"` + Radio string `json:"radio"` + RadioFrom string `json:"radio_from"` + RadioTo string `json:"radio_to"` + SiteID string `json:"site_id"` + SiteName string `json:"-"` + SourceName string `json:"-"` + SrcIP string `json:"src_ip"` + SrcMac string `json:"src_mac"` + SrcipASN string `json:"srcipASN"` + SrcipCountry string `json:"srcipCountry"` + Ssid string `json:"ssid"` + Subsystem string `json:"subsystem"` + Sw string `json:"sw"` + SwName string `json:"sw_name"` + UniqueAlertID string `json:"unique_alertid"` + User string `json:"user"` + Usgip string `json:"usgip"` + UsgipASN string `json:"usgipASN"` + UsgipCountry string `json:"usgipCountry"` + DestIPGeo IPGeo `json:"dstipGeo"` + SourceIPGeo IPGeo `json:"srcipGeo"` + USGIPGeo IPGeo `json:"usgipGeo"` +} + +// IPGeo is part of the UniFi Event data. Each event may have up to three of these. +// One for source, one for dest and one for the USG location. +type IPGeo struct { + Asn int64 `json:"asn"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + City string `json:"city"` + ContinentCode string `json:"continent_code"` + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` + Organization string `json:"organization"` +} diff --git a/core/unifi/ids.go b/core/unifi/ids.go index 1fc707aa..1946a69e 100644 --- a/core/unifi/ids.go +++ b/core/unifi/ids.go @@ -1,152 +1,103 @@ package unifi import ( - "encoding/json" "fmt" - "io/ioutil" - "net/http" "time" - - "github.com/pkg/errors" ) -// IDSList contains a list that contains all of the IDS Events on a controller. -type IDSList []*IDS - // IDS holds an Intrusion Prevention System Event. type IDS struct { - SourceName string `json:"-"` - ID string `json:"_id"` - Archived FlexBool `json:"archived"` - Timestamp int64 `json:"timestamp"` - FlowID int64 `json:"flow_id"` - InIface string `json:"in_iface"` - EventType string `json:"event_type"` - SrcIP string `json:"src_ip"` - SrcMac string `json:"src_mac"` - SrcPort int `json:"src_port,omitempty"` - DestIP string `json:"dest_ip"` - DstMac string `json:"dst_mac"` - DestPort int `json:"dest_port,omitempty"` - Proto string `json:"proto"` - AppProto string `json:"app_proto,omitempty"` - Host string `json:"host"` - Usgip string `json:"usgip"` - UniqueAlertid string `json:"unique_alertid"` - SrcipCountry string `json:"srcipCountry"` - DstipCountry FlexBool `json:"dstipCountry"` - UsgipCountry string `json:"usgipCountry"` - SrcipGeo struct { - ContinentCode string `json:"continent_code"` - CountryCode string `json:"country_code"` - CountryCode3 string `json:"country_code3"` - CountryName string `json:"country_name"` - Region string `json:"region"` - City string `json:"city"` - PostalCode string `json:"postal_code"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - DmaCode int64 `json:"dma_code"` - AreaCode int64 `json:"area_code"` - } `json:"srcipGeo"` - DstipGeo bool `json:"dstipGeo"` - UsgipGeo struct { - ContinentCode string `json:"continent_code"` - CountryCode string `json:"country_code"` - CountryCode3 string `json:"country_code3"` - CountryName string `json:"country_name"` - Region string `json:"region"` - City string `json:"city"` - PostalCode string `json:"postal_code"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - DmaCode int64 `json:"dma_code"` - AreaCode int64 `json:"area_code"` - } `json:"usgipGeo"` - SrcipASN string `json:"srcipASN"` - DstipASN string `json:"dstipASN"` - UsgipASN string `json:"usgipASN"` - Catname string `json:"catname"` - InnerAlertAction string `json:"inner_alert_action"` + Archived FlexBool `json:"archived"` + DstipCountry FlexBool `json:"dstipCountry"` + DestPort int `json:"dest_port,omitempty"` + SrcPort int `json:"src_port,omitempty"` + InnerAlertRev int64 `json:"inner_alert_rev"` + InnerAlertSeverity int64 `json:"inner_alert_severity"` InnerAlertGID int64 `json:"inner_alert_gid"` InnerAlertSignatureID int64 `json:"inner_alert_signature_id"` - InnerAlertRev int64 `json:"inner_alert_rev"` - InnerAlertSignature string `json:"inner_alert_signature"` + FlowID int64 `json:"flow_id"` + Time int64 `json:"time"` + Timestamp int64 `json:"timestamp"` + Datetime time.Time `json:"datetime"` + AppProto string `json:"app_proto,omitempty"` + Catname string `json:"catname"` + DestIP string `json:"dest_ip"` + DstMac string `json:"dst_mac"` + DstipASN string `json:"dstipASN"` + EventType string `json:"event_type"` + 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"` - InnerAlertSeverity int64 `json:"inner_alert_severity"` + InnerAlertSignature string `json:"inner_alert_signature"` Key string `json:"key"` - Subsystem string `json:"subsystem"` + Msg string `json:"msg"` + Proto string `json:"proto"` SiteID string `json:"site_id"` SiteName string `json:"-"` - Time int64 `json:"time"` - Datetime time.Time `json:"datetime"` - Msg string `json:"msg"` - IcmpType int64 `json:"icmp_type,omitempty"` - IcmpCode int64 `json:"icmp_code,omitempty"` + SourceName string `json:"-"` + SrcIP string `json:"src_ip"` + SrcMac string `json:"src_mac"` + SrcipASN string `json:"srcipASN"` + SrcipCountry string `json:"srcipCountry"` + Subsystem string `json:"subsystem"` + UniqueAlertID string `json:"unique_alertid"` + Usgip string `json:"usgip"` + UsgipASN string `json:"usgipASN"` + UsgipCountry string `json:"usgipCountry"` + DestIPGeo IPGeo `json:"dstipGeo"` + SourceIPGeo IPGeo `json:"srcipGeo"` + USGIPGeo IPGeo `json:"usgipGeo"` } -// GetIDS returns Intrusion Detection Systems events. -// Returns all events that happened in site between from and to. -func (u *Unifi) GetIDS(sites Sites, from, to time.Time) ([]*IDS, error) { +// GetIDS returns Intrusion Detection Systems events for a list of Sites. +// timeRange may have a length of 0, 1 or 2. The first time is Start, the second is End. +// Events between start and end are returned. End defaults to time.Now(). +func (u *Unifi) GetIDS(sites []*Site, timeRange ...time.Time) ([]*IDS, error) { data := []*IDS{} for _, site := range sites { - u.DebugLog("Polling Controller for IDS/IPS Data, site %s (%s) ", site.Name, site.Desc) - - ids, err := u.GetSiteIDS(site, from, to) + response, err := u.GetIDSSite(site, timeRange...) if err != nil { return data, err } - for i := range ids { - ids[i].SourceName = u.URL - } - - data = append(data, ids...) + data = append(data, response...) } return data, nil } -// GetSiteIDS is a helper to offload the for-loop work. -// This method retreives the Intrusion Detection System Data for 1 Site. -func (u *Unifi) GetSiteIDS(site *Site, from, to time.Time) ([]*IDS, error) { - var response struct { - Data []*IDS `json:"data"` +// GetIDSSite retreives the Intrusion Detection System Data for a single Site. +// timeRange may have a length of 0, 1 or 2. The first time is Start, the second is End. +// Events between start and end are returned. End defaults to time.Now(). +func (u *Unifi) GetIDSSite(site *Site, timeRange ...time.Time) ([]*IDS, error) { // nolint: dupl + if site == nil || site.Name == "" { + return nil, errNoSiteProvided } - URIpath := fmt.Sprintf(APIIPSEvents, site.Name) + u.DebugLog("Polling Controller for IDS Events, site %s (%s)", site.Name, site.Desc) - params := fmt.Sprintf(`{"start":"%v000","end":"%v000","_limit":50000}`, from.Unix(), to.Unix()) + var ( + path = fmt.Sprintf(APIEventPathIDS, site.Name) + ids struct { + Data []*IDS `json:"data"` + } + ) - req, err := u.UniReq(URIpath, params) - if err != nil { - return nil, err + if params, err := makeEventParams(timeRange...); err != nil { + return ids.Data, err + } else if err = u.GetData(path, &ids, params); err != nil { + return ids.Data, err } - resp, err := u.Do(req) - if err != nil { - return nil, err + for i := range ids.Data { + // Add special SourceName value. + ids.Data[i].SourceName = u.URL + // Add the special "Site Name" to each event. This becomes a Grafana filter somewhere. + ids.Data[i].SiteName = site.Desc + " (" + site.Name + ")" } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - return nil, errors.Wrap(errInvalidStatusCode, resp.Status) - } - - if err := json.Unmarshal(body, &response); err != nil { - return nil, err - } - - for i := range response.Data { - response.Data[i].SiteName = site.SiteName - } - - return response.Data, nil + return ids.Data, nil } diff --git a/core/unifi/site.go b/core/unifi/site.go index f521a77e..bfcd46f2 100644 --- a/core/unifi/site.go +++ b/core/unifi/site.go @@ -10,7 +10,7 @@ var ( ) // GetSites returns a list of configured sites on the UniFi controller. -func (u *Unifi) GetSites() (Sites, error) { +func (u *Unifi) GetSites() ([]*Site, error) { var response struct { Data []*Site `json:"data"` } @@ -37,7 +37,7 @@ func (u *Unifi) GetSites() (Sites, error) { } // GetSiteDPI garners dpi data for sites. -func (u *Unifi) GetSiteDPI(sites Sites) ([]*DPITable, error) { +func (u *Unifi) GetSiteDPI(sites []*Site) ([]*DPITable, error) { data := []*DPITable{} for _, site := range sites { @@ -66,9 +66,6 @@ func (u *Unifi) GetSiteDPI(sites Sites) ([]*DPITable, error) { return data, nil } -// Sites is a struct to match Devices and Clients. -type Sites []*Site - // Site represents a site's data. type Site struct { SourceName string `json:"-"` diff --git a/core/unifi/types.go b/core/unifi/types.go index a5a5c504..cd126982 100644 --- a/core/unifi/types.go +++ b/core/unifi/types.go @@ -19,6 +19,8 @@ var ( const ( // APIStatusPath shows Controller version. APIStatusPath string = "/status" + // APIEventPath contains UniFi Event data. + APIEventPath string = "/api/s/%s/stat/event" // APISiteList is the path to the api site list. APISiteList string = "/api/stat/sites" // APISiteDPI is site DPI data. @@ -33,8 +35,8 @@ const ( APILoginPath string = "/api/login" // APILoginPathNew is how we log into UDM 5.12.55+ APILoginPathNew string = "/api/auth/login" - // APIIPSEvents returns Intrusion Detection Systems Events - APIIPSEvents string = "/api/s/%s/stat/ips/event" + // APIEventPathIDS returns Intrusion Detection/Prevention Systems Events + APIEventPathIDS string = "/api/s/%s/stat/ips/event" // APIPrefixNew is the prefix added to the new API paths; except login. duh. APIPrefixNew string = "/proxy/network" )