diff --git a/core/base/global.go b/core/base/global.go index 0414a4f2b..b314797cc 100644 --- a/core/base/global.go +++ b/core/base/global.go @@ -9,7 +9,6 @@ import ( "github.com/safing/portbase/dataroot" "github.com/safing/portbase/info" "github.com/safing/portbase/modules" - "github.com/safing/portbase/modules/subsystems" ) // Default Values (changeable for testing) @@ -66,8 +65,5 @@ func globalPrep() error { // set api listen address api.SetDefaultAPIListenAddress(DefaultAPIListenAddress) - // set subsystem status dir - subsystems.SetDatabaseKeySpace("core:status/subsystems") - return nil } diff --git a/core/config.go b/core/config.go index bb6280be8..2aedff324 100644 --- a/core/config.go +++ b/core/config.go @@ -36,6 +36,7 @@ func registerConfig() error { DefaultValue: defaultDevMode, Annotations: config.Annotations{ config.DisplayOrderAnnotation: 127, + config.CategoryAnnotation: "Development", }, }) if err != nil { @@ -52,6 +53,7 @@ func registerConfig() error { DefaultValue: true, // TODO: turn off by default on unsupported systems Annotations: config.Annotations{ config.DisplayOrderAnnotation: 32, + config.CategoryAnnotation: "General", }, }) if err != nil { diff --git a/firewall/config.go b/firewall/config.go index 6dc4ba1fe..e495001e4 100644 --- a/firewall/config.go +++ b/firewall/config.go @@ -36,6 +36,7 @@ func registerConfig() error { DefaultValue: true, Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionPermanentVerdictsOrder, + config.CategoryAnnotation: "Advanced", }, }) if err != nil { @@ -53,6 +54,7 @@ func registerConfig() error { DefaultValue: true, Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionAskWithSystemNotificationsOrder, + config.CategoryAnnotation: "General", }, }) if err != nil { @@ -70,6 +72,7 @@ func registerConfig() error { Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionAskTimeoutOrder, config.UnitAnnotation: "seconds", + config.CategoryAnnotation: "General", }, }) if err != nil { diff --git a/firewall/filter.go b/firewall/filter.go index 3a2aa1370..b4b3420c1 100644 --- a/firewall/filter.go +++ b/firewall/filter.go @@ -31,6 +31,9 @@ func init() { ExpertiseLevel: config.ExpertiseLevelUser, ReleaseLevel: config.ReleaseLevelBeta, DefaultValue: true, + Annotations: config.Annotations{ + config.CategoryAnnotation: "General", + }, }, ) } diff --git a/firewall/prompt.go b/firewall/prompt.go index bc4f7109d..b05b5297f 100644 --- a/firewall/prompt.go +++ b/firewall/prompt.go @@ -46,18 +46,16 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // // do not save response to profile saveResponse = false } else { - // create new notification - n = (¬ifications.Notification{ - ID: nID, - Type: notifications.Prompt, - Expires: time.Now().Add(nTTL).Unix(), - }) + var ( + msg string + actions []notifications.Action + ) // add message and actions switch { case conn.Inbound: - n.Message = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) - n.AvailableActions = []*notifications.Action{ + msg = fmt.Sprintf("Application %s wants to accept connections from %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) + actions = []notifications.Action{ { ID: permitServingIP, Text: "Permit", @@ -68,8 +66,8 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // }, } case conn.Entity.Domain == "": // direct connection - n.Message = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) - n.AvailableActions = []*notifications.Action{ + msg = fmt.Sprintf("Application %s wants to connect to %s (%d/%d)", conn.Process(), conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) + actions = []notifications.Action{ { ID: permitIP, Text: "Permit", @@ -81,11 +79,11 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // } default: // connection to domain if pkt != nil { - n.Message = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", conn.Process(), conn.Entity.Domain, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) + msg = fmt.Sprintf("Application %s wants to connect to %s (%s %d/%d)", conn.Process(), conn.Entity.Domain, conn.Entity.IP.String(), conn.Entity.Protocol, conn.Entity.Port) } else { - n.Message = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain) + msg = fmt.Sprintf("Application %s wants to connect to %s", conn.Process(), conn.Entity.Domain) } - n.AvailableActions = []*notifications.Action{ + actions = []notifications.Action{ { ID: permitDomainAll, Text: "Permit all", @@ -100,8 +98,8 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit // }, } } - // save new notification - n.Save() + + n = notifications.NotifyPrompt(nID, msg, actions...) } // wait for response/timeout diff --git a/nameserver/takeover.go b/nameserver/takeover.go index ecbea5cf9..51da98309 100644 --- a/nameserver/takeover.go +++ b/nameserver/takeover.go @@ -47,11 +47,10 @@ func checkForConflictingService() error { // wait for a short duration for the other service to shut down time.Sleep(10 * time.Millisecond) - // notify user - (¬ifications.Notification{ - ID: "nameserver-stopped-conflicting-service", - Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid), - }).Save() + notifications.NotifyInfo( + "namserver-stopped-conflicting-service", + fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid), + ) // restart via service-worker logic return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid) diff --git a/process/config.go b/process/config.go index d96d65f20..03e6c78b5 100644 --- a/process/config.go +++ b/process/config.go @@ -22,6 +22,7 @@ func registerConfiguration() error { DefaultValue: true, Annotations: config.Annotations{ config.DisplayOrderAnnotation: 144, + config.CategoryAnnotation: "Development", }, }) if err != nil { diff --git a/profile/config.go b/profile/config.go index 96d421c28..9e5f12a6b 100644 --- a/profile/config.go +++ b/profile/config.go @@ -103,6 +103,7 @@ func registerConfiguration() error { Annotations: config.Annotations{ config.DisplayHintAnnotation: config.DisplayHintOneOf, config.DisplayOrderAnnotation: cfgOptionDefaultActionOrder, + config.CategoryAnnotation: "General", }, PossibleValues: []config.PossibleValue{ { @@ -138,6 +139,7 @@ func registerConfiguration() error { Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionDisableAutoPermitOrder, config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, + config.CategoryAnnotation: "Advanced", }, PossibleValues: status.SecurityLevelValues, }) @@ -175,15 +177,16 @@ Examples: // Endpoint Filter List err = config.Register(&config.Option{ - Name: "Endpoint Filter List", + Name: "Outgoing Rules", Key: CfgOptionEndpointsKey, - Description: "Filter outgoing connections by matching the destination endpoint. Network Scope restrictions still apply.", + Description: "Rules that apply to outgoing network connections. Network Scope restrictions still apply.", Help: filterListHelp, OptType: config.OptTypeStringArray, DefaultValue: []string{}, Annotations: config.Annotations{ config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList, config.DisplayOrderAnnotation: cfgOptionEndpointsOrder, + config.CategoryAnnotation: "Rules", }, ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`, }) @@ -195,15 +198,16 @@ Examples: // Service Endpoint Filter List err = config.Register(&config.Option{ - Name: "Service Endpoint Filter List", + Name: "Incoming Rules", Key: CfgOptionServiceEndpointsKey, - Description: "Filter incoming connections by matching the source endpoint. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.", + Description: "Rules that apply to incoming network connections. Network Scope restrictions and the inbound permission still apply. Also not that the implicit default action of this list is to always block.", Help: filterListHelp, OptType: config.OptTypeStringArray, DefaultValue: []string{"+ Localhost"}, Annotations: config.Annotations{ config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList, config.DisplayOrderAnnotation: cfgOptionServiceEndpointsOrder, + config.CategoryAnnotation: "Rules", }, ValidationRegex: `^(\+|\-) [A-z0-9\.:\-*/]+( [A-z0-9/]+)?$`, }) @@ -223,6 +227,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: "filter list", config.DisplayOrderAnnotation: cfgOptionFilterListsOrder, + config.CategoryAnnotation: "Rules", }, ValidationRegex: `^[a-zA-Z0-9\-]+$`, }) @@ -243,6 +248,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionFilterCNAMEOrder, + config.CategoryAnnotation: "DNS", }, PossibleValues: status.SecurityLevelValues, }) @@ -263,6 +269,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionFilterSubDomainsOrder, + config.CategoryAnnotation: "DNS", }, }) if err != nil { @@ -283,6 +290,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionBlockScopeLocalOrder, + config.CategoryAnnotation: "Scopes & Types", }, }) if err != nil { @@ -302,6 +310,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionBlockScopeLANOrder, + config.CategoryAnnotation: "Scopes & Types", }, }) if err != nil { @@ -321,6 +330,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionBlockScopeInternetOrder, + config.CategoryAnnotation: "Scopes & Types", }, }) if err != nil { @@ -340,6 +350,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionBlockP2POrder, + config.CategoryAnnotation: "Scopes & Types", }, }) if err != nil { @@ -359,6 +370,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionBlockInboundOrder, + config.CategoryAnnotation: "Scopes & Types", }, }) if err != nil { @@ -379,6 +391,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionEnforceSPNOrder, + config.CategoryAnnotation: "Advanced", }, }) if err != nil { @@ -400,6 +413,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionRemoveOutOfScopeDNSOrder, + config.CategoryAnnotation: "DNS", }, }) if err != nil { @@ -421,6 +435,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionRemoveBlockedDNSOrder, + config.CategoryAnnotation: "DNS", }, }) if err != nil { @@ -441,6 +456,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionDomainHeuristicsOrder, + config.CategoryAnnotation: "DNS", }, }) if err != nil { @@ -461,6 +477,7 @@ Examples: Annotations: config.Annotations{ config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, config.DisplayOrderAnnotation: cfgOptionPreventBypassingOrder, + config.CategoryAnnotation: "Advanced", }, }) if err != nil { diff --git a/resolver/config.go b/resolver/config.go index f17a4d91f..552a8acba 100644 --- a/resolver/config.go +++ b/resolver/config.go @@ -57,19 +57,19 @@ var ( cfgOptionNameServersOrder = 0 CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers" - noAssignedNameservers status.SecurityLevelOption + noAssignedNameservers status.SecurityLevelOptionFunc cfgOptionNoAssignedNameserversOrder = 1 CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS" - noMulticastDNS status.SecurityLevelOption + noMulticastDNS status.SecurityLevelOptionFunc cfgOptionNoMulticastDNSOrder = 2 CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols" - noInsecureProtocols status.SecurityLevelOption + noInsecureProtocols status.SecurityLevelOptionFunc cfgOptionNoInsecureProtocolsOrder = 3 CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains" - dontResolveSpecialDomains status.SecurityLevelOption + dontResolveSpecialDomains status.SecurityLevelOptionFunc cfgOptionDontResolveSpecialDomainsOrder = 16 CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate" @@ -113,6 +113,7 @@ Parameters: ValidationRegex: fmt.Sprintf("^(%s|%s|%s)://.*", ServerTypeDoT, ServerTypeDNS, ServerTypeTCP), Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionNameServersOrder, + config.CategoryAnnotation: "Servers", }, }) if err != nil { @@ -122,6 +123,7 @@ Parameters: err = config.Register(&config.Option{ Name: "DNS Server Retry Rate", + Key: CfgOptionNameserverRetryRateKey, Description: "Rate at which to retry failed DNS Servers, in seconds.", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelExpert, @@ -130,6 +132,7 @@ Parameters: Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionNameserverRetryRateOrder, config.UnitAnnotation: "seconds", + config.CategoryAnnotation: "Servers", }, }) if err != nil { @@ -138,42 +141,44 @@ Parameters: nameserverRetryRate = config.Concurrent.GetAsInt(CfgOptionNameserverRetryRateKey, 600) err = config.Register(&config.Option{ - Name: "Do not use Multicast DNS", - Key: CfgOptionNoMulticastDNSKey, - Description: "Multicast DNS queries other devices in the local network", + Name: "Do not use assigned Nameservers", + Key: CfgOptionNoAssignedNameserversKey, + Description: "that were acquired by the network (dhcp) or system", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelExpert, ReleaseLevel: config.ReleaseLevelStable, DefaultValue: status.SecurityLevelsHighAndExtreme, PossibleValues: status.SecurityLevelValues, Annotations: config.Annotations{ - config.DisplayOrderAnnotation: cfgOptionNoMulticastDNSOrder, + config.DisplayOrderAnnotation: cfgOptionNoAssignedNameserversOrder, config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, + config.CategoryAnnotation: "Servers", }, }) if err != nil { return err } - noMulticastDNS = status.ConfigIsActiveConcurrent(CfgOptionNoMulticastDNSKey) + noAssignedNameservers = status.SecurityLevelOption(CfgOptionNoAssignedNameserversKey) err = config.Register(&config.Option{ - Name: "Do not use assigned Nameservers", - Key: CfgOptionNoAssignedNameserversKey, - Description: "that were acquired by the network (dhcp) or system", + Name: "Do not use Multicast DNS", + Key: CfgOptionNoMulticastDNSKey, + Description: "Multicast DNS queries other devices in the local network", OptType: config.OptTypeInt, ExpertiseLevel: config.ExpertiseLevelExpert, ReleaseLevel: config.ReleaseLevelStable, DefaultValue: status.SecurityLevelsHighAndExtreme, PossibleValues: status.SecurityLevelValues, Annotations: config.Annotations{ - config.DisplayOrderAnnotation: cfgOptionNoAssignedNameserversOrder, + config.DisplayOrderAnnotation: cfgOptionNoMulticastDNSOrder, config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, + config.CategoryAnnotation: "Resolving", }, }) if err != nil { return err } - noAssignedNameservers = status.ConfigIsActiveConcurrent(CfgOptionNoAssignedNameserversKey) + noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey) err = config.Register(&config.Option{ Name: "Do not resolve insecurely", @@ -187,12 +192,13 @@ Parameters: Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionNoInsecureProtocolsOrder, config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, + config.CategoryAnnotation: "Resolving", }, }) if err != nil { return err } - noInsecureProtocols = status.ConfigIsActiveConcurrent(CfgOptionNoInsecureProtocolsKey) + noInsecureProtocols = status.SecurityLevelOption(CfgOptionNoInsecureProtocolsKey) err = config.Register(&config.Option{ Name: "Do not resolve special domains", @@ -206,12 +212,13 @@ Parameters: Annotations: config.Annotations{ config.DisplayOrderAnnotation: cfgOptionDontResolveSpecialDomainsOrder, config.DisplayHintAnnotation: status.DisplayHintSecurityLevel, + config.CategoryAnnotation: "Resolving", }, }) if err != nil { return err } - dontResolveSpecialDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveSpecialDomainsKey) + dontResolveSpecialDomains = status.SecurityLevelOption(CfgOptionDontResolveSpecialDomainsKey) return nil } diff --git a/status/autopilot.go b/status/autopilot.go new file mode 100644 index 000000000..63cf388a4 --- /dev/null +++ b/status/autopilot.go @@ -0,0 +1,36 @@ +package status + +import "context" + +var runAutoPilot = make(chan struct{}) + +func triggerAutopilot() { + select { + case runAutoPilot <- struct{}{}: + default: + } +} + +func autoPilot(ctx context.Context) error { + for { + select { + case <-ctx.Done(): + return nil + case <-runAutoPilot: + } + + selected := SelectedSecurityLevel() + mitigation := getHighestMitigationLevel() + + active := SecurityLevelNormal + if selected != SecurityLevelOff { + active = selected + } else if mitigation != SecurityLevelOff { + active = mitigation + } + + setActiveLevel(active) + + pushSystemStatus() + } +} diff --git a/status/const.go b/status/const.go deleted file mode 100644 index 96537903c..000000000 --- a/status/const.go +++ /dev/null @@ -1,56 +0,0 @@ -package status - -import ( - "github.com/safing/portbase/config" -) - -// DisplayHintSecurityLevel is an external option hint for security levels. -// It's meant to be used as a value for config.DisplayHintAnnotation. -const DisplayHintSecurityLevel string = "security level" - -// Security levels -const ( - SecurityLevelOff uint8 = 0 - SecurityLevelNormal uint8 = 1 - SecurityLevelHigh uint8 = 2 - SecurityLevelExtreme uint8 = 4 - - SecurityLevelsNormalAndHigh uint8 = SecurityLevelNormal | SecurityLevelHigh - SecurityLevelsNormalAndExtreme uint8 = SecurityLevelNormal | SecurityLevelExtreme - SecurityLevelsHighAndExtreme uint8 = SecurityLevelHigh | SecurityLevelExtreme - SecurityLevelsAll uint8 = SecurityLevelNormal | SecurityLevelHigh | SecurityLevelExtreme -) - -// SecurityLevelValues defines all possible security levels. -var SecurityLevelValues = []config.PossibleValue{ - { - Name: "Normal", - Value: SecurityLevelsAll, - }, - { - Name: "High", - Value: SecurityLevelsHighAndExtreme, - }, - { - Name: "Extreme", - Value: SecurityLevelExtreme, - }, -} - -// AllSecurityLevelValues is like SecurityLevelValues but also includes Off. -var AllSecurityLevelValues = append([]config.PossibleValue{ - { - Name: "Off", - Value: SecurityLevelOff, - }, -}, - SecurityLevelValues..., -) - -// Status constants -const ( - StatusOff uint8 = 0 - StatusError uint8 = 1 - StatusWarning uint8 = 2 - StatusOk uint8 = 3 -) diff --git a/status/database.go b/status/database.go deleted file mode 100644 index 6b89bc0f0..000000000 --- a/status/database.go +++ /dev/null @@ -1,59 +0,0 @@ -package status - -import ( - "context" - - "github.com/safing/portbase/database" - "github.com/safing/portbase/database/query" - "github.com/safing/portbase/database/record" -) - -const ( - statusDBKey = "core:status/status" -) - -var ( - statusDB = database.NewInterface(nil) - hook *database.RegisteredHook -) - -type statusHook struct { - database.HookBase -} - -// UsesPrePut implements the Hook interface. -func (sh *statusHook) UsesPrePut() bool { - return true -} - -// PrePut implements the Hook interface. -func (sh *statusHook) PrePut(r record.Record) (record.Record, error) { - // record is already locked! - - newStatus, err := EnsureSystemStatus(r) - if err != nil { - return nil, err - } - - // apply applicable settings - if SelectedSecurityLevel() != newStatus.SelectedSecurityLevel { - module.StartWorker("set selected security level", func(_ context.Context) error { - setSelectedSecurityLevel(newStatus.SelectedSecurityLevel) - return nil - }) - } - - // TODO: allow setting of Gate17 status (on/off) - - // return original status - return status, nil -} - -func initStatusHook() (err error) { - hook, err = database.RegisterHook(query.New(statusDBKey), &statusHook{}) - return err -} - -func stopStatusHook() error { - return hook.Cancel() -} diff --git a/status/get-config.go b/status/get-config.go deleted file mode 100644 index b216e4b26..000000000 --- a/status/get-config.go +++ /dev/null @@ -1,33 +0,0 @@ -package status - -import ( - "github.com/safing/portbase/config" -) - -type ( - // SecurityLevelOption defines the returned function by ConfigIsActive. - SecurityLevelOption func(minSecurityLevel uint8) bool -) - -func max(a, b uint8) uint8 { - if a > b { - return a - } - return b -} - -// ConfigIsActive returns whether the given security level dependent config option is on or off. -func ConfigIsActive(name string) SecurityLevelOption { - activeAtLevel := config.GetAsInt(name, int64(SecurityLevelsAll)) - return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 - } -} - -// ConfigIsActiveConcurrent returns whether the given security level dependent config option is on or off and is concurrency safe. -func ConfigIsActiveConcurrent(name string) SecurityLevelOption { - activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll)) - return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 - } -} diff --git a/status/get.go b/status/get.go deleted file mode 100644 index 24d5300c1..000000000 --- a/status/get.go +++ /dev/null @@ -1,20 +0,0 @@ -package status - -import ( - "sync/atomic" -) - -var ( - activeSecurityLevel = new(uint32) - selectedSecurityLevel = new(uint32) -) - -// ActiveSecurityLevel returns the current security level. -func ActiveSecurityLevel() uint8 { - return uint8(atomic.LoadUint32(activeSecurityLevel)) -} - -// SelectedSecurityLevel returns the selected security level. -func SelectedSecurityLevel() uint8 { - return uint8(atomic.LoadUint32(selectedSecurityLevel)) -} diff --git a/status/get_test.go b/status/get_test.go deleted file mode 100644 index 10de0a852..000000000 --- a/status/get_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package status - -import "testing" - -func TestGet(t *testing.T) { - - // only test for panics - // TODO: write real tests - ActiveSecurityLevel() - SelectedSecurityLevel() - option := ConfigIsActive("invalid") - option(0) - option = ConfigIsActiveConcurrent("invalid") - option(0) - -} diff --git a/status/mitigation.go b/status/mitigation.go new file mode 100644 index 000000000..5d103eb44 --- /dev/null +++ b/status/mitigation.go @@ -0,0 +1,60 @@ +package status + +import ( + "sync" + + "github.com/safing/portbase/log" +) + +type knownThreats struct { + sync.RWMutex + // active threats and their recommended mitigation level + list map[string]uint8 +} + +var threats = &knownThreats{ + list: make(map[string]uint8), +} + +// SetMitigationLevel sets the mitigation level for id +// to mitigation. If mitigation is SecurityLevelOff the +// mitigation record will be removed. If mitigation is +// an invalid level the call to SetMitigationLevel is a +// no-op. +func SetMitigationLevel(id string, mitigation uint8) { + if !IsValidSecurityLevel(mitigation) { + log.Warningf("tried to set invalid mitigation level %d for threat %s", mitigation, id) + return + } + + defer triggerAutopilot() + + threats.Lock() + defer threats.Unlock() + if mitigation == 0 { + delete(threats.list, id) + } else { + threats.list[id] = mitigation + } +} + +// DeleteMitigationLevel deletes the mitigation level for id. +func DeleteMitigationLevel(id string) { + SetMitigationLevel(id, SecurityLevelOff) +} + +// getHighestMitigationLevel returns the highest mitigation +// level set on a threat. +func getHighestMitigationLevel() uint8 { + threats.RLock() + defer threats.RUnlock() + + var level uint8 + for _, lvl := range threats.list { + if lvl > level { + level = lvl + } + } + + return level +} diff --git a/status/module.go b/status/module.go index 126077672..2dbb13a52 100644 --- a/status/module.go +++ b/status/module.go @@ -1,9 +1,10 @@ package status import ( - "github.com/safing/portbase/database" - "github.com/safing/portbase/log" + "context" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/netenv" ) var ( @@ -11,56 +12,25 @@ var ( ) func init() { - module = modules.Register("status", nil, start, stop, "base") + module = modules.Register("status", nil, start, nil, "base") } func start() error { - err := initSystemStatus() - if err != nil { - return err - } - - err = startNetEnvHooking() + module.StartWorker("auto-pilot", autoPilot) + triggerAutopilot() + + err := module.RegisterEventHook( + "netenv", + netenv.OnlineStatusChangedEvent, + "update online status in system status", + func(_ context.Context, _ interface{}) error { + triggerAutopilot() + return nil + }, + ) if err != nil { return err } - status.Save() - - return initStatusHook() -} - -func initSystemStatus() error { - // load status from database - r, err := statusDB.Get(statusDBKey) - switch err { - case nil: - loadedStatus, err := EnsureSystemStatus(r) - if err != nil { - log.Criticalf("status: failed to unwrap system status: %s", err) - } else { - status = loadedStatus - } - case database.ErrNotFound: - // create new status - default: - log.Criticalf("status: failed to load system status: %s", err) - } - - status.Lock() - defer status.Unlock() - - // load status into atomic getters - atomicUpdateSelectedSecurityLevel(status.SelectedSecurityLevel) - - // update status - status.updateThreatMitigationLevel() - status.autopilot() - status.updateOnlineStatus() - return nil } - -func stop() error { - return stopStatusHook() -} diff --git a/status/netenv.go b/status/netenv.go deleted file mode 100644 index 8d57c6153..000000000 --- a/status/netenv.go +++ /dev/null @@ -1,28 +0,0 @@ -package status - -import ( - "context" - - "github.com/safing/portmaster/netenv" -) - -// startNetEnvHooking starts the listener for online status changes. -func startNetEnvHooking() error { - return module.RegisterEventHook( - "netenv", - netenv.OnlineStatusChangedEvent, - "update online status in system status", - func(_ context.Context, _ interface{}) error { - status.Lock() - status.updateOnlineStatus() - status.Unlock() - status.Save() - return nil - }, - ) -} - -func (s *SystemStatus) updateOnlineStatus() { - s.OnlineStatus = netenv.GetOnlineStatus() - s.CaptivePortal = netenv.GetCaptivePortal() -} diff --git a/status/provider.go b/status/provider.go new file mode 100644 index 000000000..130972db3 --- /dev/null +++ b/status/provider.go @@ -0,0 +1,93 @@ +package status + +import ( + "fmt" + + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/runtime" + "github.com/safing/portmaster/netenv" +) + +var ( + pushUpdate runtime.PushFunc +) + +func setupRuntimeProvider() (err error) { + // register the system status getter + // + statusProvider := runtime.SimpleValueGetterFunc(func(_ string) ([]record.Record, error) { + return []record.Record{buildSystemStatus()}, nil + }) + pushUpdate, err = runtime.Register("system/status", statusProvider) + if err != nil { + return err + } + + // register the selected security level setter + // + levelProvider := runtime.SimpleValueSetterFunc(setSelectedSecurityLevel) + _, err = runtime.Register("system/security-level", levelProvider) + if err != nil { + return err + } + + return nil +} + +// setSelectedSecurityLevel updates the selected security level +func setSelectedSecurityLevel(r record.Record) (record.Record, error) { + var upd *SelectedSecurityLevelRecord + if r.IsWrapped() { + upd = new(SelectedSecurityLevelRecord) + if err := record.Unwrap(r, upd); err != nil { + return nil, err + } + } else { + // TODO(ppacher): this can actually never happen + // as we're write-only and ValueProvider.Set() should + // only ever be called from the HTTP API (so r must be wrapped). + // Though, make sure we handle the case as well ... + var ok bool + upd, ok = r.(*SelectedSecurityLevelRecord) + if !ok { + return nil, fmt.Errorf("expected *SelectedSecurityLevelRecord but got %T", r) + } + } + + if !IsValidSecurityLevel(upd.SelectedSecurityLevel) { + return nil, fmt.Errorf("invalid security level: %d", upd.SelectedSecurityLevel) + } + + if SelectedSecurityLevel() != upd.SelectedSecurityLevel { + setSelectedLevel(upd.SelectedSecurityLevel) + triggerAutopilot() + } + + return r, nil +} + +// buildSystemStatus build a new system status record. +func buildSystemStatus() *SystemStatusRecord { + status := &SystemStatusRecord{ + ActiveSecurityLevel: ActiveSecurityLevel(), + SelectedSecurityLevel: SelectedSecurityLevel(), + ThreatMitigationLevel: getHighestMitigationLevel(), + CaptivePortal: netenv.GetCaptivePortal(), + OnlineStatus: netenv.GetOnlineStatus(), + } + + status.CreateMeta() + status.SetKey("runtime:system/status") + + return status +} + +// pushSystemStatus pushes a new system status via +// the runtime database. +func pushSystemStatus() { + if pushUpdate == nil { + return + } + + pushUpdate(buildSystemStatus()) +} diff --git a/status/records.go b/status/records.go new file mode 100644 index 000000000..73801c62b --- /dev/null +++ b/status/records.go @@ -0,0 +1,42 @@ +package status + +import ( + "sync" + + "github.com/safing/portbase/database/record" + "github.com/safing/portmaster/netenv" +) + +// SystemStatusRecord describes the overall status of the Portmaster. +// It's a read-only record exposed via runtime:system/status. +type SystemStatusRecord struct { + record.Base + sync.Mutex + + // ActiveSecurityLevel holds the currently + // active security level. + ActiveSecurityLevel uint8 + // SelectedSecurityLevel holds the security level + // as selected by the user. + SelectedSecurityLevel uint8 + // ThreatMitigationLevel holds the security level + // as selected by the auto-pilot. + ThreatMitigationLevel uint8 + // OnlineStatus holds the current online status as + // seen by the netenv package. + OnlineStatus netenv.OnlineStatus + // CaptivePortal holds all information about the captive + // portal of the network the portmaster is currently + // connected to, if any. + CaptivePortal *netenv.CaptivePortal +} + +// SelectedSecurityLevelRecord is used as a dummy record.Record +// to provide a simply runtime-configuration for the user. +// It is write-only and exposed at runtime:system/security-level +type SelectedSecurityLevelRecord struct { + record.Base + sync.Mutex + + SelectedSecurityLevel uint8 +} diff --git a/status/security_level.go b/status/security_level.go new file mode 100644 index 000000000..ea8badb79 --- /dev/null +++ b/status/security_level.go @@ -0,0 +1,114 @@ +package status + +import "github.com/safing/portbase/config" + +type ( + // SecurityLevelOptionFunc can be called with a minimum security level + // and returns whether or not a given security option is enabled or + // not. + // Use SecurityLevelOption() to get a SecurityLevelOptionFunc for a + // specific option. + SecurityLevelOptionFunc func(minSecurityLevel uint8) bool +) + +// DisplayHintSecurityLevel is an external option hint for security levels. +// It's meant to be used as a value for config.DisplayHintAnnotation. +const DisplayHintSecurityLevel string = "security level" + +// Security levels +const ( + SecurityLevelOff uint8 = 0 + SecurityLevelNormal uint8 = 1 + SecurityLevelHigh uint8 = 2 + SecurityLevelExtreme uint8 = 4 + + SecurityLevelsNormalAndHigh uint8 = SecurityLevelNormal | SecurityLevelHigh + SecurityLevelsNormalAndExtreme uint8 = SecurityLevelNormal | SecurityLevelExtreme + SecurityLevelsHighAndExtreme uint8 = SecurityLevelHigh | SecurityLevelExtreme + SecurityLevelsAll uint8 = SecurityLevelNormal | SecurityLevelHigh | SecurityLevelExtreme +) + +// SecurityLevelValues defines all possible security levels. +var SecurityLevelValues = []config.PossibleValue{ + { + Name: "Normal", + Value: SecurityLevelsAll, + }, + { + Name: "High", + Value: SecurityLevelsHighAndExtreme, + }, + { + Name: "Extreme", + Value: SecurityLevelExtreme, + }, +} + +// AllSecurityLevelValues is like SecurityLevelValues but also includes Off. +var AllSecurityLevelValues = append([]config.PossibleValue{ + { + Name: "Off", + Value: SecurityLevelOff, + }, +}, + SecurityLevelValues..., +) + +// IsValidSecurityLevel returns true if level is a valid, +// single security level. Level is also invalid if it's a +// bitmask with more that one security level set. +func IsValidSecurityLevel(level uint8) bool { + return level == SecurityLevelOff || + level == SecurityLevelNormal || + level == SecurityLevelHigh || + level == SecurityLevelExtreme +} + +// IsValidSecurityLevelMask returns true if level is a valid +// security level mask. It's like IsValidSecurityLevel but +// also allows bitmask combinations. +func IsValidSecurityLevelMask(level uint8) bool { + return level <= 7 +} + +func max(a, b uint8) uint8 { + if a > b { + return a + } + return b +} + +// SecurityLevelOption returns a function to check if the option +// identified by name is active at a given minimum security level. +// The returned function is safe for concurrent use with configuration +// updates. +func SecurityLevelOption(name string) SecurityLevelOptionFunc { + activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelsAll)) + return func(minSecurityLevel uint8) bool { + return uint8(activeAtLevel())&max(ActiveSecurityLevel(), minSecurityLevel) > 0 + } +} + +// SecurityLevelString returns the given security level as a string. +func SecurityLevelString(level uint8) string { + switch level { + case SecurityLevelOff: + return "Off" + case SecurityLevelNormal: + return "Normal" + case SecurityLevelHigh: + return "High" + case SecurityLevelExtreme: + return "Extreme" + case SecurityLevelsNormalAndHigh: + return "Normal and High" + case SecurityLevelsNormalAndExtreme: + return "Normal and Extreme" + case SecurityLevelsHighAndExtreme: + return "High and Extreme" + case SecurityLevelsAll: + return "Normal, High and Extreme" + default: + return "INVALID" + } +} diff --git a/status/set.go b/status/set.go deleted file mode 100644 index 348998814..000000000 --- a/status/set.go +++ /dev/null @@ -1,54 +0,0 @@ -package status - -import ( - "sync/atomic" - - "github.com/safing/portbase/log" -) - -// autopilot automatically adjusts the security level as needed. -func (s *SystemStatus) autopilot() { - // check if users is overruling - if s.SelectedSecurityLevel > SecurityLevelOff { - s.ActiveSecurityLevel = s.SelectedSecurityLevel - atomicUpdateActiveSecurityLevel(s.SelectedSecurityLevel) - return - } - - // update active security level - switch s.ThreatMitigationLevel { - case SecurityLevelOff: - s.ActiveSecurityLevel = SecurityLevelNormal - atomicUpdateActiveSecurityLevel(SecurityLevelNormal) - case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: - s.ActiveSecurityLevel = s.ThreatMitigationLevel - atomicUpdateActiveSecurityLevel(s.ThreatMitigationLevel) - default: - log.Errorf("status: threat mitigation level is set to invalid value: %d", s.ThreatMitigationLevel) - } -} - -// setSelectedSecurityLevel sets the selected security level. -func setSelectedSecurityLevel(level uint8) { - switch level { - case SecurityLevelOff, SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: - status.Lock() - - status.SelectedSecurityLevel = level - atomicUpdateSelectedSecurityLevel(level) - status.autopilot() - - status.Unlock() - status.Save() - default: - log.Errorf("status: tried to set selected security level to invalid value: %d", level) - } -} - -func atomicUpdateActiveSecurityLevel(level uint8) { - atomic.StoreUint32(activeSecurityLevel, uint32(level)) -} - -func atomicUpdateSelectedSecurityLevel(level uint8) { - atomic.StoreUint32(selectedSecurityLevel, uint32(level)) -} diff --git a/status/set_test.go b/status/set_test.go deleted file mode 100644 index 7bb70f41a..000000000 --- a/status/set_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package status - -import "testing" - -func TestSet(t *testing.T) { - - // only test for panics - // TODO: write real tests - setSelectedSecurityLevel(0) - -} diff --git a/status/state.go b/status/state.go new file mode 100644 index 000000000..a3fb079a7 --- /dev/null +++ b/status/state.go @@ -0,0 +1,30 @@ +package status + +import ( + "sync/atomic" +) + +var ( + activeLevel = new(uint32) + selectedLevel = new(uint32) +) + +func setActiveLevel(lvl uint8) { + atomic.StoreUint32(activeLevel, uint32(lvl)) +} + +func setSelectedLevel(lvl uint8) { + atomic.StoreUint32(selectedLevel, uint32(lvl)) +} + +// ActiveSecurityLevel returns the currently active security +// level. +func ActiveSecurityLevel() uint8 { + return uint8(atomic.LoadUint32(activeLevel)) +} + +// SelectedSecurityLevel returns the security level as selected +// by the user. +func SelectedSecurityLevel() uint8 { + return uint8(atomic.LoadUint32(selectedLevel)) +} diff --git a/status/status.go b/status/status.go deleted file mode 100644 index fb2ad0b9a..000000000 --- a/status/status.go +++ /dev/null @@ -1,113 +0,0 @@ -package status - -import ( - "context" - "fmt" - "sync" - - "github.com/safing/portmaster/netenv" - - "github.com/safing/portbase/database/record" - "github.com/safing/portbase/log" -) - -var ( - status *SystemStatus -) - -func init() { - status = &SystemStatus{ - Threats: make(map[string]*Threat), - } - status.SetKey(statusDBKey) -} - -// SystemStatus saves basic information about the current system status. -//nolint:maligned // TODO -type SystemStatus struct { - record.Base - sync.Mutex - - ActiveSecurityLevel uint8 - SelectedSecurityLevel uint8 - - OnlineStatus netenv.OnlineStatus - CaptivePortal *netenv.CaptivePortal - - ThreatMitigationLevel uint8 - Threats map[string]*Threat -} - -// SaveAsync saves the SystemStatus to the database asynchronously. -func (s *SystemStatus) SaveAsync() { - module.StartWorker("save system status", func(_ context.Context) error { - s.Save() - return nil - }) -} - -// Save saves the SystemStatus to the database. -func (s *SystemStatus) Save() { - err := statusDB.Put(s) - if err != nil { - log.Errorf("status: could not save status to database: %s", err) - } -} - -// EnsureSystemStatus ensures that the given record is of type SystemStatus and unwraps it, if needed. -func EnsureSystemStatus(r record.Record) (*SystemStatus, error) { - // unwrap - if r.IsWrapped() { - // only allocate a new struct, if we need it - new := &SystemStatus{} - err := record.Unwrap(r, new) - if err != nil { - return nil, err - } - return new, nil - } - - // or adjust type - new, ok := r.(*SystemStatus) - if !ok { - return nil, fmt.Errorf("record not of type *SystemStatus, but %T", r) - } - return new, nil -} - -// FmtActiveSecurityLevel returns the current security level as a string. -func FmtActiveSecurityLevel() string { - status.Lock() - mitigationLevel := status.ThreatMitigationLevel - status.Unlock() - active := ActiveSecurityLevel() - s := FmtSecurityLevel(active) - if mitigationLevel > 0 && active != mitigationLevel { - s += "*" - } - return s -} - -// FmtSecurityLevel returns the given security level as a string. -func FmtSecurityLevel(level uint8) string { - switch level { - case SecurityLevelOff: - return "Off" - case SecurityLevelNormal: - return "Normal" - case SecurityLevelHigh: - return "High" - case SecurityLevelExtreme: - return "Extreme" - case SecurityLevelsNormalAndHigh: - return "Normal and High" - case SecurityLevelsNormalAndExtreme: - return "Normal and Extreme" - case SecurityLevelsHighAndExtreme: - return "High and Extreme" - case SecurityLevelsAll: - return "Normal, High and Extreme" - default: - return "INVALID" - } -} diff --git a/status/status_test.go b/status/status_test.go deleted file mode 100644 index 818b5801f..000000000 --- a/status/status_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package status - -import "testing" - -func TestStatus(t *testing.T) { - - setSelectedSecurityLevel(SecurityLevelOff) - if FmtActiveSecurityLevel() != "Normal" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelNormal) - AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelHigh}) - if FmtActiveSecurityLevel() != "Normal*" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelHigh) - if FmtActiveSecurityLevel() != "High" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelHigh) - AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelExtreme}) - if FmtActiveSecurityLevel() != "High*" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - - setSelectedSecurityLevel(SecurityLevelExtreme) - if FmtActiveSecurityLevel() != "Extreme" { - t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) - } - -} diff --git a/status/threat.go b/status/threat.go index 632ce8354..4b70bfabd 100644 --- a/status/threat.go +++ b/status/threat.go @@ -1,73 +1,131 @@ package status import ( - "strings" - "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/notifications" ) -// Threat describes a detected threat. +// Threat represents a threat to the system. +// A threat is basically a notification with strong +// typed EventData. Use the methods expored on Threat +// to manipulate the EventData field and push updates +// of the notification. +// Do not use EventData directly! type Threat struct { - ID string // A unique ID chosen by reporting module (eg. modulePrefix-incident) to periodically check threat existence - Name string // Descriptive (human readable) name for detected threat - Description string // Simple description - AdditionalData interface{} // Additional data a module wants to make available for the user - MitigationLevel uint8 // Recommended Security Level to switch to for mitigation - Started int64 - Ended int64 - // TODO: add locking + *notifications.Notification +} + +// ThreatPayload holds threat related information. +type ThreatPayload struct { + // MitigationLevel holds the recommended security + // level to mitigate the threat. + MitigationLevel uint8 + // Started holds the UNIX epoch timestamp in seconds + // at which the threat has been detected the first time. + Started int64 + // Ended holds the UNIX epoch timestamp in seconds + // at which the threat has been detected the last time. + Ended int64 + // Data may holds threat-specific data. + Data interface{} +} + +// NewThreat returns a new threat. Note that the +// threat only gets published once Publish is called. +// +// Example: +// +// threat := NewThreat("portscan", "Someone is scanning you"). +// SetData(portscanResult). +// SetMitigationLevel(SecurityLevelExtreme). +// Publish() +// +// // Once you're done, delete the threat +// threat.Delete().Publish() +// +func NewThreat(id, msg string) *Threat { + t := &Threat{ + Notification: ¬ifications.Notification{ + EventID: id, + Message: msg, + Type: notifications.Warning, + State: notifications.Active, + }, + } + t.threatData().Started = time.Now().Unix() + + return t } -// AddOrUpdateThreat adds or updates a new threat in the system status. -func AddOrUpdateThreat(new *Threat) { - status.Lock() - defer status.Unlock() +// SetData sets the data member of the threat payload. +func (t *Threat) SetData(data interface{}) *Threat { + t.Lock() + defer t.Unlock() - status.Threats[new.ID] = new - status.updateThreatMitigationLevel() - status.autopilot() + t.threatData().Data = data + return t +} + +// SetMitigationLevel sets the mitigation level of the +// threat data. +func (t *Threat) SetMitigationLevel(lvl uint8) *Threat { + t.Lock() + defer t.Unlock() - status.SaveAsync() + t.threatData().MitigationLevel = lvl + return t } -// DeleteThreat deletes a threat from the system status. -func DeleteThreat(id string) { - status.Lock() - defer status.Unlock() +// Delete sets the ended timestamp of the threat. +func (t *Threat) Delete() *Threat { + t.Lock() + defer t.Unlock() - delete(status.Threats, id) - status.updateThreatMitigationLevel() - status.autopilot() + t.threatData().Ended = time.Now().Unix() - status.SaveAsync() + return t } -// GetThreats returns all threats who's IDs are prefixed by the given string, and also a locker for editing them. -func GetThreats(idPrefix string) ([]*Threat, sync.Locker) { - status.Lock() - defer status.Unlock() +// Payload returns a copy of the threat payload. +func (t *Threat) Payload() ThreatPayload { + t.Lock() + defer t.Unlock() + + return *t.threatData() // creates a copy +} - var exportedThreats []*Threat - for id, threat := range status.Threats { - if strings.HasPrefix(id, idPrefix) { - exportedThreats = append(exportedThreats, threat) - } +// Publish publishes the current threat. +// Publish should always be called when changes to +// the threat are recorded. +func (t *Threat) Publish() *Threat { + data := t.Payload() + if data.Ended > 0 { + DeleteMitigationLevel(t.EventID) + } else { + SetMitigationLevel(t.EventID, data.MitigationLevel) } - return exportedThreats, &status.Mutex + t.Save() + + return t } -func (s *SystemStatus) updateThreatMitigationLevel() { - // get highest mitigationLevel - var mitigationLevel uint8 - for _, threat := range s.Threats { - switch threat.MitigationLevel { - case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme: - if threat.MitigationLevel > mitigationLevel { - mitigationLevel = threat.MitigationLevel - } - } +// threatData returns the threat payload associated with this +// threat. If not data has been created yet a new ThreatPayload +// is attached to t and returned. The caller must make sure to +// hold appropriate locks when working with the returned payload. +func (t *Threat) threatData() *ThreatPayload { + if t.EventData == nil { + t.EventData = new(ThreatPayload) + } + + payload, ok := t.EventData.(*ThreatPayload) + if !ok { + log.Warningf("unexpected type %T in thread notification payload", t.EventData) + return new(ThreatPayload) } - // set new ThreatMitigationLevel - s.ThreatMitigationLevel = mitigationLevel + return payload } diff --git a/updates/config.go b/updates/config.go index 5c81ffa44..61789f541 100644 --- a/updates/config.go +++ b/updates/config.go @@ -44,6 +44,7 @@ func registerConfig() error { Annotations: config.Annotations{ config.DisplayOrderAnnotation: 1, config.DisplayHintAnnotation: config.DisplayHintOneOf, + config.CategoryAnnotation: "Expertise & Release", }, }) if err != nil { @@ -61,6 +62,7 @@ func registerConfig() error { DefaultValue: false, Annotations: config.Annotations{ config.DisplayOrderAnnotation: 64, + config.CategoryAnnotation: "General", }, }) if err != nil {