diff --git a/profile/profile.go b/profile/profile.go index 5be0d1a8..9ba6929e 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -135,7 +135,7 @@ func EnsureProfile(r record.Record) (*Profile, error) { // or adjust type new, ok := r.(*Profile) if !ok { - return nil, fmt.Errorf("record not of type *Example, but %T", r) + return nil, fmt.Errorf("record not of type *Profile, but %T", r) } return new, nil } diff --git a/profile/updates.go b/profile/updates.go index 1aa0ddf3..7d767052 100644 --- a/profile/updates.go +++ b/profile/updates.go @@ -31,7 +31,7 @@ func updateListener(sub *database.Subscription) { profile, err := EnsureProfile(r) if err != nil { - log.Errorf("profile: received update for special profile, but could not read: %s", err) + log.Errorf("profile: received update for profile, but could not read: %s", err) continue } diff --git a/status/database.go b/status/database.go new file mode 100644 index 00000000..935e4049 --- /dev/null +++ b/status/database.go @@ -0,0 +1,47 @@ +package status + +import ( + "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) { + newStatus, err := EnsureSystemStatus(r) + if err != nil { + return nil, err + } + newStatus.Lock() + defer newStatus.Unlock() + + // apply applicable settings + setSelectedSecurityLevel(newStatus.SelectedSecurityLevel) + // 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 +} diff --git a/status/get-config.go b/status/get-config.go index 134a9cef..00349f73 100644 --- a/status/get-config.go +++ b/status/get-config.go @@ -20,7 +20,7 @@ func max(a, b uint8) uint8 { func ConfigIsActive(name string) SecurityLevelOption { activeAtLevel := config.GetAsInt(name, int64(SecurityLevelDynamic)) return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel()) <= max(CurrentSecurityLevel(), minSecurityLevel) + return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel) } } @@ -28,6 +28,6 @@ func ConfigIsActive(name string) SecurityLevelOption { func ConfigIsActiveConcurrent(name string) SecurityLevelOption { activeAtLevel := config.Concurrent.GetAsInt(name, int64(SecurityLevelDynamic)) return func(minSecurityLevel uint8) bool { - return uint8(activeAtLevel()) <= max(CurrentSecurityLevel(), minSecurityLevel) + return uint8(activeAtLevel()) <= max(ActiveSecurityLevel(), minSecurityLevel) } } diff --git a/status/get.go b/status/get.go index 158a36eb..8cd4f191 100644 --- a/status/get.go +++ b/status/get.go @@ -5,32 +5,29 @@ import ( ) var ( - currentSecurityLevel *uint32 + activeSecurityLevel *uint32 selectedSecurityLevel *uint32 - threatLevel *uint32 portmasterStatus *uint32 gate17Status *uint32 ) func init() { var ( - currentSecurityLevelValue uint32 + activeSecurityLevelValue uint32 selectedSecurityLevelValue uint32 - threatLevelValue uint32 portmasterStatusValue uint32 gate17StatusValue uint32 ) - currentSecurityLevel = ¤tSecurityLevelValue + activeSecurityLevel = &activeSecurityLevelValue selectedSecurityLevel = &selectedSecurityLevelValue - threatLevel = &threatLevelValue portmasterStatus = &portmasterStatusValue gate17Status = &gate17StatusValue } -// CurrentSecurityLevel returns the current security level. -func CurrentSecurityLevel() uint8 { - return uint8(atomic.LoadUint32(currentSecurityLevel)) +// ActiveSecurityLevel returns the current security level. +func ActiveSecurityLevel() uint8 { + return uint8(atomic.LoadUint32(activeSecurityLevel)) } // SelectedSecurityLevel returns the selected security level. @@ -38,11 +35,6 @@ func SelectedSecurityLevel() uint8 { return uint8(atomic.LoadUint32(selectedSecurityLevel)) } -// ThreatLevel returns the current threat level. -func ThreatLevel() uint8 { - return uint8(atomic.LoadUint32(threatLevel)) -} - // PortmasterStatus returns the current Portmaster status. func PortmasterStatus() uint8 { return uint8(atomic.LoadUint32(portmasterStatus)) diff --git a/status/get_test.go b/status/get_test.go index 2777157e..413ae269 100644 --- a/status/get_test.go +++ b/status/get_test.go @@ -5,9 +5,9 @@ import "testing" func TestGet(t *testing.T) { // only test for panics - CurrentSecurityLevel() + // TODO: write real tests + ActiveSecurityLevel() SelectedSecurityLevel() - ThreatLevel() PortmasterStatus() Gate17Status() option := ConfigIsActive("invalid") diff --git a/status/module.go b/status/module.go index bd0c3a42..4aebbc0c 100644 --- a/status/module.go +++ b/status/module.go @@ -1,25 +1,59 @@ package status import ( + "github.com/Safing/portbase/database" "github.com/Safing/portbase/log" "github.com/Safing/portbase/modules" ) +var ( + shutdownSignal = make(chan struct{}) +) + func init() { - modules.Register("status", prep, nil, nil) + modules.Register("status", nil, start, stop) } -func prep() error { +func start() error { + var loadedStatus *SystemStatus - if CurrentSecurityLevel() == SecurityLevelOff { - log.Infof("switching to default active security level: dynamic") - SetCurrentSecurityLevel(SecurityLevelDynamic) + // 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) + loadedStatus = nil + } + case database.ErrNotFound: + // create new status + default: + log.Criticalf("status: failed to load system status: %s", err) } - if SelectedSecurityLevel() == SecurityLevelOff { - log.Infof("switching to default selected security level: dynamic") - SetSelectedSecurityLevel(SecurityLevelDynamic) + // activate loaded status, if available + if loadedStatus != nil { + status = loadedStatus } + status.Lock() + defer status.Unlock() + // load status into atomic getters + atomicUpdateSelectedSecurityLevel(status.SelectedSecurityLevel) + atomicUpdatePortmasterStatus(status.PortmasterStatus) + atomicUpdateGate17Status(status.Gate17Status) + + // update status + status.updateThreatMitigationLevel() + status.autopilot() + + go status.Save() + + return initStatusHook() +} + +func stop() error { + close(shutdownSignal) return nil } diff --git a/status/set.go b/status/set.go index f46b3ded..4d63ac16 100644 --- a/status/set.go +++ b/status/set.go @@ -1,61 +1,93 @@ package status -import "sync/atomic" +import ( + "sync/atomic" -// SetCurrentSecurityLevel sets the current security level. -func SetCurrentSecurityLevel(level uint8) { - sysStatusLock.Lock() - defer sysStatusLock.Unlock() - sysStatus.CurrentSecurityLevel = level - atomicUpdateCurrentSecurityLevel(level) + "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 = SecurityLevelDynamic + atomicUpdateActiveSecurityLevel(SecurityLevelDynamic) + case SecurityLevelDynamic, SecurityLevelSecure, SecurityLevelFortress: + 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) { - sysStatusLock.Lock() - defer sysStatusLock.Unlock() - sysStatus.SelectedSecurityLevel = level - atomicUpdateSelectedSecurityLevel(level) -} +// setSelectedSecurityLevel sets the selected security level. +func setSelectedSecurityLevel(level uint8) { + switch level { + case SecurityLevelOff, SecurityLevelDynamic, SecurityLevelSecure, SecurityLevelFortress: + status.Lock() + defer status.Unlock() -// SetThreatLevel sets the current threat level. -func SetThreatLevel(level uint8) { - sysStatusLock.Lock() - defer sysStatusLock.Unlock() - sysStatus.ThreatLevel = level - atomicUpdateThreatLevel(level) + status.SelectedSecurityLevel = level + atomicUpdateSelectedSecurityLevel(level) + status.autopilot() + + go status.Save() + default: + log.Errorf("status: tried to set selected security level to invalid value: %d", level) + } } // SetPortmasterStatus sets the current Portmaster status. -func SetPortmasterStatus(status uint8) { - sysStatusLock.Lock() - defer sysStatusLock.Unlock() - sysStatus.PortmasterStatus = status - atomicUpdatePortmasterStatus(status) +func SetPortmasterStatus(pmStatus uint8, msg string) { + switch pmStatus { + case StatusOff, StatusError, StatusWarning, StatusOk: + status.Lock() + defer status.Unlock() + + status.PortmasterStatus = pmStatus + status.PortmasterStatusMsg = msg + atomicUpdatePortmasterStatus(pmStatus) + + go status.Save() + default: + log.Errorf("status: tried to set portmaster to invalid status: %d", status) + } } // SetGate17Status sets the current Gate17 status. -func SetGate17Status(status uint8) { - sysStatusLock.Lock() - defer sysStatusLock.Unlock() - sysStatus.Gate17Status = status - atomicUpdateGate17Status(status) +func SetGate17Status(g17Status uint8, msg string) { + switch g17Status { + case StatusOff, StatusError, StatusWarning, StatusOk: + status.Lock() + defer status.Unlock() + + status.Gate17Status = g17Status + status.Gate17StatusMsg = msg + atomicUpdateGate17Status(g17Status) + + go status.Save() + default: + log.Errorf("status: tried to set gate17 to invalid status: %d", status) + } } // update functions for atomic stuff - -func atomicUpdateCurrentSecurityLevel(level uint8) { - atomic.StoreUint32(currentSecurityLevel, uint32(level)) +func atomicUpdateActiveSecurityLevel(level uint8) { + atomic.StoreUint32(activeSecurityLevel, uint32(level)) } func atomicUpdateSelectedSecurityLevel(level uint8) { atomic.StoreUint32(selectedSecurityLevel, uint32(level)) } -func atomicUpdateThreatLevel(level uint8) { - atomic.StoreUint32(threatLevel, uint32(level)) -} - func atomicUpdatePortmasterStatus(status uint8) { atomic.StoreUint32(portmasterStatus, uint32(status)) } diff --git a/status/set_test.go b/status/set_test.go index 626882ab..3502c152 100644 --- a/status/set_test.go +++ b/status/set_test.go @@ -5,10 +5,9 @@ import "testing" func TestSet(t *testing.T) { // only test for panics - SetCurrentSecurityLevel(0) - SetSelectedSecurityLevel(0) - SetThreatLevel(0) - SetPortmasterStatus(0) - SetGate17Status(0) + // TODO: write real tests + setSelectedSecurityLevel(0) + SetPortmasterStatus(0, "") + SetGate17Status(0, "") } diff --git a/status/status.go b/status/status.go index 86a34f8c..e71d892c 100644 --- a/status/status.go +++ b/status/status.go @@ -1,38 +1,81 @@ package status -import "sync" +import ( + "fmt" + "sync" + + "github.com/Safing/portbase/database/record" + "github.com/Safing/portbase/log" +) var ( - sysStatus *SystemStatus - sysStatusLock sync.RWMutex + status *SystemStatus ) func init() { - sysStatus = &SystemStatus{} + status = &SystemStatus{ + Threats: make(map[string]*Threat), + } + status.SetKey(statusDBKey) } // SystemStatus saves basic information about the current system status. type SystemStatus struct { - // database.Base - CurrentSecurityLevel uint8 + record.Base + sync.Mutex + + ActiveSecurityLevel uint8 SelectedSecurityLevel uint8 - ThreatLevel uint8 `json:",omitempty" bson:",omitempty"` - ThreatReason string `json:",omitempty" bson:",omitempty"` + PortmasterStatus uint8 + PortmasterStatusMsg string - PortmasterStatus uint8 `json:",omitempty" bson:",omitempty"` - PortmasterStatusMsg string `json:",omitempty" bson:",omitempty"` + Gate17Status uint8 + Gate17StatusMsg string - Gate17Status uint8 `json:",omitempty" bson:",omitempty"` - Gate17StatusMsg string `json:",omitempty" bson:",omitempty"` + ThreatMitigationLevel uint8 + Threats map[string]*Threat + + UpdateStatus string } -// FmtCurrentSecurityLevel returns the current security level as a string. -func FmtCurrentSecurityLevel() string { - current := CurrentSecurityLevel() - selected := SelectedSecurityLevel() - s := FmtSecurityLevel(current) - if current != selected { +// 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 diff --git a/status/status_test.go b/status/status_test.go index d4c4d328..b08eb664 100644 --- a/status/status_test.go +++ b/status/status_test.go @@ -4,33 +4,31 @@ import "testing" func TestStatus(t *testing.T) { - SetCurrentSecurityLevel(SecurityLevelOff) - SetSelectedSecurityLevel(SecurityLevelOff) - if FmtCurrentSecurityLevel() != "Off" { - t.Error("unexpected string representation") + setSelectedSecurityLevel(SecurityLevelOff) + if FmtActiveSecurityLevel() != "Dynamic" { + t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) } - SetCurrentSecurityLevel(SecurityLevelDynamic) - SetSelectedSecurityLevel(SecurityLevelDynamic) - if FmtCurrentSecurityLevel() != "Dynamic" { - t.Error("unexpected string representation") + setSelectedSecurityLevel(SecurityLevelDynamic) + AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelSecure}) + if FmtActiveSecurityLevel() != "Dynamic*" { + t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) } - SetCurrentSecurityLevel(SecurityLevelSecure) - SetSelectedSecurityLevel(SecurityLevelSecure) - if FmtCurrentSecurityLevel() != "Secure" { - t.Error("unexpected string representation") + setSelectedSecurityLevel(SecurityLevelSecure) + if FmtActiveSecurityLevel() != "Secure" { + t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) } - SetCurrentSecurityLevel(SecurityLevelFortress) - SetSelectedSecurityLevel(SecurityLevelFortress) - if FmtCurrentSecurityLevel() != "Fortress" { - t.Error("unexpected string representation") + setSelectedSecurityLevel(SecurityLevelSecure) + AddOrUpdateThreat(&Threat{MitigationLevel: SecurityLevelFortress}) + if FmtActiveSecurityLevel() != "Secure*" { + t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) } - SetSelectedSecurityLevel(SecurityLevelDynamic) - if FmtCurrentSecurityLevel() != "Fortress*" { - t.Error("unexpected string representation") + setSelectedSecurityLevel(SecurityLevelFortress) + if FmtActiveSecurityLevel() != "Fortress" { + t.Errorf("unexpected string representation: %s", FmtActiveSecurityLevel()) } } diff --git a/status/threat.go b/status/threat.go new file mode 100644 index 00000000..f829e91c --- /dev/null +++ b/status/threat.go @@ -0,0 +1,72 @@ +package status + +import ( + "strings" + "sync" +) + +// Threat describes a detected threat. +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 +} + +// AddOrUpdateThreat adds or updates a new threat in the system status. +func AddOrUpdateThreat(new *Threat) { + status.Lock() + defer status.Unlock() + + status.Threats[new.ID] = new + status.updateThreatMitigationLevel() + status.autopilot() + + go status.Save() +} + +// DeleteThreat deletes a threat from the system status. +func DeleteThreat(id string) { + status.Lock() + defer status.Unlock() + + delete(status.Threats, id) + status.updateThreatMitigationLevel() + status.autopilot() + + go status.Save() +} + +// 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() + + var exportedThreats []*Threat + for id, threat := range status.Threats { + if strings.HasPrefix(id, idPrefix) { + exportedThreats = append(exportedThreats, threat) + } + } + + return exportedThreats, &status.Mutex +} + +func (s *SystemStatus) updateThreatMitigationLevel() { + // get highest mitigationLevel + var mitigationLevel uint8 + for _, threat := range s.Threats { + switch threat.MitigationLevel { + case SecurityLevelDynamic, SecurityLevelSecure, SecurityLevelFortress: + if threat.MitigationLevel > mitigationLevel { + mitigationLevel = threat.MitigationLevel + } + } + } + + // set new ThreatMitigationLevel + s.ThreatMitigationLevel = mitigationLevel +} diff --git a/status/updates.go b/status/updates.go new file mode 100644 index 00000000..2b9ef79f --- /dev/null +++ b/status/updates.go @@ -0,0 +1,18 @@ +package status + +// Update status options +const ( + UpdateStatusCurrentStable = "stable" + UpdateStatusCurrentBeta = "beta" + UpdateStatusAvailable = "available" // restart or reboot required + UpdateStatusFailed = "failed" // check logs +) + +// SetUpdateStatus updates the system status with a new update status. +func SetUpdateStatus(newStatus string) { + status.Lock() + status.UpdateStatus = newStatus + status.Unlock() + + go status.Save() +}