/
profile.go
561 lines (483 loc) Β· 16.2 KB
/
profile.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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
}