From e81953d8f30208a73d1bd6aed53ade194a9f1503 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 12 Sep 2023 15:39:12 +0200 Subject: [PATCH] Derive profile ID from fingerprints --- profile/fingerprint.go | 69 +++++++++++++++++++++++++++++++++++++ profile/fingerprint_test.go | 53 ++++++++++++++++++++++++++++ profile/profile.go | 11 ++++-- 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 profile/fingerprint_test.go diff --git a/profile/fingerprint.go b/profile/fingerprint.go index 6f5e6389..d84f5fa2 100644 --- a/profile/fingerprint.go +++ b/profile/fingerprint.go @@ -4,6 +4,10 @@ import ( "fmt" "regexp" "strings" + + "github.com/safing/jess/lhash" + "github.com/safing/portbase/container" + "golang.org/x/exp/slices" ) // # Matching and Scores @@ -57,6 +61,12 @@ type ( 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 } // Tag represents a simple key/value kind of tag used in process metadata @@ -347,3 +357,62 @@ func checkMatchStrength(value int) int { } return value } + +const ( + deriveFPKeyIDForItemStart = iota + 1 + deriveFPKeyIDForType + deriveFPKeyIDForKey + deriveFPKeyIDForOperation + deriveFPKeyIDForValue +) + +func deriveProfileID(fps []Fingerprint) string { + // Sort the fingerprints. + sortFingerprints(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 sortFingerprints(fps []Fingerprint) { + 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 + } + }) +} diff --git a/profile/fingerprint_test.go b/profile/fingerprint_test.go new file mode 100644 index 00000000..4857d8be --- /dev/null +++ b/profile/fingerprint_test.go @@ -0,0 +1,53 @@ +package profile + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestDeriveProfileID(t *testing.T) { + t.Parallel() + + fps := []Fingerprint{ + { + Type: FingerprintTypePathID, + Operation: FingerprintOperationEqualsID, + Value: "/sbin/init", + }, + { + Type: FingerprintTypePathID, + Operation: FingerprintOperationPrefixID, + Value: "/", + }, + { + Type: FingerprintTypeEnvID, + Key: "PORTMASTER_PROFILE", + Operation: FingerprintOperationEqualsID, + Value: "TEST-1", + }, + { + Type: FingerprintTypeTagID, + Key: "tag-key-1", + Operation: FingerprintOperationEqualsID, + Value: "tag-key-2", + }, + } + + // Create rand source for shuffling. + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:gosec + + // Test 100 times. + for i := 0; i < 100; i++ { + // Shuffle fingerprints. + rnd.Shuffle(len(fps), func(i, j int) { + fps[i], fps[j] = fps[j], fps[i] + }) + + // Check if fingerprint matches. + id := deriveProfileID(fps) + assert.Equal(t, "PTSRP7rdCnmvdjRoPMTrtjj7qk7PxR1a9YdBWUGwnZXJh2", id) + } +} diff --git a/profile/profile.go b/profile/profile.go index 42a39738..0ef7e9f5 100644 --- a/profile/profile.go +++ b/profile/profile.go @@ -265,9 +265,16 @@ func New(profile *Profile) *Profile { profile.Config = make(map[string]interface{}) } - // Generate random ID if none is given. + // Generate ID if none is given. if profile.ID == "" { - profile.ID = utils.RandomUUID("").String() + 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.