mirror of
https://github.com/safing/portmaster
synced 2025-09-02 18:49:14 +00:00
Refactor status package to use portbase/runtime.
Refactor the status package to use portbase/runtime and make system status readonly. Also adapts the code base to the new portbase/notifications package.
This commit is contained in:
parent
52c4cfe11d
commit
a5e3f7ff37
22 changed files with 527 additions and 554 deletions
|
@ -9,7 +9,6 @@ import (
|
||||||
"github.com/safing/portbase/dataroot"
|
"github.com/safing/portbase/dataroot"
|
||||||
"github.com/safing/portbase/info"
|
"github.com/safing/portbase/info"
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
"github.com/safing/portbase/modules/subsystems"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default Values (changeable for testing)
|
// Default Values (changeable for testing)
|
||||||
|
@ -66,8 +65,5 @@ func globalPrep() error {
|
||||||
// set api listen address
|
// set api listen address
|
||||||
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
||||||
|
|
||||||
// set subsystem status dir
|
|
||||||
subsystems.SetDatabaseKeySpace("core:status/subsystems")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,18 +46,16 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit //
|
||||||
// do not save response to profile
|
// do not save response to profile
|
||||||
saveResponse = false
|
saveResponse = false
|
||||||
} else {
|
} else {
|
||||||
// create new notification
|
var (
|
||||||
n = (¬ifications.Notification{
|
msg string
|
||||||
ID: nID,
|
actions []notifications.Action
|
||||||
Type: notifications.Prompt,
|
)
|
||||||
Expires: time.Now().Add(nTTL).Unix(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// add message and actions
|
// add message and actions
|
||||||
switch {
|
switch {
|
||||||
case conn.Inbound:
|
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)
|
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)
|
||||||
n.AvailableActions = []*notifications.Action{
|
actions = []notifications.Action{
|
||||||
{
|
{
|
||||||
ID: permitServingIP,
|
ID: permitServingIP,
|
||||||
Text: "Permit",
|
Text: "Permit",
|
||||||
|
@ -68,8 +66,8 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit //
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case conn.Entity.Domain == "": // direct connection
|
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)
|
msg = 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{
|
actions = []notifications.Action{
|
||||||
{
|
{
|
||||||
ID: permitIP,
|
ID: permitIP,
|
||||||
Text: "Permit",
|
Text: "Permit",
|
||||||
|
@ -81,11 +79,11 @@ func prompt(conn *network.Connection, pkt packet.Packet) { //nolint:gocognit //
|
||||||
}
|
}
|
||||||
default: // connection to domain
|
default: // connection to domain
|
||||||
if pkt != nil {
|
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 {
|
} 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,
|
ID: permitDomainAll,
|
||||||
Text: "Permit all",
|
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
|
// wait for response/timeout
|
||||||
|
|
|
@ -47,11 +47,10 @@ func checkForConflictingService() error {
|
||||||
// wait for a short duration for the other service to shut down
|
// wait for a short duration for the other service to shut down
|
||||||
time.Sleep(10 * time.Millisecond)
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
// notify user
|
notifications.NotifyInfo(
|
||||||
(¬ifications.Notification{
|
"namserver-stopped-conflicting-service",
|
||||||
ID: "nameserver-stopped-conflicting-service",
|
fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
|
||||||
Message: fmt.Sprintf("Portmaster stopped a conflicting name service (pid %d) to gain required system integration.", pid),
|
)
|
||||||
}).Save()
|
|
||||||
|
|
||||||
// restart via service-worker logic
|
// restart via service-worker logic
|
||||||
return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid)
|
return fmt.Errorf("%w: stopped conflicting name service with pid %d", modules.ErrRestartNow, pid)
|
||||||
|
|
|
@ -57,19 +57,19 @@ var (
|
||||||
cfgOptionNameServersOrder = 0
|
cfgOptionNameServersOrder = 0
|
||||||
|
|
||||||
CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers"
|
CfgOptionNoAssignedNameserversKey = "dns/noAssignedNameservers"
|
||||||
noAssignedNameservers status.SecurityLevelOption
|
noAssignedNameservers status.SecurityLevelOptionFunc
|
||||||
cfgOptionNoAssignedNameserversOrder = 1
|
cfgOptionNoAssignedNameserversOrder = 1
|
||||||
|
|
||||||
CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS"
|
CfgOptionNoMulticastDNSKey = "dns/noMulticastDNS"
|
||||||
noMulticastDNS status.SecurityLevelOption
|
noMulticastDNS status.SecurityLevelOptionFunc
|
||||||
cfgOptionNoMulticastDNSOrder = 2
|
cfgOptionNoMulticastDNSOrder = 2
|
||||||
|
|
||||||
CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols"
|
CfgOptionNoInsecureProtocolsKey = "dns/noInsecureProtocols"
|
||||||
noInsecureProtocols status.SecurityLevelOption
|
noInsecureProtocols status.SecurityLevelOptionFunc
|
||||||
cfgOptionNoInsecureProtocolsOrder = 3
|
cfgOptionNoInsecureProtocolsOrder = 3
|
||||||
|
|
||||||
CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains"
|
CfgOptionDontResolveSpecialDomainsKey = "dns/dontResolveSpecialDomains"
|
||||||
dontResolveSpecialDomains status.SecurityLevelOption
|
dontResolveSpecialDomains status.SecurityLevelOptionFunc
|
||||||
cfgOptionDontResolveSpecialDomainsOrder = 16
|
cfgOptionDontResolveSpecialDomainsOrder = 16
|
||||||
|
|
||||||
CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate"
|
CfgOptionNameserverRetryRateKey = "dns/nameserverRetryRate"
|
||||||
|
@ -122,6 +122,7 @@ Parameters:
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "DNS Server Retry Rate",
|
Name: "DNS Server Retry Rate",
|
||||||
|
Key: CfgOptionNameserverRetryRateKey,
|
||||||
Description: "Rate at which to retry failed DNS Servers, in seconds.",
|
Description: "Rate at which to retry failed DNS Servers, in seconds.",
|
||||||
OptType: config.OptTypeInt,
|
OptType: config.OptTypeInt,
|
||||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||||
|
@ -154,7 +155,7 @@ Parameters:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
noMulticastDNS = status.ConfigIsActiveConcurrent(CfgOptionNoMulticastDNSKey)
|
noMulticastDNS = status.SecurityLevelOption(CfgOptionNoMulticastDNSKey)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not use assigned Nameservers",
|
Name: "Do not use assigned Nameservers",
|
||||||
|
@ -173,7 +174,7 @@ Parameters:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
noAssignedNameservers = status.ConfigIsActiveConcurrent(CfgOptionNoAssignedNameserversKey)
|
noAssignedNameservers = status.SecurityLevelOption(CfgOptionNoAssignedNameserversKey)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not resolve insecurely",
|
Name: "Do not resolve insecurely",
|
||||||
|
@ -192,7 +193,7 @@ Parameters:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
noInsecureProtocols = status.ConfigIsActiveConcurrent(CfgOptionNoInsecureProtocolsKey)
|
noInsecureProtocols = status.SecurityLevelOption(CfgOptionNoInsecureProtocolsKey)
|
||||||
|
|
||||||
err = config.Register(&config.Option{
|
err = config.Register(&config.Option{
|
||||||
Name: "Do not resolve special domains",
|
Name: "Do not resolve special domains",
|
||||||
|
@ -211,7 +212,7 @@ Parameters:
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dontResolveSpecialDomains = status.ConfigIsActiveConcurrent(CfgOptionDontResolveSpecialDomainsKey)
|
dontResolveSpecialDomains = status.SecurityLevelOption(CfgOptionDontResolveSpecialDomainsKey)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
36
status/autopilot.go
Normal file
36
status/autopilot.go
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
60
status/mitigation.go
Normal file
60
status/mitigation.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/safing/portbase/database"
|
"context"
|
||||||
"github.com/safing/portbase/log"
|
|
||||||
"github.com/safing/portbase/modules"
|
"github.com/safing/portbase/modules"
|
||||||
|
"github.com/safing/portmaster/netenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -11,56 +12,25 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
module = modules.Register("status", nil, start, stop, "base")
|
module = modules.Register("status", nil, start, nil, "base")
|
||||||
}
|
}
|
||||||
|
|
||||||
func start() error {
|
func start() error {
|
||||||
err := initSystemStatus()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = startNetEnvHooking()
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() error {
|
|
||||||
return stopStatusHook()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
93
status/provider.go
Normal file
93
status/provider.go
Normal file
|
@ -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())
|
||||||
|
}
|
42
status/records.go
Normal file
42
status/records.go
Normal file
|
@ -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
|
||||||
|
}
|
114
status/security_level.go
Normal file
114
status/security_level.go
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package status
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestSet(t *testing.T) {
|
|
||||||
|
|
||||||
// only test for panics
|
|
||||||
// TODO: write real tests
|
|
||||||
setSelectedSecurityLevel(0)
|
|
||||||
|
|
||||||
}
|
|
30
status/state.go
Normal file
30
status/state.go
Normal file
|
@ -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))
|
||||||
|
}
|
113
status/status.go
113
status/status.go
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
156
status/threat.go
156
status/threat.go
|
@ -1,73 +1,131 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"time"
|
||||||
"sync"
|
|
||||||
|
"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 {
|
type Threat struct {
|
||||||
ID string // A unique ID chosen by reporting module (eg. modulePrefix-incident) to periodically check threat existence
|
*notifications.Notification
|
||||||
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
|
// ThreatPayload holds threat related information.
|
||||||
MitigationLevel uint8 // Recommended Security Level to switch to for mitigation
|
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
|
Started int64
|
||||||
|
// Ended holds the UNIX epoch timestamp in seconds
|
||||||
|
// at which the threat has been detected the last time.
|
||||||
Ended int64
|
Ended int64
|
||||||
// TODO: add locking
|
// Data may holds threat-specific data.
|
||||||
|
Data interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddOrUpdateThreat adds or updates a new threat in the system status.
|
// NewThreat returns a new threat. Note that the
|
||||||
func AddOrUpdateThreat(new *Threat) {
|
// threat only gets published once Publish is called.
|
||||||
status.Lock()
|
//
|
||||||
defer status.Unlock()
|
// 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()
|
||||||
|
|
||||||
status.Threats[new.ID] = new
|
return t
|
||||||
status.updateThreatMitigationLevel()
|
|
||||||
status.autopilot()
|
|
||||||
|
|
||||||
status.SaveAsync()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteThreat deletes a threat from the system status.
|
// SetData sets the data member of the threat payload.
|
||||||
func DeleteThreat(id string) {
|
func (t *Threat) SetData(data interface{}) *Threat {
|
||||||
status.Lock()
|
t.Lock()
|
||||||
defer status.Unlock()
|
defer t.Unlock()
|
||||||
|
|
||||||
delete(status.Threats, id)
|
t.threatData().Data = data
|
||||||
status.updateThreatMitigationLevel()
|
return t
|
||||||
status.autopilot()
|
|
||||||
|
|
||||||
status.SaveAsync()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetThreats returns all threats who's IDs are prefixed by the given string, and also a locker for editing them.
|
// SetMitigationLevel sets the mitigation level of the
|
||||||
func GetThreats(idPrefix string) ([]*Threat, sync.Locker) {
|
// threat data.
|
||||||
status.Lock()
|
func (t *Threat) SetMitigationLevel(lvl uint8) *Threat {
|
||||||
defer status.Unlock()
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
var exportedThreats []*Threat
|
t.threatData().MitigationLevel = lvl
|
||||||
for id, threat := range status.Threats {
|
return t
|
||||||
if strings.HasPrefix(id, idPrefix) {
|
|
||||||
exportedThreats = append(exportedThreats, threat)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return exportedThreats, &status.Mutex
|
// Delete sets the ended timestamp of the threat.
|
||||||
|
func (t *Threat) Delete() *Threat {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
t.threatData().Ended = time.Now().Unix()
|
||||||
|
|
||||||
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SystemStatus) updateThreatMitigationLevel() {
|
// Payload returns a copy of the threat payload.
|
||||||
// get highest mitigationLevel
|
func (t *Threat) Payload() ThreatPayload {
|
||||||
var mitigationLevel uint8
|
t.Lock()
|
||||||
for _, threat := range s.Threats {
|
defer t.Unlock()
|
||||||
switch threat.MitigationLevel {
|
|
||||||
case SecurityLevelNormal, SecurityLevelHigh, SecurityLevelExtreme:
|
return *t.threatData() // creates a copy
|
||||||
if threat.MitigationLevel > mitigationLevel {
|
|
||||||
mitigationLevel = threat.MitigationLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set new ThreatMitigationLevel
|
// Publish publishes the current threat.
|
||||||
s.ThreatMitigationLevel = mitigationLevel
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Save()
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue