mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
438 lines
11 KiB
Go
438 lines
11 KiB
Go
package profile
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/exp/slices"
|
|
|
|
"github.com/safing/jess/lhash"
|
|
"github.com/safing/structures/container"
|
|
)
|
|
|
|
// # Matching and Scores
|
|
//
|
|
// There are three levels:
|
|
//
|
|
// 1. Type: What matched?
|
|
// 1. Tag: 50.000 points
|
|
// 2. Cmdline: 40.000 points
|
|
// 3. Env: 30.000 points
|
|
// 4. MatchingPath: 20.000 points
|
|
// 5. Path: 10.000 points
|
|
// 2. Operation: How was it mached?
|
|
// 1. Equals: 3.000 points
|
|
// 2. Prefix: 2.000 points
|
|
// 3. Regex: 1.000 points
|
|
// 3. How "strong" was the match?
|
|
// 1. Equals: Length of path (irrelevant)
|
|
// 2. Prefix: Length of prefix
|
|
// 3. Regex: Length of match
|
|
|
|
// Fingerprint Type IDs.
|
|
const (
|
|
FingerprintTypeTagID = "tag"
|
|
FingerprintTypeCmdlineID = "cmdline"
|
|
FingerprintTypeEnvID = "env"
|
|
FingerprintTypePathID = "path" // Matches both MatchingPath and Path.
|
|
|
|
FingerprintOperationEqualsID = "equals"
|
|
FingerprintOperationPrefixID = "prefix"
|
|
FingerprintOperationRegexID = "regex"
|
|
|
|
tagMatchBaseScore = 50_000
|
|
cmdlineMatchBaseScore = 40_000
|
|
envMatchBaseScore = 30_000
|
|
matchingPathMatchBaseScore = 20_000
|
|
pathMatchBaseScore = 10_000
|
|
|
|
fingerprintEqualsBaseScore = 3_000
|
|
fingerprintPrefixBaseScore = 2_000
|
|
fingerprintRegexBaseScore = 1_000
|
|
|
|
maxMatchStrength = 499
|
|
)
|
|
|
|
type (
|
|
// Fingerprint defines a way of matching a process.
|
|
// The Key is only valid - but required - for some types.
|
|
Fingerprint struct {
|
|
Type string
|
|
Key string // Key must always fully match.
|
|
Operation string
|
|
Value string
|
|
|
|
// MergedFrom holds the ID of the profile from which this fingerprint was
|
|
// merged from. The merged profile should create a new profile ID derived
|
|
// from the new fingerprints and add all fingerprints with this field set
|
|
// to the originating profile ID
|
|
MergedFrom string // `json:"mergedFrom,omitempty"`
|
|
}
|
|
|
|
// Tag represents a simple key/value kind of tag used in process metadata
|
|
// and fingerprints.
|
|
Tag struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
// MatchingData is an interface to fetching data in the matching process.
|
|
MatchingData interface {
|
|
Tags() []Tag
|
|
Env() map[string]string
|
|
Path() string
|
|
MatchingPath() string
|
|
Cmdline() string
|
|
}
|
|
|
|
matchingFingerprint interface {
|
|
MatchesKey(key string) bool
|
|
Match(value string) (score int)
|
|
}
|
|
)
|
|
|
|
// MatchesKey returns whether the optional fingerprint key (for some types
|
|
// only) matches the given key.
|
|
func (fp Fingerprint) MatchesKey(key string) bool {
|
|
return key == fp.Key
|
|
}
|
|
|
|
// KeyInTags checks is the given key is in the tags.
|
|
func KeyInTags(tags []Tag, key string) bool {
|
|
for _, tag := range tags {
|
|
if key == tag.Key {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// KeyAndValueInTags checks is the given key/value pair is in the tags.
|
|
func KeyAndValueInTags(tags []Tag, key, value string) bool {
|
|
for _, tag := range tags {
|
|
if key == tag.Key && value == tag.Value {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type fingerprintEquals struct {
|
|
Fingerprint
|
|
}
|
|
|
|
func (fp fingerprintEquals) Match(value string) (score int) {
|
|
if value == fp.Value {
|
|
return fingerprintEqualsBaseScore + checkMatchStrength(len(fp.Value))
|
|
}
|
|
return 0
|
|
}
|
|
|
|
type fingerprintPrefix struct {
|
|
Fingerprint
|
|
}
|
|
|
|
func (fp fingerprintPrefix) Match(value string) (score int) {
|
|
if strings.HasPrefix(value, fp.Value) {
|
|
return fingerprintPrefixBaseScore + checkMatchStrength(len(fp.Value))
|
|
}
|
|
return 0
|
|
}
|
|
|
|
type fingerprintRegex struct {
|
|
Fingerprint
|
|
regex *regexp.Regexp
|
|
}
|
|
|
|
func (fp fingerprintRegex) Match(value string) (score int) {
|
|
// Find best match.
|
|
for _, match := range fp.regex.FindAllString(value, -1) {
|
|
// Save match length if higher than score.
|
|
// This will also ignore empty matches.
|
|
if len(match) > score {
|
|
score = len(match)
|
|
}
|
|
}
|
|
|
|
// Add base score and return if anything was found.
|
|
if score > 0 {
|
|
return fingerprintRegexBaseScore + checkMatchStrength(score)
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// ParsedFingerprints holds parsed fingerprints for fast usage.
|
|
type ParsedFingerprints struct {
|
|
tagPrints []matchingFingerprint
|
|
envPrints []matchingFingerprint
|
|
pathPrints []matchingFingerprint
|
|
cmdlinePrints []matchingFingerprint
|
|
}
|
|
|
|
// ParseFingerprints parses the fingerprints to make them ready for matching.
|
|
func ParseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *ParsedFingerprints, firstErr error) {
|
|
parsed = &ParsedFingerprints{}
|
|
|
|
// Add deprecated LinkedPath to fingerprints, if they are empty.
|
|
// TODO: Remove in v1.5
|
|
if len(raw) == 0 && deprecatedLinkedPath != "" {
|
|
parsed.pathPrints = append(parsed.pathPrints, &fingerprintEquals{
|
|
Fingerprint: Fingerprint{
|
|
Type: FingerprintTypePathID,
|
|
Operation: FingerprintOperationEqualsID,
|
|
Value: deprecatedLinkedPath,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Parse all fingerprints.
|
|
// Do not fail when one fails, instead return the first encountered error.
|
|
for _, entry := range raw {
|
|
// Check type and required key.
|
|
switch entry.Type {
|
|
case FingerprintTypeTagID, FingerprintTypeEnvID:
|
|
if entry.Key == "" {
|
|
if firstErr == nil {
|
|
firstErr = fmt.Errorf("%s fingerprint is missing key", entry.Type)
|
|
}
|
|
continue
|
|
}
|
|
case FingerprintTypePathID, FingerprintTypeCmdlineID:
|
|
// Don't need a key.
|
|
default:
|
|
// Unknown type.
|
|
if firstErr == nil {
|
|
firstErr = fmt.Errorf("unknown fingerprint type: %q", entry.Type)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Create and/or collect operation match functions.
|
|
switch entry.Operation {
|
|
case FingerprintOperationEqualsID:
|
|
parsed.addMatchingFingerprint(entry, fingerprintEquals{entry})
|
|
|
|
case FingerprintOperationPrefixID:
|
|
parsed.addMatchingFingerprint(entry, fingerprintPrefix{entry})
|
|
|
|
case FingerprintOperationRegexID:
|
|
regex, err := regexp.Compile(entry.Value)
|
|
if err != nil {
|
|
if firstErr == nil {
|
|
firstErr = fmt.Errorf("failed to compile regex fingerprint: %s", entry.Value)
|
|
}
|
|
} else {
|
|
parsed.addMatchingFingerprint(entry, fingerprintRegex{
|
|
Fingerprint: entry,
|
|
regex: regex,
|
|
})
|
|
}
|
|
|
|
default:
|
|
if firstErr == nil {
|
|
firstErr = fmt.Errorf("unknown fingerprint operation: %q", entry.Operation)
|
|
}
|
|
}
|
|
}
|
|
|
|
return parsed, firstErr
|
|
}
|
|
|
|
func (parsed *ParsedFingerprints) addMatchingFingerprint(fp Fingerprint, matchingPrint matchingFingerprint) {
|
|
switch fp.Type {
|
|
case FingerprintTypeTagID:
|
|
parsed.tagPrints = append(parsed.tagPrints, matchingPrint)
|
|
case FingerprintTypeEnvID:
|
|
parsed.envPrints = append(parsed.envPrints, matchingPrint)
|
|
case FingerprintTypePathID:
|
|
parsed.pathPrints = append(parsed.pathPrints, matchingPrint)
|
|
case FingerprintTypeCmdlineID:
|
|
parsed.cmdlinePrints = append(parsed.cmdlinePrints, matchingPrint)
|
|
default:
|
|
// This should never happen, as the types are checked already.
|
|
panic(fmt.Sprintf("unknown fingerprint type: %q", fp.Type))
|
|
}
|
|
}
|
|
|
|
// MatchFingerprints returns the highest matching score of the given
|
|
// fingerprints and matching data.
|
|
func MatchFingerprints(prints *ParsedFingerprints, md MatchingData) (highestScore int) {
|
|
// Check tags.
|
|
tags := md.Tags()
|
|
if len(tags) > 0 {
|
|
for _, tagPrint := range prints.tagPrints {
|
|
for _, tag := range tags {
|
|
// Check if tag key matches.
|
|
if !tagPrint.MatchesKey(tag.Key) {
|
|
continue
|
|
}
|
|
|
|
// Try matching the tag value.
|
|
score := tagPrint.Match(tag.Value)
|
|
if score > highestScore {
|
|
highestScore = score
|
|
}
|
|
}
|
|
}
|
|
// If something matched, add base score and return.
|
|
if highestScore > 0 {
|
|
return tagMatchBaseScore + highestScore
|
|
}
|
|
}
|
|
|
|
// Check cmdline.
|
|
cmdline := md.Cmdline()
|
|
if cmdline != "" {
|
|
for _, cmdlinePrint := range prints.cmdlinePrints {
|
|
if score := cmdlinePrint.Match(cmdline); score > highestScore {
|
|
highestScore = score
|
|
}
|
|
}
|
|
if highestScore > 0 {
|
|
return cmdlineMatchBaseScore + highestScore
|
|
}
|
|
}
|
|
|
|
// Check env.
|
|
for _, envPrint := range prints.envPrints {
|
|
for key, value := range md.Env() {
|
|
// Check if env key matches.
|
|
if !envPrint.MatchesKey(key) {
|
|
continue
|
|
}
|
|
|
|
// Try matching the env value.
|
|
score := envPrint.Match(value)
|
|
if score > highestScore {
|
|
highestScore = score
|
|
}
|
|
}
|
|
}
|
|
// If something matched, add base score and return.
|
|
if highestScore > 0 {
|
|
return envMatchBaseScore + highestScore
|
|
}
|
|
|
|
// Check matching path.
|
|
matchingPath := md.MatchingPath()
|
|
if matchingPath != "" {
|
|
for _, pathPrint := range prints.pathPrints {
|
|
// Try matching the path value.
|
|
score := pathPrint.Match(matchingPath)
|
|
if score > highestScore {
|
|
highestScore = score
|
|
}
|
|
}
|
|
// If something matched, add base score and return.
|
|
if highestScore > 0 {
|
|
return matchingPathMatchBaseScore + highestScore
|
|
}
|
|
}
|
|
|
|
// Check path.
|
|
path := md.Path()
|
|
if path != "" {
|
|
for _, pathPrint := range prints.pathPrints {
|
|
// Try matching the path value.
|
|
score := pathPrint.Match(path)
|
|
if score > highestScore {
|
|
highestScore = score
|
|
}
|
|
}
|
|
// If something matched, add base score and return.
|
|
if highestScore > 0 {
|
|
return pathMatchBaseScore + highestScore
|
|
}
|
|
}
|
|
|
|
// Nothing matched.
|
|
return 0
|
|
}
|
|
|
|
func checkMatchStrength(value int) int {
|
|
if value > maxMatchStrength {
|
|
return maxMatchStrength
|
|
}
|
|
if value < -maxMatchStrength {
|
|
return -maxMatchStrength
|
|
}
|
|
return value
|
|
}
|
|
|
|
const (
|
|
deriveFPKeyIDForItemStart = iota + 1
|
|
deriveFPKeyIDForType
|
|
deriveFPKeyIDForKey
|
|
deriveFPKeyIDForOperation
|
|
deriveFPKeyIDForValue
|
|
)
|
|
|
|
// DeriveProfileID derives a profile ID from the given fingerprints.
|
|
func DeriveProfileID(fps []Fingerprint) string {
|
|
// Sort the fingerprints.
|
|
sortAndCompactFingerprints(fps)
|
|
|
|
// Compile data for hashing.
|
|
c := container.New(nil)
|
|
c.AppendInt(len(fps))
|
|
for _, fp := range fps {
|
|
c.AppendNumber(deriveFPKeyIDForItemStart)
|
|
if fp.Type != "" {
|
|
c.AppendNumber(deriveFPKeyIDForType)
|
|
c.AppendAsBlock([]byte(fp.Type))
|
|
}
|
|
if fp.Key != "" {
|
|
c.AppendNumber(deriveFPKeyIDForKey)
|
|
c.AppendAsBlock([]byte(fp.Key))
|
|
}
|
|
if fp.Operation != "" {
|
|
c.AppendNumber(deriveFPKeyIDForOperation)
|
|
c.AppendAsBlock([]byte(fp.Operation))
|
|
}
|
|
if fp.Value != "" {
|
|
c.AppendNumber(deriveFPKeyIDForValue)
|
|
c.AppendAsBlock([]byte(fp.Value))
|
|
}
|
|
}
|
|
|
|
// Hash and return.
|
|
h := lhash.Digest(lhash.SHA3_256, c.CompileData())
|
|
return h.Base58()
|
|
}
|
|
|
|
func sortAndCompactFingerprints(fps []Fingerprint) []Fingerprint {
|
|
// Sort.
|
|
slices.SortFunc[[]Fingerprint, Fingerprint](fps, func(a, b Fingerprint) int {
|
|
switch {
|
|
case a.Type != b.Type:
|
|
return strings.Compare(a.Type, b.Type)
|
|
case a.Key != b.Key:
|
|
return strings.Compare(a.Key, b.Key)
|
|
case a.Operation != b.Operation:
|
|
return strings.Compare(a.Operation, b.Operation)
|
|
case a.Value != b.Value:
|
|
return strings.Compare(a.Value, b.Value)
|
|
case a.MergedFrom != b.MergedFrom:
|
|
return strings.Compare(a.MergedFrom, b.MergedFrom)
|
|
default:
|
|
return 0
|
|
}
|
|
})
|
|
|
|
// De-duplicate.
|
|
// Important: Even if the fingerprint is the same, but MergedFrom is
|
|
// different, we need to keep the separate fingerprint, so that new installs
|
|
// will cleanly update to the synced state: Auto-generated profiles need to
|
|
// be automatically replaced by the merged version.
|
|
fps = slices.CompactFunc[[]Fingerprint, Fingerprint](fps, func(a, b Fingerprint) bool {
|
|
return a.Type == b.Type &&
|
|
a.Key == b.Key &&
|
|
a.Operation == b.Operation &&
|
|
a.Value == b.Value &&
|
|
a.MergedFrom == b.MergedFrom
|
|
})
|
|
|
|
return fps
|
|
}
|