safing-portmaster/service/profile/profile.go
2024-04-18 10:21:25 +02:00

561 lines
16 KiB
Go

package profile
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tevino/abool"
"github.com/safing/portbase/config"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/service/intel/filterlists"
"github.com/safing/portmaster/service/profile/binmeta"
"github.com/safing/portmaster/service/profile/endpoints"
)
// ProfileSource is the source of the profile.
type ProfileSource string //nolint:golint
// Profile Sources.
const (
SourceLocal ProfileSource = "local" // local, editable
SourceSpecial ProfileSource = "special" // specials (read-only)
)
// Default Action IDs.
const (
DefaultActionNotSet uint8 = 0
DefaultActionBlock uint8 = 1
DefaultActionAsk uint8 = 2
DefaultActionPermit uint8 = 3
)
// Profile is used to predefine a security profile for applications.
type Profile struct { //nolint:maligned // not worth the effort
record.Base
sync.RWMutex
// ID is a unique identifier for the profile.
ID string // constant
// Source describes the source of the profile.
Source ProfileSource // constant
// Name is a human readable name of the profile. It
// defaults to the basename of the application.
Name string
// Description may hold an optional description of the
// profile or the purpose of the application.
Description string
// Warning may hold an optional warning about this application.
// It may be static or be added later on when the Portmaster detected an
// issue with the application.
Warning string
// WarningLastUpdated holds the timestamp when the Warning field was last
// updated.
WarningLastUpdated time.Time
// Homepage may refer to the website of the application
// vendor.
Homepage string
// Deprecated: Icon holds the icon of the application. The value
// may either be a filepath, a database key or a blob URL.
// See IconType for more information.
Icon string
// Deprecated: IconType describes the type of the Icon property.
IconType binmeta.IconType
// Icons holds a list of icons to represent the application.
Icons []binmeta.Icon
// Deprecated: LinkedPath used to point to the executableis this
// profile was created for.
// Until removed, it will be added to the Fingerprints as an exact path match.
LinkedPath string // constant
// PresentationPath holds the path of an executable that should be used for
// get representative information from, like the name of the program or the icon.
// Is automatically removed when the path does not exist.
// Is automatically populated with the next match when empty.
PresentationPath string
// UsePresentationPath can be used to enable/disable fetching information
// from the executable at PresentationPath. In some cases, this is not
// desirable.
UsePresentationPath bool
// Fingerprints holds process matching information.
Fingerprints []Fingerprint
// Config holds profile specific setttings. It's a nested
// object with keys defining the settings database path. All keys
// until the actual settings value (which is everything that is not
// an object) need to be concatenated for the settings database
// path.
Config map[string]interface{}
// LastEdited holds the UTC timestamp in seconds when the profile was last
// edited by the user. This is not set automatically, but has to be manually
// set by the user interface.
LastEdited int64
// Created holds the UTC timestamp in seconds when the
// profile has been created.
Created int64
// Internal is set to true if the profile is attributed to a
// Portmaster internal process. Internal is set during profile
// creation and may be accessed without lock.
Internal bool
// layeredProfile is a link to the layered profile with this profile as the
// main profile.
// All processes with the same binary should share the same instance of the
// local profile and the associated layered profile.
layeredProfile *LayeredProfile
// Interpreted Data
configPerspective *config.Perspective
dataParsed bool
defaultAction uint8
endpoints endpoints.Endpoints
serviceEndpoints endpoints.Endpoints
filterListsSet bool
filterListIDs []string
spnUsagePolicy endpoints.Endpoints
spnTransitHubPolicy endpoints.Endpoints
spnExitHubPolicy endpoints.Endpoints
// Lifecycle Management
outdated *abool.AtomicBool
lastActive *int64
// savedInternally is set to true for profiles that are saved internally.
savedInternally bool
}
func (profile *Profile) prepProfile() {
// prepare configuration
profile.outdated = abool.New()
profile.lastActive = new(int64)
// Migration of LinkedPath to PresentationPath
if profile.PresentationPath == "" && profile.LinkedPath != "" {
profile.PresentationPath = profile.LinkedPath
}
}
func (profile *Profile) parseConfig() error {
// Check if already parsed.
if profile.dataParsed {
return nil
}
// Create new perspective and marked as parsed.
var err error
profile.configPerspective, err = config.NewPerspective(profile.Config)
if err != nil {
return fmt.Errorf("failed to create config perspective: %w", err)
}
profile.dataParsed = true
var lastErr error
action, ok := profile.configPerspective.GetAsString(CfgOptionDefaultActionKey)
profile.defaultAction = DefaultActionNotSet
if ok {
switch action {
case DefaultActionPermitValue:
profile.defaultAction = DefaultActionPermit
case DefaultActionAskValue:
profile.defaultAction = DefaultActionAsk
case DefaultActionBlockValue:
profile.defaultAction = DefaultActionBlock
default:
lastErr = fmt.Errorf(`default action "%s" invalid`, action)
}
}
list, ok := profile.configPerspective.GetAsStringArray(CfgOptionEndpointsKey)
profile.endpoints = nil
if ok {
profile.endpoints, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionServiceEndpointsKey)
profile.serviceEndpoints = nil
if ok {
profile.serviceEndpoints, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionFilterListsKey)
profile.filterListsSet = false
if ok {
profile.filterListIDs, err = filterlists.ResolveListIDs(list)
if err != nil {
lastErr = err
} else {
profile.filterListsSet = true
}
}
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionSPNUsagePolicyKey)
profile.spnUsagePolicy = nil
if ok {
profile.spnUsagePolicy, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionTransitHubPolicyKey)
profile.spnTransitHubPolicy = nil
if ok {
profile.spnTransitHubPolicy, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
list, ok = profile.configPerspective.GetAsStringArray(CfgOptionExitHubPolicyKey)
profile.spnExitHubPolicy = nil
if ok {
profile.spnExitHubPolicy, err = endpoints.ParseEndpoints(list)
if err != nil {
lastErr = err
}
}
return lastErr
}
// New returns a new Profile.
// Optionally, you may supply custom configuration in the flat (key=value) form.
func New(profile *Profile) *Profile {
// Create profile if none is given.
if profile == nil {
profile = &Profile{}
}
// Set default and internal values.
profile.Created = time.Now().Unix()
profile.savedInternally = true
// Expand any given configuration.
if profile.Config != nil {
profile.Config = config.Expand(profile.Config)
} else {
profile.Config = make(map[string]interface{})
}
// Generate ID if none is given.
if profile.ID == "" {
if len(profile.Fingerprints) > 0 {
// Derive from fingerprints.
profile.ID = DeriveProfileID(profile.Fingerprints)
} else {
// Generate random ID as fallback.
log.Warningf("profile: creating new profile without fingerprints to derive ID from")
profile.ID = utils.RandomUUID("").String()
}
}
// Make key from ID and source.
profile.makeKey()
// Prepare and parse initial profile config.
profile.prepProfile()
if err := profile.parseConfig(); err != nil {
log.Errorf("profile: failed to parse new profile: %s", err)
}
return profile
}
// ScopedID returns the scoped ID (Source + ID) of the profile.
func (profile *Profile) ScopedID() string {
return MakeScopedID(profile.Source, profile.ID)
}
// makeKey derives and sets the record Key from the profile attributes.
func (profile *Profile) makeKey() {
profile.SetKey(MakeProfileKey(profile.Source, profile.ID))
}
// Save saves the profile to the database.
func (profile *Profile) Save() error {
if profile.ID == "" {
return errors.New("profile: tried to save profile without ID")
}
if profile.Source == "" {
return fmt.Errorf("profile: profile %s does not specify a source", profile.ID)
}
return profileDB.Put(profile)
}
// delete deletes the profile from the database.
func (profile *Profile) delete() error {
// Check if a key is set.
if !profile.KeyIsSet() {
return errors.New("key is not set")
}
// Delete from database.
profile.Meta().Delete()
err := profileDB.Put(profile)
if err != nil {
return err
}
// Post handling is done by the profile update feed.
return nil
}
// MarkStillActive marks the profile as still active.
func (profile *Profile) MarkStillActive() {
atomic.StoreInt64(profile.lastActive, time.Now().Unix())
}
// LastActive returns the unix timestamp when the profile was last marked as
// still active.
func (profile *Profile) LastActive() int64 {
return atomic.LoadInt64(profile.lastActive)
}
// String returns a string representation of the Profile.
func (profile *Profile) String() string {
return fmt.Sprintf("<%s %s/%s>", profile.Name, profile.Source, profile.ID)
}
// IsOutdated returns whether the this instance of the profile is marked as outdated.
func (profile *Profile) IsOutdated() bool {
return profile.outdated.IsSet()
}
// GetEndpoints returns the endpoint list of the profile. This functions
// requires the profile to be read locked.
func (profile *Profile) GetEndpoints() endpoints.Endpoints {
return profile.endpoints
}
// GetServiceEndpoints returns the service endpoint list of the profile. This
// functions requires the profile to be read locked.
func (profile *Profile) GetServiceEndpoints() endpoints.Endpoints {
return profile.serviceEndpoints
}
// AddEndpoint adds an endpoint to the endpoint list, saves the profile and reloads the configuration.
func (profile *Profile) AddEndpoint(newEntry string) {
profile.addEndpointEntry(CfgOptionEndpointsKey, newEntry)
}
// AddServiceEndpoint adds a service endpoint to the endpoint list, saves the profile and reloads the configuration.
func (profile *Profile) AddServiceEndpoint(newEntry string) {
profile.addEndpointEntry(CfgOptionServiceEndpointsKey, newEntry)
}
func (profile *Profile) addEndpointEntry(cfgKey, newEntry string) {
changed := false
// When finished, save the profile.
defer func() {
if !changed {
return
}
err := profile.Save()
if err != nil {
log.Warningf("profile: failed to save profile %s after add an endpoint rule: %s", profile.ScopedID(), err)
}
}()
// Lock the profile for editing.
profile.Lock()
defer profile.Unlock()
// Get the endpoint list configuration value and add the new entry.
endpointList, ok := profile.configPerspective.GetAsStringArray(cfgKey)
if ok {
// A list already exists, check for duplicates within the same prefix.
newEntryPrefix := strings.Split(newEntry, " ")[0] + " "
for _, entry := range endpointList {
if !strings.HasPrefix(entry, newEntryPrefix) {
// We found an entry with a different prefix than the new entry.
// Beyond this entry we cannot possibly know if identical entries will
// match, so we will have to add the new entry no matter what the rest
// of the list has.
break
}
if entry == newEntry {
// An identical entry is already in the list, abort.
log.Debugf("profile: ignoring new endpoint rule for %s, as identical is already present: %s", profile, newEntry)
return
}
}
endpointList = append([]string{newEntry}, endpointList...)
} else {
endpointList = []string{newEntry}
}
// Save new value back to profile.
config.PutValueIntoHierarchicalConfig(profile.Config, cfgKey, endpointList)
changed = true
// Reload the profile manually in order to parse the newly added entry.
profile.dataParsed = false
err := profile.parseConfig()
if err != nil {
log.Errorf("profile: failed to parse %s config after adding endpoint: %s", profile, err)
}
}
// LayeredProfile returns the layered profile associated with this profile.
func (profile *Profile) LayeredProfile() *LayeredProfile {
profile.Lock()
defer profile.Unlock()
return profile.layeredProfile
}
// EnsureProfile ensures that the given record is a *Profile, and returns it.
func EnsureProfile(r record.Record) (*Profile, error) {
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
newProfile := &Profile{}
err := record.Unwrap(r, newProfile)
if err != nil {
return nil, err
}
return newProfile, nil
}
// or adjust type
newProfile, ok := r.(*Profile)
if !ok {
return nil, fmt.Errorf("record not of type *Profile, but %T", r)
}
return newProfile, nil
}
// updateMetadata updates meta data fields on the profile and returns whether
// the profile was changed.
func (profile *Profile) updateMetadata(binaryPath string) (changed bool) {
// Check if this is a local profile, else warn and return.
if profile.Source != SourceLocal {
log.Warningf("tried to update metadata for non-local profile %s", profile.ScopedID())
return false
}
// Set PresentationPath if unset.
if profile.PresentationPath == "" && binaryPath != "" {
profile.PresentationPath = binaryPath
changed = true
}
// Migrate LinkedPath to PresentationPath.
// TODO: Remove in v1.5
if profile.PresentationPath == "" && profile.LinkedPath != "" {
profile.PresentationPath = profile.LinkedPath
changed = true
}
// Set Name if unset.
if profile.Name == "" && profile.PresentationPath != "" {
// Generate a default profile name from path.
profile.Name = binmeta.GenerateBinaryNameFromPath(profile.PresentationPath)
changed = true
}
// Migrate to Fingerprints.
// TODO: Remove in v1.5
if len(profile.Fingerprints) == 0 && profile.LinkedPath != "" {
profile.Fingerprints = []Fingerprint{
{
Type: FingerprintTypePathID,
Operation: FingerprintOperationEqualsID,
Value: profile.LinkedPath,
},
}
changed = true
}
// UI Backward Compatibility:
// Fill LinkedPath with PresentationPath
// TODO: Remove in v1.1
if profile.LinkedPath == "" && profile.PresentationPath != "" {
profile.LinkedPath = profile.PresentationPath
changed = true
}
return changed
}
// updateMetadataFromSystem updates the profile metadata with data from the
// operating system and saves it afterwards.
func (profile *Profile) updateMetadataFromSystem(ctx context.Context, md MatchingData) error {
var changed bool
// This function is only valid for local profiles.
if profile.Source != SourceLocal || profile.PresentationPath == "" {
return fmt.Errorf("tried to update metadata for non-local or non-path profile %s", profile.ScopedID())
}
// Get home from ENV.
var home string
if env := md.Env(); env != nil {
home = env["HOME"]
}
// Get binary icon and name.
newIcon, newName, err := binmeta.GetIconAndName(ctx, profile.PresentationPath, home)
switch {
case err == nil:
// Continue
case errors.Is(err, binmeta.ErrIconIgnored):
newIcon = nil
// Continue
default:
log.Warningf("profile: failed to get binary icon/name for %s: %s", profile.PresentationPath, err)
}
// Apply new data to profile.
func() {
// Lock profile for applying metadata.
profile.Lock()
defer profile.Unlock()
// Apply new name if it changed.
if newName != "" && profile.Name != newName {
profile.Name = newName
changed = true
}
// Apply new icon if found.
if newIcon != nil {
if len(profile.Icons) == 0 {
profile.Icons = []binmeta.Icon{*newIcon}
} else {
profile.Icons = append(profile.Icons, *newIcon)
profile.Icons = binmeta.SortAndCompactIcons(profile.Icons)
}
}
}()
// If anything changed, save the profile.
// profile.Lock must not be held!
if changed {
err := profile.Save()
if err != nil {
log.Warningf("profile: failed to save %s after metadata update: %s", profile.ScopedID(), err)
}
}
return nil
}