Derive profile ID from fingerprints

This commit is contained in:
Daniel 2023-09-12 15:39:12 +02:00
parent bd410afee6
commit e81953d8f3
3 changed files with 131 additions and 2 deletions

View file

@ -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
}
})
}

View file

@ -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)
}
}

View file

@ -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.