Finish initial adaption of profiles

This commit is contained in:
Daniel 2018-10-30 19:13:21 +01:00
parent 247c7dca00
commit 5533203fa1
26 changed files with 1299 additions and 814 deletions

9
profile/const.go Normal file
View file

@ -0,0 +1,9 @@
package profile
// Platform identifiers
const (
PlatformAny = "any"
PlatformLinux = "lin"
PlatformWindows = "win"
PlatformMac = "mac"
)

6
profile/const_linux.go Normal file
View file

@ -0,0 +1,6 @@
package profile
// OS Identifier Prefix
const (
IdentifierPrefix = "lin:"
)

21
profile/database.go Normal file
View file

@ -0,0 +1,21 @@
package profile
import (
"github.com/Safing/portbase/database"
)
// core:profiles/user/12345-1234-125-1234-1235
// core:profiles/special/default
// /global
// core:profiles/stamp/12334-1235-1234-5123-1234
// core:profiles/identifier/base64
// Namespaces
const (
UserNamespace = "user"
StampNamespace = "stamp"
)
var (
profileDB = database.NewInterface(nil)
)

42
profile/domains.go Normal file
View file

@ -0,0 +1,42 @@
package profile
import "strings"
// Domains is a list of permitted or denied domains.
type Domains map[string]*DomainDecision
// DomainDecision holds a decision about a domain.
type DomainDecision struct {
Permit bool
Created int64
IncludeSubdomains bool
}
// IsSet returns whether the Domains object is "set".
func (d Domains) IsSet() bool {
if d != nil {
return true
}
return false
}
// CheckStatus checks if the given domain is governed in the list of domains and returns whether it is permitted.
func (d Domains) CheckStatus(domain string) (permit, ok bool) {
// check for exact domain
dd, ok := d[domain]
if ok {
return dd.Permit, true
}
// check if domain is a subdomain of any of the domains
for key, dd := range d {
if dd.IncludeSubdomains && strings.HasSuffix(domain, key) {
preDottedKey := "." + key
if strings.HasSuffix(domain, preDottedKey) {
return dd.Permit, true
}
}
}
return false, false
}

145
profile/flags.go Normal file
View file

@ -0,0 +1,145 @@
package profile
import (
"errors"
"strings"
"github.com/Safing/portmaster/status"
)
// ProfileFlags are used to quickly add common attributes to profiles
type ProfileFlags map[uint8]uint8
// Profile Flags
const (
// Profile Modes
Prompt uint8 = 0 // Prompt first-seen connections
Blacklist uint8 = 1 // Allow everything not explicitly denied
Whitelist uint8 = 2 // Only allow everything explicitly allowed
// Network Locations
Internet uint8 = 16 // Allow connections to the Internet
LAN uint8 = 17 // Allow connections to the local area network
Localhost uint8 = 18 // Allow connections on the local host
// Specials
Related uint8 = 32 // If and before prompting, allow domains that are related to the program
PeerToPeer uint8 = 33 // Allow program to directly communicate with peers, without resolving DNS first
Service uint8 = 34 // Allow program to accept incoming connections
Independent uint8 = 35 // Ignore profile settings coming from the Community
RequireGate17 uint8 = 36 // Require all connections to go over Gate17
)
var (
// ErrProfileFlagsParseFailed is returned if a an invalid flag is encountered while parsing
ErrProfileFlagsParseFailed = errors.New("profiles: failed to parse flags")
sortedFlags = []uint8{
Prompt,
Blacklist,
Whitelist,
Internet,
LAN,
Localhost,
Related,
PeerToPeer,
Service,
Independent,
RequireGate17,
}
flagIDs = map[string]uint8{
"Prompt": Prompt,
"Blacklist": Blacklist,
"Whitelist": Whitelist,
"Internet": Internet,
"LAN": LAN,
"Localhost": Localhost,
"Related": Related,
"PeerToPeer": PeerToPeer,
"Service": Service,
"Independent": Independent,
"RequireGate17": RequireGate17,
}
flagNames = map[uint8]string{
Prompt: "Prompt",
Blacklist: "Blacklist",
Whitelist: "Whitelist",
Internet: "Internet",
LAN: "LAN",
Localhost: "Localhost",
Related: "Related",
PeerToPeer: "PeerToPeer",
Service: "Service",
Independent: "Independent",
RequireGate17: "RequireGate17",
}
)
// FlagsFromNames creates ProfileFlags from a comma seperated list of flagnames (e.g. "System,Strict,Secure")
// func FlagsFromNames(words []string) (*ProfileFlags, error) {
// var flags ProfileFlags
// for _, entry := range words {
// flag, ok := flagIDs[entry]
// if !ok {
// return nil, ErrProfileFlagsParseFailed
// }
// flags = append(flags, flag)
// }
// return &flags, nil
// }
// IsSet returns whether the ProfileFlags object is "set".
func (pf ProfileFlags) IsSet() bool {
if pf != nil {
return true
}
return false
}
// Has checks if a ProfileFlags object has a flag set in the given security level
func (pf ProfileFlags) Has(flag, level uint8) bool {
setting, ok := pf[flag]
if ok && setting&level > 0 {
return true
}
return false
}
func getLevelMarker(levels, level uint8) string {
if levels&level > 0 {
return "+"
}
return "-"
}
// String return a string representation of ProfileFlags
func (pf ProfileFlags) String() string {
var namedFlags []string
for _, flag := range sortedFlags {
levels, ok := pf[flag]
if ok {
s := flagNames[flag]
if levels != status.SecurityLevelsAll {
s += getLevelMarker(levels, status.SecurityLevelDynamic)
s += getLevelMarker(levels, status.SecurityLevelSecure)
s += getLevelMarker(levels, status.SecurityLevelFortress)
}
}
}
for _, flag := range pf {
namedFlags = append(namedFlags, flagNames[flag])
}
return strings.Join(namedFlags, ", ")
}
// Add adds a flag to the Flags with the given level.
func (pf ProfileFlags) Add(flag, levels uint8) {
pf[flag] = levels
}
// Remove removes a flag from the Flags.
func (pf ProfileFlags) Remove(flag uint8) {
delete(pf, flag)
}

54
profile/flags_test.go Normal file
View file

@ -0,0 +1,54 @@
package profile
import (
"testing"
)
func TestProfileFlags(t *testing.T) {
// check if all IDs have a name
for key, entry := range flagIDs {
if _, ok := flagNames[entry]; !ok {
t.Errorf("could not find entry for %s in flagNames", key)
}
}
// check if all names have an ID
for key, entry := range flagNames {
if _, ok := flagIDs[entry]; !ok {
t.Errorf("could not find entry for %d in flagNames", key)
}
}
// // check Has
// emptyFlags := ProfileFlags{}
// for flag, name := range flagNames {
// if !sortedFlags.Has(flag) {
// t.Errorf("sortedFlags should have flag %s (%d)", name, flag)
// }
// if emptyFlags.Has(flag) {
// t.Errorf("emptyFlags should not have flag %s (%d)", name, flag)
// }
// }
//
// // check ProfileFlags creation from strings
// var allFlagStrings []string
// for _, flag := range *sortedFlags {
// allFlagStrings = append(allFlagStrings, flagNames[flag])
// }
// newFlags, err := FlagsFromNames(allFlagStrings)
// if err != nil {
// t.Errorf("error while parsing flags: %s", err)
// }
// if newFlags.String() != sortedFlags.String() {
// t.Errorf("parsed flags are not correct (or tests have not been updated to reflect the right number), expected %v, got %v", *sortedFlags, *newFlags)
// }
//
// // check ProfileFlags Stringer
// flagString := newFlags.String()
// check := strings.Join(allFlagStrings, ",")
// if flagString != check {
// t.Errorf("flag string is not correct, expected %s, got %s", check, flagString)
// }
}

76
profile/framework.go Normal file
View file

@ -0,0 +1,76 @@
package profile
// DEACTIVATED
// import (
// "fmt"
// "os"
// "path/filepath"
// "regexp"
// "strings"
//
// "github.com/Safing/portbase/log"
// )
//
// type Framework struct {
// // go hirarchy up
// FindParent uint8 `json:",omitempty bson:",omitempty"`
// // get path from parent, amount of levels to go up the tree (1 means parent, 2 means parent of parents, and so on)
// MergeWithParent bool `json:",omitempty bson:",omitempty"`
// // instead of getting the path of the parent, merge with it by presenting connections as if they were from that parent
//
// // go hirarchy down
// Find string `json:",omitempty bson:",omitempty"`
// // Regular expression for finding path elements
// Build string `json:",omitempty bson:",omitempty"`
// // Path definitions for building path
// Virtual bool `json:",omitempty bson:",omitempty"`
// // Treat resulting path as virtual, do not check if valid
// }
//
// func (f *Framework) GetNewPath(command string, cwd string) (string, error) {
// // "/usr/bin/python script"
// // to
// // "/path/to/script"
// regex, err := regexp.Compile(f.Find)
// if err != nil {
// return "", fmt.Errorf("profiles(framework): failed to compile framework regex: %s", err)
// }
// matched := regex.FindAllStringSubmatch(command, -1)
// if len(matched) == 0 || len(matched[0]) < 2 {
// return "", fmt.Errorf("profiles(framework): regex \"%s\" for constructing path did not match command \"%s\"", f.Find, command)
// }
//
// var lastError error
// var buildPath string
// for _, buildPath = range strings.Split(f.Build, "|") {
//
// buildPath = strings.Replace(buildPath, "{CWD}", cwd, -1)
// for i := 1; i < len(matched[0]); i++ {
// buildPath = strings.Replace(buildPath, fmt.Sprintf("{%d}", i), matched[0][i], -1)
// }
//
// buildPath = filepath.Clean(buildPath)
//
// if !f.Virtual {
// if !strings.HasPrefix(buildPath, "~/") && !filepath.IsAbs(buildPath) {
// lastError = fmt.Errorf("constructed path \"%s\" from framework is not absolute", buildPath)
// continue
// }
// if _, err := os.Stat(buildPath); os.IsNotExist(err) {
// lastError = fmt.Errorf("constructed path \"%s\" does not exist", buildPath)
// continue
// }
// }
//
// lastError = nil
// break
//
// }
//
// if lastError != nil {
// return "", fmt.Errorf("profiles(framework): failed to construct valid path, last error: %s", lastError)
// }
// log.Tracef("profiles(framework): transformed \"%s\" (%s) to \"%s\"", command, cwd, buildPath)
// return buildPath, nil
// }

30
profile/framework_test.go Normal file
View file

@ -0,0 +1,30 @@
package profile
// DEACTIVATED
// import (
// "testing"
// )
//
// func testGetNewPath(t *testing.T, f *Framework, command, cwd, expect string) {
// newPath, err := f.GetNewPath(command, cwd)
// if err != nil {
// t.Errorf("GetNewPath failed: %s", err)
// }
// if newPath != expect {
// t.Errorf("GetNewPath return unexpected result: got %s, expected %s", newPath, expect)
// }
// }
//
// func TestFramework(t *testing.T) {
// f1 := &Framework{
// Find: "([^ ]+)$",
// Build: "{CWD}/{1}",
// }
// testGetNewPath(t, f1, "/usr/bin/python bash", "/bin", "/bin/bash")
// f2 := &Framework{
// Find: "([^ ]+)$",
// Build: "{1}|{CWD}/{1}",
// }
// testGetNewPath(t, f2, "/usr/bin/python /bin/bash", "/tmp", "/bin/bash")
// }

101
profile/index/index.go Normal file
View file

@ -0,0 +1,101 @@
package index
import (
"sync"
"fmt"
"errors"
"encoding/base64"
"github.com/Safing/portbase/database/record"
"github.com/Safing/portbase/utils"
)
// ProfileIndex links an Identifier to Profiles
type ProfileIndex struct {
record.Base
sync.Mutex
ID string
UserProfiles []string
StampProfiles []string
}
func makeIndexRecordKey(id string) string {
return fmt.Sprintf("core:profiles/index/%s", base64.RawURLEncoding.EncodeToString([]byte(id)))
}
// NewIndex returns a new ProfileIndex.
func NewIndex(id string) *ProfileIndex {
return &ProfileIndex{
ID: id,
}
}
// AddUserProfile adds a User Profile to the index.
func (pi *ProfileIndex) AddUserProfile(id string) (changed bool) {
if !utils.StringInSlice(pi.UserProfiles, id) {
pi.UserProfiles = append(pi.UserProfiles, id)
return true
}
return false
}
// AddStampProfile adds a Stamp Profile to the index.
func (pi *ProfileIndex) AddStampProfile(id string) (changed bool) {
if !utils.StringInSlice(pi.StampProfiles, id) {
pi.StampProfiles = append(pi.StampProfiles, id)
return true
}
return false
}
// RemoveUserProfile removes a profile from the index.
func (pi *ProfileIndex) RemoveUserProfile(id string) {
pi.UserProfiles = utils.RemoveFromStringSlice(pi.UserProfiles, id)
}
// RemoveStampProfile removes a profile from the index.
func (pi *ProfileIndex) RemoveStampProfile(id string) {
pi.StampProfiles = utils.RemoveFromStringSlice(pi.StampProfiles, id)
}
// GetIndex gets a ProfileIndex from the database.
func GetIndex(id string) (*ProfileIndex, error) {
key := makeIndexRecordKey(id)
r, err := indexDB.Get(key)
if err != nil {
return nil, err
}
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
new := &ProfileIndex{}
err = record.Unwrap(r, new)
if err != nil {
return nil, err
}
return new, nil
}
// or adjust type
new, ok := r.(*ProfileIndex)
if !ok {
return nil, fmt.Errorf("record not of type *ProfileIndex, but %T", r)
}
return new, nil
}
// Save saves the Identifiers to the database
func (pi *ProfileIndex) Save() error {
if pi.Key() == "" {
if pi.ID != "" {
pi.SetKey(makeIndexRecordKey(pi.ID))
} else {
return errors.New("missing identification Key")
}
}
return indexDB.Put(pi)
}

101
profile/index/indexer.go Normal file
View file

@ -0,0 +1,101 @@
package index
import (
"strings"
"github.com/Safing/portbase/database"
"github.com/Safing/portbase/database/query"
"github.com/Safing/portbase/database/record"
"github.com/Safing/portbase/log"
"github.com/Safing/portbase/modules"
"github.com/Safing/portmaster/profile"
)
// FIXME: listen for profile changes and update the index
var (
indexDB = database.NewInterface(&database.Options{
Local: true, // we want to access crownjewel records
AlwaysMakeCrownjewel: true, // never sync the index
})
indexSub *database.Subscription
shutdownIndexer = make(chan struct{})
)
func init() {
modules.Register("profile:index", nil, start, stop, "database")
}
func start() (err error) {
indexSub, err = indexDB.Subscribe(query.New("core:profiles/user/"))
if err != nil {
return err
}
return nil
}
func stop() error {
close(shutdownIndexer)
indexSub.Cancel()
return nil
}
func indexer() {
for {
select {
case <-shutdownIndexer:
return
case r := <-indexSub.Feed:
prof := ensureProfile(r)
if prof != nil {
for _, id := range prof.Identifiers {
if strings.HasPrefix(id, profile.IdentifierPrefix) {
// get Profile and ensure identifier is set
pi, err := GetIndex(id)
if err != nil {
if err == database.ErrNotFound {
pi = NewIndex(id)
} else {
log.Errorf("profile/index: could not save updated profile index: %s", err)
}
}
if pi.AddUserProfile(prof.ID) {
err := pi.Save()
if err != nil {
log.Errorf("profile/index: could not save updated profile index: %s", err)
}
}
}
}
}
}
}
}
func ensureProfile(r record.Record) *profile.Profile {
// unwrap
if r.IsWrapped() {
// only allocate a new struct, if we need it
new := &profile.Profile{}
err := record.Unwrap(r, new)
if err != nil {
log.Errorf("profile/index: could not unwrap Profile: %s", err)
return nil
}
return new
}
// or adjust type
new, ok := r.(*profile.Profile)
if !ok {
log.Errorf("profile/index: record not of type *Profile, but %T", r)
return nil
}
return new
}

View file

@ -0,0 +1,17 @@
package matcher
import (
"github.com/Safing/portbase/database"
)
// core:profiles/user/12345-1234-125-1234-1235
// core:profiles/special/default
// /global
// core:profiles/stamp/12334-1235-1234-5123-1234
// core:profiles/identifier/base64
var (
profileDB = database.NewInterface(&database.Options{
Local: true, // we want to access crownjewel records (indexes are)
})
)

View file

@ -0,0 +1,23 @@
package matcher
import (
"strings"
"github.com/Safing/portmaster/process"
"github.com/Safing/portmaster/profile"
)
// CheckFingerprints checks what fingerprints match and returns the total score.
func CheckFingerprints(proc *process.Process, prof *profile.Profile) (score int, err error) {
// FIXME: kinda a dummy for now
for _, fp := range prof.Fingerprints {
if strings.HasPrefix(fp, "fullpath:") {
if fp[9:] == proc.Path {
return 3, nil
}
}
}
return 0, nil
}

View file

@ -0,0 +1,18 @@
package matcher
import (
"fmt"
"strings"
"github.com/Safing/portmaster/process"
"github.com/Safing/portmaster/profile"
)
// GetIdentificationPath returns the identifier for the given process (linux edition).
func GetIdentificationPath(p *process.Process) string {
splittedPath := strings.Split(p.Path, "/")
if len(splittedPath) > 3 {
return fmt.Sprintf("%s%s", profile.IdentifierPrefix, strings.Join(splittedPath[len(splittedPath)-3:len(splittedPath)], "/"))
}
return fmt.Sprintf("%s%s", profile.IdentifierPrefix, p.Path)
}

View file

@ -0,0 +1,25 @@
package matcher
import (
"testing"
"github.com/Safing/portmaster/process"
)
func TestGetIdentifierLinux(t *testing.T) {
p := &process.Process{
Path: "/usr/lib/firefox/firefox",
}
if GetIdentificationPath(p) != "lin:lib/firefox/firefox" {
t.Fatal("mismatch!")
}
p = &process.Process{
Path: "/opt/start",
}
if GetIdentificationPath(p) != "lin:/opt/start" {
t.Fatal("mismatch!")
}
}

View file

@ -0,0 +1,61 @@
package matcher
import (
"fmt"
"strings"
"github.com/Safing/portbase/log"
"github.com/Safing/portmaster/process"
"github.com/Safing/portmaster/profile"
"github.com/Safing/portmaster/profile/index"
)
// GetProfileSet finds a local profile.
func GetProfileSet(proc *process.Process) (set *profile.ProfileSet, err error) {
identPath := GetIdentificationPath(proc)
pi, err := index.GetIndex(identPath)
var bestScore int
var bestProfile *profile.Profile
for _, id := range pi.UserProfiles {
prof, err := profile.GetUserProfile(id)
if err != nil {
log.Errorf("profile/matcher: failed to load profile: %s", err)
continue
}
score, err := CheckFingerprints(proc, prof)
if score > bestScore {
bestScore = score
bestProfile = prof
}
}
if bestProfile == nil {
bestProfile = ProfileFromProcess(proc)
}
// FIXME: fetch stamp profile
set = profile.NewSet(bestProfile, nil)
return set, nil
}
// ProfileFromProcess creates an initial profile based on the given process.
func ProfileFromProcess(proc *process.Process) *profile.Profile {
new := profile.New()
splittedPath := strings.Split(proc.Path, "/")
new.Name = strings.ToTitle(splittedPath[len(splittedPath)-1])
new.Identifiers = append(new.Identifiers, GetIdentificationPath(proc))
new.Fingerprints = append(new.Fingerprints, fmt.Sprintf("fullpath:%s", proc.Path))
err := new.Save(profile.UserNamespace)
if err != nil {
log.Errorf("profile/matcher: could not save new profile: %s", new.Name)
}
return new
}

89
profile/ports.go Normal file
View file

@ -0,0 +1,89 @@
package profile
import (
"fmt"
"strconv"
"strings"
)
// Ports is a list of permitted or denied ports
type Ports map[string][]*Port
// IsSet returns whether the Ports object is "set".
func (p Ports) IsSet() bool {
if p != nil {
return true
}
return false
}
// CheckStatus returns whether listening/connecting to a certain port is allowed, and if this option is even set.
func (p Ports) CheckStatus(listen bool, protocol string, port uint16) (permit, ok bool) {
if p == nil {
return false, false
}
if listen {
protocol = "<" + protocol
}
portDefinitions, ok := p[protocol]
if ok {
for _, portD := range portDefinitions {
if portD.Matches(port) {
return portD.Permit, true
}
}
}
return false, true
}
func (p Ports) String() string {
var s []string
for protocol, ports := range p {
var portStrings []string
for _, port := range ports {
portStrings = append(portStrings, port.String())
}
s = append(s, fmt.Sprintf("%s:[%s]", protocol, strings.Join(portStrings, ", ")))
}
if len(s) == 0 {
return "None"
}
return strings.Join(s, ", ")
}
// Port represents a port range and a verdict.
type Port struct {
Permit bool
Created int64
Start uint16
End uint16
}
// Matches checks whether a port object matches the given port.
func (p Port) Matches(port uint16) bool {
if port >= p.Start && port <= p.End {
return true
}
return false
}
func (p Port) String() string {
var s string
if p.Permit {
s += "permit:"
} else {
s += "deny:"
}
if p.Start == p.End {
s += strconv.Itoa(int(p.Start))
} else {
s += fmt.Sprintf("%d-%d", p.Start, p.End)
}
return s
}

87
profile/profile.go Normal file
View file

@ -0,0 +1,87 @@
package profile
import (
"fmt"
"sync"
"github.com/satori/go.uuid"
"github.com/Safing/portbase/database/record"
"github.com/Safing/portmaster/status"
)
// Profile is used to predefine a security profile for applications.
type Profile struct {
record.Base
sync.Mutex
// Profile Metadata
ID string
Name string
Description string
Homepage string
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for a database path or "e:" for the encoded data.
Icon string
// Identification
Identifiers []string
Fingerprints []string
// The mininum security level to apply to connections made with this profile
SecurityLevel uint8
Flags ProfileFlags
Domains Domains
Ports Ports
StampProfileKey string
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
// Framework *Framework `json:",omitempty bson:",omitempty"`
// When this Profile was approximately last used (for performance reasons not every single usage is saved)
ApproxLastUsed int64
}
func New() *Profile {
return &Profile{}
}
// Save saves the profile to the database
func (profile *Profile) Save(namespace string) error {
if profile.ID == "" {
u, err := uuid.NewV4()
if err != nil {
return err
}
profile.ID = u.String()
}
if profile.Key() == "" {
if namespace == "" {
return fmt.Errorf("no key or namespace defined for profile %s", profile.String())
}
profile.SetKey(fmt.Sprintf("config:profiles/%s/%s", namespace, profile.ID))
}
return profileDB.Put(profile)
}
// String returns a string representation of the Profile.
func (profile *Profile) String() string {
return profile.Name
}
// DetailedString returns a more detailed string representation of theProfile.
func (profile *Profile) DetailedString() string {
return fmt.Sprintf("%s(SL=%s Flags=[%s] Ports=[%s] #Domains=%d)", profile.Name, status.FmtSecurityLevel(profile.SecurityLevel), profile.Flags.String(), profile.Ports.String(), len(profile.Domains))
}
// GetUserProfile loads a profile from the database.
func GetUserProfile(ID string) (*Profile, error) {
return nil, nil
}
// GetStampProfile loads a profile from the database.
func GetStampProfile(ID string) (*Profile, error) {
return nil, nil
}

121
profile/profileset.go Normal file
View file

@ -0,0 +1,121 @@
package profile
var (
emptyFlags = ProfileFlags{}
emptyPorts = Ports{}
)
// ProfileSet handles Profile chaining.
type ProfileSet struct {
Profiles [4]*Profile
// Application
// Global
// Stamp
// Default
Independent bool
}
// NewSet returns a new profile set with given the profiles.
func NewSet(user, stamp *Profile) *ProfileSet {
new := &ProfileSet{
Profiles: [4]*Profile{
user, // Application
nil, // Global
stamp, // Stamp
nil, // Default
},
}
new.Update()
return new
}
// Update gets the new global and default profile and updates the independence status. It must be called when reusing a profile set for a series of calls.
func (ps *ProfileSet) Update() {
specialProfileLock.RLock()
defer specialProfileLock.RUnlock()
// update profiles
ps.Profiles[1] = globalProfile
ps.Profiles[3] = defaultProfile
// update independence
if ps.Flags().Has(Independent, ps.SecurityLevel()) {
// Stamp profiles do not have the Independent flag
ps.Independent = true
} else {
ps.Independent = false
}
}
// Flags returns the highest prioritized ProfileFlags configuration.
func (ps *ProfileSet) Flags() ProfileFlags {
for _, profile := range ps.Profiles {
if profile != nil {
if profile.Flags.IsSet() {
return profile.Flags
}
}
}
return emptyFlags
}
// CheckDomainStatus checks if the given domain is governed in any the lists of domains and returns whether it is permitted.
func (ps *ProfileSet) CheckDomainStatus(domain string) (permit, ok bool) {
for i, profile := range ps.Profiles {
if i == 2 && ps.Independent {
continue
}
if profile != nil {
if profile.Domains.IsSet() {
permit, ok = profile.Domains.CheckStatus(domain)
if ok {
return
}
}
}
}
return false, false
}
// Ports returns the highest prioritized Ports configuration.
func (ps *ProfileSet) Ports() Ports {
for i, profile := range ps.Profiles {
if i == 2 && ps.Independent {
continue
}
if profile != nil {
if profile.Ports.IsSet() {
return profile.Ports
}
}
}
return emptyPorts
}
// SecurityLevel returns the highest prioritized security level.
func (ps *ProfileSet) SecurityLevel() uint8 {
for i, profile := range ps.Profiles {
if i == 2 {
// Stamp profiles do not have the SecurityLevel setting
continue
}
if profile != nil {
if profile.SecurityLevel > 0 {
return profile.SecurityLevel
}
}
}
return 0
}

261
profile/sampledata.go Normal file
View file

@ -0,0 +1,261 @@
package profile
// DEACTIVATED
// import (
// "runtime"
//
// "github.com/Safing/portbase/database"
// "github.com/Safing/portbase/log"
// )
//
// func init() {
//
// // Data here is for demo purposes, Profiles will be served over network soon™.
//
// log.Tracef("profiles: loading sample profiles for %s", runtime.GOOS)
//
// switch runtime.GOOS {
// case "linux":
//
// log.Trace("profiles: loading linux sample profiles")
//
// (&Profile{
// Name: "Chromium",
// Description: "Browser by Google",
// Path: "/usr/lib/chromium-browser/chromium-browser",
// Flags: []int8{User, Internet, LocalNet, Browser},
// ConnectPorts: []uint16{80, 443},
// }).CreateInDist()
//
// (&Profile{
// Name: "Evolution",
// Description: "PIM solution by GNOME",
// Path: "/usr/bin/evolution",
// Flags: []int8{User, Internet, Gateway},
// ConnectPorts: []uint16{25, 80, 143, 443, 465, 587, 993, 995},
// SecurityLevel: 2,
// }).CreateInDist()
//
// (&Profile{
// Name: "Evolution Calendar",
// Description: "PIM solution by GNOME - Calendar",
// Path: "/usr/lib/evolution/evolution-calendar-factory-subprocess",
// Flags: []int8{User, Internet, Gateway},
// ConnectPorts: []uint16{80, 443},
// SecurityLevel: 2,
// }).CreateInDist()
//
// (&Profile{
// Name: "Spotify",
// Description: "Music streaming",
// Path: "/usr/share/spotify/spotify",
// ConnectPorts: []uint16{80, 443, 4070},
// Flags: []int8{User, Internet, Strict},
// }).CreateInDist()
//
// (&Profile{
// // flatpak edition
// Name: "Spotify",
// Description: "Music streaming",
// Path: "/newroot/app/extra/share/spotify/spotify",
// ConnectPorts: []uint16{80, 443, 4070},
// Flags: []int8{User, Internet, Strict},
// }).CreateInDist()
//
// (&Profile{
// Name: "Evince",
// Description: "PDF Document Reader",
// Path: "/usr/bin/evince",
// Flags: []int8{},
// SecurityLevel: 2,
// }).CreateInDist()
//
// (&Profile{
// Name: "Ahavi",
// Description: "mDNS service",
// Path: "/usr/bin/avahi-daemon",
// Flags: []int8{System, LocalNet, Service, Directconnect},
// }).CreateInDist()
//
// (&Profile{
// Name: "Python 2.7 Framework",
// Description: "Correctly handle python scripts",
// Path: "/usr/bin/python2.7",
// Framework: &Framework{
// Find: "^[^ ]+ ([^ ]+)",
// Build: "{1}|{CWD}/{1}",
// },
// }).CreateInDist()
//
// (&Profile{
// Name: "Python 3.5 Framework",
// Description: "Correctly handle python scripts",
// Path: "/usr/bin/python3.5",
// Framework: &Framework{
// Find: "^[^ ]+ ([^ ]+)",
// Build: "{1}|{CWD}/{1}",
// },
// }).CreateInDist()
//
// (&Profile{
// Name: "DHCP Client",
// Description: "Client software for the DHCP protocol",
// Path: "/sbin/dhclient",
// Framework: &Framework{
// FindParent: 1,
// MergeWithParent: true,
// },
// }).CreateInDist()
//
// // Default Profiles
// // Until Profiles are distributed over the network, default profiles are activated when the Default Profile for "/" is missing.
//
// if ok, err := database.Has(ds.NewKey("/Data/Profiles/Profile_d-2f")); !ok || err != nil {
//
// log.Trace("profiles: loading linux default sample profiles")
//
// (&Profile{
// Name: "Default Base",
// Description: "Default Profile for /",
// Path: "/",
// Flags: []int8{Internet, LocalNet, Strict},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "Installed Applications",
// Description: "Default Profile for /usr/bin",
// Path: "/usr/bin/",
// Flags: []int8{Internet, LocalNet, Gateway},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "System Binaries (/sbin)",
// Description: "Default Profile for ~/Downloads",
// Path: "/sbin/",
// Flags: []int8{Internet, LocalNet, Directconnect, Service, System},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "System Binaries (/usr/sbin)",
// Description: "Default Profile for ~/Downloads",
// Path: "/usr/sbin/",
// Flags: []int8{Internet, LocalNet, Directconnect, Service, System},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "System Tmp folder",
// Description: "Default Profile for /tmp",
// Path: "/tmp/",
// Flags: []int8{}, // deny all
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "User Home",
// Description: "Default Profile for ~/",
// Path: "~/",
// Flags: []int8{Internet, LocalNet, Gateway},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "User Downloads",
// Description: "Default Profile for ~/Downloads",
// Path: "~/Downloads/",
// Flags: []int8{}, // deny all
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "User Cache",
// Description: "Default Profile for ~/.cache",
// Path: "~/.cache/",
// Flags: []int8{}, // deny all
// Default: true,
// }).Create()
//
// }
//
// case "windows":
//
// log.Trace("profiles: loading windows sample profiles")
//
// (&Profile{
// Name: "Firefox",
// Description: "Firefox Browser by Mozilla",
// Path: "C:\\Program Files\\Mozilla Firefox\\firefox.exe",
// Flags: []int8{User, Internet, LocalNet, Browser},
// ConnectPorts: []uint16{80, 443},
// }).CreateInDist()
//
// // Default Profiles
// // Until Profiles are distributed over the network, default profiles are activated when the Default Profile for "C" is missing.
//
// if ok, err := database.Has(ds.NewKey("/Data/Profiles/Profile:d-C")); !ok || err != nil {
//
// log.Trace("profiles: loading windows default sample profiles")
//
// (&Profile{
// Name: "Default Base",
// Description: "Default Profile for C",
// Path: "C",
// Flags: []int8{Internet, LocalNet, Strict},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "Installed Applications",
// Description: "Default Profile for C:\\Program Files",
// Path: "C:\\Program Files\\",
// Flags: []int8{Internet, LocalNet, Gateway},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "Installed Applications (x86)",
// Description: "Default Profile for C:\\Program Files (x86)",
// Path: "C:\\Program Files (x86)\\",
// Flags: []int8{Internet, LocalNet, Gateway},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "System Applications (C:\\Windows\\System32)",
// Description: "Default Profile for C:\\Windows\\System32",
// Path: "C:\\Windows\\System32\\",
// Flags: []int8{Internet, LocalNet, Directconnect, Service, System},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "User Home",
// Description: "Default Profile for ~/",
// Path: "~/",
// Flags: []int8{Internet, LocalNet, Gateway},
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "User Downloads",
// Description: "Default Profile for ~/Downloads",
// Path: "~/Downloads/",
// Flags: []int8{}, // deny all
// Default: true,
// }).Create()
//
// (&Profile{
// Name: "User Cache",
// Description: "Default Profile for ~/.cache",
// Path: "~/.cache/",
// Flags: []int8{}, // deny all
// Default: true,
// }).Create()
// }
// }
//
// }

View file

@ -0,0 +1,12 @@
package profile
import "sync"
var (
globalProfile *Profile
defaultProfile *Profile
specialProfileLock sync.RWMutex
)
// FIXME: subscribe to changes and update profiles

View file

@ -1,76 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package profiles
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/Safing/safing-core/log"
)
type Framework struct {
// go hirarchy up
FindParent uint8 `json:",omitempty bson:",omitempty"`
// get path from parent, amount of levels to go up the tree (1 means parent, 2 means parent of parents, and so on)
MergeWithParent bool `json:",omitempty bson:",omitempty"`
// instead of getting the path of the parent, merge with it by presenting connections as if they were from that parent
// go hirarchy down
Find string `json:",omitempty bson:",omitempty"`
// Regular expression for finding path elements
Build string `json:",omitempty bson:",omitempty"`
// Path definitions for building path
Virtual bool `json:",omitempty bson:",omitempty"`
// Treat resulting path as virtual, do not check if valid
}
func (f *Framework) GetNewPath(command string, cwd string) (string, error) {
// "/usr/bin/python script"
// to
// "/path/to/script"
regex, err := regexp.Compile(f.Find)
if err != nil {
return "", fmt.Errorf("profiles(framework): failed to compile framework regex: %s", err)
}
matched := regex.FindAllStringSubmatch(command, -1)
if len(matched) == 0 || len(matched[0]) < 2 {
return "", fmt.Errorf("profiles(framework): regex \"%s\" for constructing path did not match command \"%s\"", f.Find, command)
}
var lastError error
var buildPath string
for _, buildPath = range strings.Split(f.Build, "|") {
buildPath = strings.Replace(buildPath, "{CWD}", cwd, -1)
for i := 1; i < len(matched[0]); i++ {
buildPath = strings.Replace(buildPath, fmt.Sprintf("{%d}", i), matched[0][i], -1)
}
buildPath = filepath.Clean(buildPath)
if !f.Virtual {
if !strings.HasPrefix(buildPath, "~/") && !filepath.IsAbs(buildPath) {
lastError = fmt.Errorf("constructed path \"%s\" from framework is not absolute", buildPath)
continue
}
if _, err := os.Stat(buildPath); os.IsNotExist(err) {
lastError = fmt.Errorf("constructed path \"%s\" does not exist", buildPath)
continue
}
}
lastError = nil
break
}
if lastError != nil {
return "", fmt.Errorf("profiles(framework): failed to construct valid path, last error: %s", lastError)
}
log.Tracef("profiles(framework): transformed \"%s\" (%s) to \"%s\"", command, cwd, buildPath)
return buildPath, nil
}

View file

@ -1,30 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package profiles
import (
"testing"
)
func testGetNewPath(t *testing.T, f *Framework, command, cwd, expect string) {
newPath, err := f.GetNewPath(command, cwd)
if err != nil {
t.Errorf("GetNewPath failed: %s", err)
}
if newPath != expect {
t.Errorf("GetNewPath return unexpected result: got %s, expected %s", newPath, expect)
}
}
func TestFramework(t *testing.T) {
f1 := &Framework{
Find: "([^ ]+)$",
Build: "{CWD}/{1}",
}
testGetNewPath(t, f1, "/usr/bin/python bash", "/bin", "/bin/bash")
f2 := &Framework{
Find: "([^ ]+)$",
Build: "{1}|{CWD}/{1}",
}
testGetNewPath(t, f2, "/usr/bin/python /bin/bash", "/tmp", "/bin/bash")
}

View file

@ -1,265 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package profiles
import (
"encoding/hex"
"strings"
datastore "github.com/ipfs/go-datastore"
dsq "github.com/ipfs/go-datastore/query"
"github.com/Safing/safing-core/database"
"github.com/Safing/safing-core/intel"
"github.com/Safing/safing-core/log"
)
// Profile is used to predefine a security profile for applications.
type Profile struct {
database.Base
Name string
Path string
Description string `json:",omitempty bson:",omitempty"`
Icon string `json:",omitempty bson:",omitempty"`
// Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache.
// TODO: Think more about using one profile for multiple paths
// Refer string `json:",omitempty bson:",omitempty"`
// If a Profile is declared as a Framework (i.e. an Interpreter and the likes), then the real process must be found
Framework *Framework `json:",omitempty bson:",omitempty"`
// The format how to real process is to be found is yet to be defined.
// Ideas:
// - Regex for finding the executed script in the arguments, prepend working directory if path is not absolute
// - Parent Process?
// Use Cases:
// - Interpreters (Python, Java, ...)
// - Sandboxes (Flatpak, Snapd, Docker, ...)
// - Subprocesses of main application process
SecurityLevel int8 `json:",omitempty bson:",omitempty"`
// The mininum security level to apply to connections made with this profile
Flags ProfileFlags
ClassificationBlacklist *intel.EntityClassification `json:",omitempty bson:",omitempty"`
ClassificationWhitelist *intel.EntityClassification `json:",omitempty bson:",omitempty"`
DomainWhitelistIsBlacklist bool `json:",omitempty bson:",omitempty"`
DomainWhitelist []string `json:",omitempty bson:",omitempty"`
ConnectPorts []uint16 `json:",omitempty bson:",omitempty"`
ListenPorts []uint16 `json:",omitempty bson:",omitempty"`
Default bool `json:",omitempty bson:",omitempty"`
// This flag indicates that this profile is a default profile. If no profile is found for a process, the default profile with the longest matching prefix path is used.
PromptUserToAdapt bool `json:",omitempty bson:",omitempty"`
// This flag is only valid for default profiles and indicates that should this profile be used for a process, the user will be prompted to adapt it for the process and save a new profile.
Authoritative bool `json:",omitempty bson:",omitempty"`
// This flag indicates that this profile was loaded from an authoritative source - the Safing Community or the Company. Authoritative Profiles that have been modified can be reverted back to their original state.
Locked bool `json:",omitempty bson:",omitempty"`
// This flag indicates that this profile was locked by the company. This means that the profile may not be edited by the user. If an authoritative default profile is locked, it wins over non-authoritative profiles and the user will not be prompted to adapt the profile, should the PromptUserToAdapt flag be set.
Modified bool `json:",omitempty bson:",omitempty"`
// This flag indicates that this profile has been modified by the user. Non-modified authoritative profiles will be automatically overwritten with new versions.
Orphaned bool `json:",omitempty bson:",omitempty"`
// This flag indicates that the associated program (on path) does not exist (Either this entry was manually created, or the program has been uninstalled). Only valid for non-default profiles.
ApproxLastUsed int64 `json:",omitempty bson:",omitempty"`
// When this Profile was approximately last used (for performance reasons not every single usage is saved)
}
var profileModel *Profile // only use this as parameter for database.EnsureModel-like functions
func init() {
database.RegisterModel(profileModel, func() database.Model { return new(Profile) })
}
// Create saves Profile with the provided name in the Profiles namespace.
func (m *Profile) Create() error {
name := hex.EncodeToString([]byte(m.Path))
if m.Default {
name = "d-" + name
}
return m.CreateObject(&database.Profiles, name, m)
}
// CreateInNamespace saves Profile with the provided name in the provided namespace.
func (m *Profile) CreateInNamespace(namespace *datastore.Key) error {
name := hex.EncodeToString([]byte(m.Path))
if m.Default {
name = "d-" + name
}
return m.CreateObject(namespace, name, m)
}
// CreateInDist saves Profile with the (hash of the) path as the name in the Dist namespace.
func (m *Profile) CreateInDist() error {
return m.CreateInNamespace(&database.DistProfiles)
}
// CreateInCompany saves Profile with the (hash of the) path as the name in the Company namespace.
func (m *Profile) CreateInCompany() error {
return m.CreateInNamespace(&database.CompanyProfiles)
}
// Save saves Profile.
func (m *Profile) Save() error {
return m.SaveObject(m)
}
// String returns a string representation of Profile.
func (m *Profile) String() string {
if m.Default {
return "[D] " + m.Name
}
return m.Name
}
// GetProfile fetches Profile with the provided name from the default namespace.
func GetProfile(name string) (*Profile, error) {
return GetProfileFromNamespace(&database.Profiles, name)
}
// GetProfileFromNamespace fetches Profile with the provided name from the provided namespace.
func GetProfileFromNamespace(namespace *datastore.Key, name string) (*Profile, error) {
object, err := database.GetAndEnsureModel(namespace, name, profileModel)
if err != nil {
return nil, err
}
model, ok := object.(*Profile)
if !ok {
return nil, database.NewMismatchError(object, profileModel)
}
return model, nil
}
// GetActiveProfileByPath fetches Profile with the (hash of the) path as the name from the default namespace.
func GetActiveProfileByPath(path string) (*Profile, error) {
return GetProfileFromNamespace(&database.Profiles, hex.EncodeToString([]byte(path)))
// TODO: check for locked authoritative default profiles
}
// FindProfileByPath looks for a Profile first in the Company namespace and then in the Dist namespace. Should no Profile be available it searches for a Default Profile.
func FindProfileByPath(path, homeDir string) (profile *Profile, err error) {
name := hex.EncodeToString([]byte(path))
var homeName string
slashedHomeDir := strings.TrimRight(homeDir, "/") + "/"
if homeDir != "" && strings.HasPrefix(path, slashedHomeDir) {
homeName = hex.EncodeToString([]byte("~/" + path[len(slashedHomeDir):]))
}
// check for available company profiles
profile, err = GetProfileFromNamespace(&database.CompanyProfiles, name)
if err != database.ErrNotFound {
if err == nil {
return profile.Activate()
}
return
}
if homeName != "" {
profile, err = GetProfileFromNamespace(&database.CompanyProfiles, homeName)
if err != database.ErrNotFound {
if err == nil {
return profile.Activate()
}
return
}
}
// check for available dist profiles
profile, err = GetProfileFromNamespace(&database.DistProfiles, name)
if err != database.ErrNotFound {
if err == nil {
return profile.Activate()
}
return
}
if homeName != "" {
profile, err = GetProfileFromNamespace(&database.DistProfiles, homeName)
if err != database.ErrNotFound {
if err == nil {
return profile.Activate()
}
return
}
}
// search for best-matching default profile
err = nil
profile, _ = SearchForDefaultProfile(name, homeName, len(slashedHomeDir)-2, &database.Profiles)
return
}
func (m *Profile) Activate() (*Profile, error) {
return m, m.Create()
}
func SearchForDefaultProfile(matchKey, matchHomeKey string, addHomeDirLen int, namespace *datastore.Key) (*Profile, int) {
// log.Tracef("profiles: searching for default profile with %s", matchKey)
query := dsq.Query{
Prefix: namespace.ChildString("Profile:d-").String(),
}
// filter := LongestMatch{
// Offset: len(query.Prefix),
// Longest: 0,
// Match: hex.EncodeToString([]byte(path)),
// }
// query.Filters = []dsq.Filter{
// filter,
// }
prefixOffset := len(query.Prefix)
longest := 0
var longestMatch interface{}
currentLongestIsHomeBased := false
currentLength := 0
iterator, err := database.Query(query)
if err != nil {
return nil, 0
}
for entry, ok := iterator.NextSync(); ok; entry, ok = iterator.NextSync() {
// log.Tracef("profiles: checking %s for default profile", entry.Key)
// TODO: prioritize locked profiles
prefix := entry.Key[prefixOffset:]
// skip directly if current longest match is longer than the key
// log.Tracef("profiles: comparing %s to %s", matchKey, prefix)
switch {
case strings.HasPrefix(matchKey, prefix):
currentLength = len(prefix)
currentLongestIsHomeBased = false
case strings.HasPrefix(matchHomeKey, prefix):
currentLength = len(prefix) + addHomeDirLen
currentLongestIsHomeBased = true
default:
continue
}
// longest wins, if a root-based and home-based tie, root-based wins.
if currentLength > longest || (currentLongestIsHomeBased && currentLength == longest) {
longest = currentLength
longestMatch = entry.Value
// log.Tracef("profiles: found new longest (%d) default profile match: %s", currentLength, entry.Key)
}
}
if longestMatch == nil {
return nil, 0
}
matched, ok := longestMatch.(database.Model)
if !ok {
log.Warningf("profiles: matched default profile is not of type database.Model")
return nil, 0
}
profile, ok := database.SilentEnsureModel(matched, profileModel).(*Profile)
if !ok {
log.Warningf("profiles: matched default profile is not of type *Profile")
return nil, 0
}
return profile, longest
}

View file

@ -1,115 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package profiles
import (
"errors"
"strings"
)
// ProfileFlags are used to quickly add common attributes to profiles
type ProfileFlags []int8
const (
// Who?
// System apps must be run by system user, else deny
System int8 = iota + 1
// Admin apps must be run by user with admin privileges, else deny
Admin
// User apps must be run by user (identified by having an active safing UI), else deny
User
// Where?
// Internet apps may connect to the Internet, if unset, all connections to the Internet are denied
Internet
// LocalNet apps may connect to the local network (i.e. private IP address spaces), if unset, all connections to the local network are denied
LocalNet
// How?
// Strict apps may only connect to domains that are related to themselves
Strict
// Gateway apps will connect to user-defined servers
Gateway
// Browser apps connect to multitudes of different servers and require special handling
Browser
// Directconnect apps may connect to any IP without dns association (e.g. P2P apps, network analysis tools)
Directconnect
// Service apps may accept incoming connections
Service
)
var (
// ErrProfileFlagsParseFailed is returned if a an invalid flag is encountered while parsing
ErrProfileFlagsParseFailed = errors.New("profiles: failed to parse flags")
sortedFlags = &ProfileFlags{
System,
Admin,
User,
Internet,
LocalNet,
Strict,
Gateway,
Service,
Directconnect,
Browser,
}
flagIDs = map[string]int8{
"System": System,
"Admin": Admin,
"User": User,
"Internet": Internet,
"LocalNet": LocalNet,
"Strict": Strict,
"Gateway": Gateway,
"Service": Service,
"Directconnect": Directconnect,
"Browser": Browser,
}
flagNames = map[int8]string{
System: "System",
Admin: "Admin",
User: "User",
Internet: "Internet",
LocalNet: "LocalNet",
Strict: "Strict",
Gateway: "Gateway",
Service: "Service",
Directconnect: "Directconnect",
Browser: "Browser",
}
)
// FlagsFromNames creates ProfileFlags from a comma seperated list of flagnames (e.g. "System,Strict,Secure")
func FlagsFromNames(words []string) (*ProfileFlags, error) {
var flags ProfileFlags
for _, entry := range words {
flag, ok := flagIDs[entry]
if !ok {
return nil, ErrProfileFlagsParseFailed
}
flags = append(flags, flag)
}
return &flags, nil
}
// Has checks if a ProfileFlags object has a flag
func (pf *ProfileFlags) Has(searchFlag int8) bool {
for _, flag := range *pf {
if flag == searchFlag {
return true
}
}
return false
}
// String return a string representation of ProfileFlags
func (pf *ProfileFlags) String() string {
var namedFlags []string
for _, flag := range *pf {
namedFlags = append(namedFlags, flagNames[flag])
}
return strings.Join(namedFlags, ",")
}

View file

@ -1,65 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package profiles
import (
"strings"
"testing"
)
func TestProfileFlags(t *testing.T) {
// check if SYSTEM is 1
if System != 1 {
t.Errorf("System ist first const and must be 1")
}
if Admin != 2 {
t.Errorf("Admin ist second const and must be 2")
}
// check if all IDs have a name
for key, entry := range flagIDs {
if _, ok := flagNames[entry]; !ok {
t.Errorf("could not find entry for %s in flagNames", key)
}
}
// check if all names have an ID
for key, entry := range flagNames {
if _, ok := flagIDs[entry]; !ok {
t.Errorf("could not find entry for %d in flagNames", key)
}
}
// check Has
emptyFlags := ProfileFlags{}
for flag, name := range flagNames {
if !sortedFlags.Has(flag) {
t.Errorf("sortedFlags should have flag %s (%d)", name, flag)
}
if emptyFlags.Has(flag) {
t.Errorf("emptyFlags should not have flag %s (%d)", name, flag)
}
}
// check ProfileFlags creation from strings
var allFlagStrings []string
for _, flag := range *sortedFlags {
allFlagStrings = append(allFlagStrings, flagNames[flag])
}
newFlags, err := FlagsFromNames(allFlagStrings)
if err != nil {
t.Errorf("error while parsing flags: %s", err)
}
if newFlags.String() != sortedFlags.String() {
t.Errorf("parsed flags are not correct (or tests have not been updated to reflect the right number), expected %v, got %v", *sortedFlags, *newFlags)
}
// check ProfileFlags Stringer
flagString := newFlags.String()
check := strings.Join(allFlagStrings, ",")
if flagString != check {
t.Errorf("flag string is not correct, expected %s, got %s", check, flagString)
}
}

View file

@ -1,263 +0,0 @@
// Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the AGPL license that can be found in the LICENSE file.
package profiles
import (
"runtime"
ds "github.com/ipfs/go-datastore"
"github.com/Safing/safing-core/database"
"github.com/Safing/safing-core/log"
)
func init() {
// Data here is for demo purposes, Profiles will be served over network soon™.
log.Tracef("profiles: loading sample profiles for %s", runtime.GOOS)
switch runtime.GOOS {
case "linux":
log.Trace("profiles: loading linux sample profiles")
(&Profile{
Name: "Chromium",
Description: "Browser by Google",
Path: "/usr/lib/chromium-browser/chromium-browser",
Flags: []int8{User, Internet, LocalNet, Browser},
ConnectPorts: []uint16{80, 443},
}).CreateInDist()
(&Profile{
Name: "Evolution",
Description: "PIM solution by GNOME",
Path: "/usr/bin/evolution",
Flags: []int8{User, Internet, Gateway},
ConnectPorts: []uint16{25, 80, 143, 443, 465, 587, 993, 995},
SecurityLevel: 2,
}).CreateInDist()
(&Profile{
Name: "Evolution Calendar",
Description: "PIM solution by GNOME - Calendar",
Path: "/usr/lib/evolution/evolution-calendar-factory-subprocess",
Flags: []int8{User, Internet, Gateway},
ConnectPorts: []uint16{80, 443},
SecurityLevel: 2,
}).CreateInDist()
(&Profile{
Name: "Spotify",
Description: "Music streaming",
Path: "/usr/share/spotify/spotify",
ConnectPorts: []uint16{80, 443, 4070},
Flags: []int8{User, Internet, Strict},
}).CreateInDist()
(&Profile{
// flatpak edition
Name: "Spotify",
Description: "Music streaming",
Path: "/newroot/app/extra/share/spotify/spotify",
ConnectPorts: []uint16{80, 443, 4070},
Flags: []int8{User, Internet, Strict},
}).CreateInDist()
(&Profile{
Name: "Evince",
Description: "PDF Document Reader",
Path: "/usr/bin/evince",
Flags: []int8{},
SecurityLevel: 2,
}).CreateInDist()
(&Profile{
Name: "Ahavi",
Description: "mDNS service",
Path: "/usr/bin/avahi-daemon",
Flags: []int8{System, LocalNet, Service, Directconnect},
}).CreateInDist()
(&Profile{
Name: "Python 2.7 Framework",
Description: "Correctly handle python scripts",
Path: "/usr/bin/python2.7",
Framework: &Framework{
Find: "^[^ ]+ ([^ ]+)",
Build: "{1}|{CWD}/{1}",
},
}).CreateInDist()
(&Profile{
Name: "Python 3.5 Framework",
Description: "Correctly handle python scripts",
Path: "/usr/bin/python3.5",
Framework: &Framework{
Find: "^[^ ]+ ([^ ]+)",
Build: "{1}|{CWD}/{1}",
},
}).CreateInDist()
(&Profile{
Name: "DHCP Client",
Description: "Client software for the DHCP protocol",
Path: "/sbin/dhclient",
Framework: &Framework{
FindParent: 1,
MergeWithParent: true,
},
}).CreateInDist()
// Default Profiles
// Until Profiles are distributed over the network, default profiles are activated when the Default Profile for "/" is missing.
if ok, err := database.Has(ds.NewKey("/Data/Profiles/Profile_d-2f")); !ok || err != nil {
log.Trace("profiles: loading linux default sample profiles")
(&Profile{
Name: "Default Base",
Description: "Default Profile for /",
Path: "/",
Flags: []int8{Internet, LocalNet, Strict},
Default: true,
}).Create()
(&Profile{
Name: "Installed Applications",
Description: "Default Profile for /usr/bin",
Path: "/usr/bin/",
Flags: []int8{Internet, LocalNet, Gateway},
Default: true,
}).Create()
(&Profile{
Name: "System Binaries (/sbin)",
Description: "Default Profile for ~/Downloads",
Path: "/sbin/",
Flags: []int8{Internet, LocalNet, Directconnect, Service, System},
Default: true,
}).Create()
(&Profile{
Name: "System Binaries (/usr/sbin)",
Description: "Default Profile for ~/Downloads",
Path: "/usr/sbin/",
Flags: []int8{Internet, LocalNet, Directconnect, Service, System},
Default: true,
}).Create()
(&Profile{
Name: "System Tmp folder",
Description: "Default Profile for /tmp",
Path: "/tmp/",
Flags: []int8{}, // deny all
Default: true,
}).Create()
(&Profile{
Name: "User Home",
Description: "Default Profile for ~/",
Path: "~/",
Flags: []int8{Internet, LocalNet, Gateway},
Default: true,
}).Create()
(&Profile{
Name: "User Downloads",
Description: "Default Profile for ~/Downloads",
Path: "~/Downloads/",
Flags: []int8{}, // deny all
Default: true,
}).Create()
(&Profile{
Name: "User Cache",
Description: "Default Profile for ~/.cache",
Path: "~/.cache/",
Flags: []int8{}, // deny all
Default: true,
}).Create()
}
case "windows":
log.Trace("profiles: loading windows sample profiles")
(&Profile{
Name: "Firefox",
Description: "Firefox Browser by Mozilla",
Path: "C:\\Program Files\\Mozilla Firefox\\firefox.exe",
Flags: []int8{User, Internet, LocalNet, Browser},
ConnectPorts: []uint16{80, 443},
}).CreateInDist()
// Default Profiles
// Until Profiles are distributed over the network, default profiles are activated when the Default Profile for "C" is missing.
if ok, err := database.Has(ds.NewKey("/Data/Profiles/Profile:d-C")); !ok || err != nil {
log.Trace("profiles: loading windows default sample profiles")
(&Profile{
Name: "Default Base",
Description: "Default Profile for C",
Path: "C",
Flags: []int8{Internet, LocalNet, Strict},
Default: true,
}).Create()
(&Profile{
Name: "Installed Applications",
Description: "Default Profile for C:\\Program Files",
Path: "C:\\Program Files\\",
Flags: []int8{Internet, LocalNet, Gateway},
Default: true,
}).Create()
(&Profile{
Name: "Installed Applications (x86)",
Description: "Default Profile for C:\\Program Files (x86)",
Path: "C:\\Program Files (x86)\\",
Flags: []int8{Internet, LocalNet, Gateway},
Default: true,
}).Create()
(&Profile{
Name: "System Applications (C:\\Windows\\System32)",
Description: "Default Profile for C:\\Windows\\System32",
Path: "C:\\Windows\\System32\\",
Flags: []int8{Internet, LocalNet, Directconnect, Service, System},
Default: true,
}).Create()
(&Profile{
Name: "User Home",
Description: "Default Profile for ~/",
Path: "~/",
Flags: []int8{Internet, LocalNet, Gateway},
Default: true,
}).Create()
(&Profile{
Name: "User Downloads",
Description: "Default Profile for ~/Downloads",
Path: "~/Downloads/",
Flags: []int8{}, // deny all
Default: true,
}).Create()
(&Profile{
Name: "User Cache",
Description: "Default Profile for ~/.cache",
Path: "~/.cache/",
Flags: []int8{}, // deny all
Default: true,
}).Create()
}
}
}