227 lines
4.4 KiB
Go
227 lines
4.4 KiB
Go
// ©AngelaMos | 2026
|
|
// entropy_test.go
|
|
|
|
package rules
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestShannonEntropy(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := map[string]struct {
|
|
data string
|
|
charset string
|
|
wantMin float64
|
|
wantMax float64
|
|
}{
|
|
"empty string": {
|
|
data: "",
|
|
charset: "",
|
|
wantMin: 0.0,
|
|
wantMax: 0.0,
|
|
},
|
|
"single repeated character": {
|
|
data: "aaaaaaa",
|
|
charset: "",
|
|
wantMin: 0.0,
|
|
wantMax: 0.0,
|
|
},
|
|
"two equally distributed chars": {
|
|
data: "ab",
|
|
charset: "",
|
|
wantMin: 1.0,
|
|
wantMax: 1.0,
|
|
},
|
|
"four equally distributed chars": {
|
|
data: "abcd",
|
|
charset: "",
|
|
wantMin: 2.0,
|
|
wantMax: 2.0,
|
|
},
|
|
"real password low entropy": {
|
|
data: "password123",
|
|
charset: "",
|
|
wantMin: 2.5,
|
|
wantMax: 3.5,
|
|
},
|
|
"aws key high entropy": { //nolint:gosec
|
|
data: "AKIAIOSFODNN7EXAMPLE",
|
|
charset: Base64Charset,
|
|
wantMin: 3.5,
|
|
wantMax: 5.0,
|
|
},
|
|
"hex string": {
|
|
data: "a1b2c3d4e5f6a7b8c9d0",
|
|
charset: HexCharset,
|
|
wantMin: 3.0,
|
|
wantMax: 4.0,
|
|
},
|
|
"high entropy random base64": {
|
|
data: "kR9mPx2vBnQ8jL5wYz3hTf",
|
|
charset: Base64Charset,
|
|
wantMin: 4.0,
|
|
wantMax: 5.5,
|
|
},
|
|
"charset filtering removes non-charset": {
|
|
data: "abc!!!def!!!ghi",
|
|
charset: AlphanumericCharset,
|
|
wantMin: 2.5,
|
|
wantMax: 3.5,
|
|
},
|
|
"empty after charset filter": {
|
|
data: "!!!???",
|
|
charset: AlphanumericCharset,
|
|
wantMin: 0.0,
|
|
wantMax: 0.0,
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := ShannonEntropy(tc.data, tc.charset)
|
|
assert.GreaterOrEqual(t, got, tc.wantMin,
|
|
"entropy too low")
|
|
assert.LessOrEqual(t, got, tc.wantMax,
|
|
"entropy too high")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShannonEntropyExactValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
got := ShannonEntropy("ab", "")
|
|
assert.InDelta(t, 1.0, got, 0.001)
|
|
|
|
got = ShannonEntropy("aabb", "")
|
|
assert.InDelta(t, 1.0, got, 0.001)
|
|
|
|
got = ShannonEntropy("abcdefgh", "")
|
|
assert.InDelta(t, 3.0, got, 0.001)
|
|
}
|
|
|
|
func TestDetectCharset(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := map[string]struct {
|
|
input string
|
|
want string
|
|
}{
|
|
"hex string": {
|
|
input: "a1b2c3d4e5f6",
|
|
want: "hex",
|
|
},
|
|
"base64 with special chars": {
|
|
input: "kR9mPx2vBnQ8jL5w+/YzTf==",
|
|
want: "base64",
|
|
},
|
|
"pure alphanumeric": {
|
|
input: "HelloWorld123XYZ",
|
|
want: "base64",
|
|
},
|
|
"mixed with symbols": {
|
|
input: "hello@world#foo$bar",
|
|
want: "alphanumeric",
|
|
},
|
|
"empty string": {
|
|
input: "",
|
|
want: "alphanumeric",
|
|
},
|
|
"only hex digits": {
|
|
input: "deadbeef0123456789",
|
|
want: "hex",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := DetectCharset(tc.input)
|
|
assert.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractHighEntropyTokens(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := map[string]struct {
|
|
line string
|
|
charset string
|
|
threshold float64
|
|
minLen int
|
|
wantCount int
|
|
wantFirst string
|
|
}{
|
|
"extracts high entropy token": {
|
|
line: `api_key = "kR9mPx2vBnQ8jL5wYz3hTf6aU7cD"`,
|
|
charset: Base64Charset,
|
|
threshold: 3.5,
|
|
minLen: 20,
|
|
wantCount: 1,
|
|
wantFirst: "kR9mPx2vBnQ8jL5wYz3hTf6aU7cD",
|
|
},
|
|
"skips low entropy": {
|
|
line: `password = "aaaaaaaaaaaaaaaaaaaaaa"`,
|
|
charset: Base64Charset,
|
|
threshold: 3.5,
|
|
minLen: 20,
|
|
wantCount: 0,
|
|
},
|
|
"skips short tokens": {
|
|
line: `key = "abc"`,
|
|
charset: Base64Charset,
|
|
threshold: 1.0,
|
|
minLen: 20,
|
|
wantCount: 0,
|
|
},
|
|
"multiple tokens in one line": {
|
|
line: `FIRST="kR9mPx2vBnQ8jL5wYz3hTf" ` +
|
|
`SECOND="aX7bC4dE9fG2hI5jK8mN"`,
|
|
charset: Base64Charset,
|
|
threshold: 3.0,
|
|
minLen: 20,
|
|
wantCount: 2,
|
|
},
|
|
"no tokens in plain text": {
|
|
line: "this is just a normal line of code",
|
|
charset: Base64Charset,
|
|
threshold: 4.0,
|
|
minLen: 20,
|
|
wantCount: 0,
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tokens := ExtractHighEntropyTokens(
|
|
tc.line,
|
|
tc.charset,
|
|
tc.threshold,
|
|
tc.minLen,
|
|
)
|
|
require.Len(t, tokens, tc.wantCount)
|
|
if tc.wantCount > 0 && tc.wantFirst != "" {
|
|
assert.Equal(t, tc.wantFirst, tokens[0].Value)
|
|
assert.Greater(
|
|
t,
|
|
tokens[0].Entropy,
|
|
tc.threshold,
|
|
)
|
|
assert.False(
|
|
t,
|
|
math.IsNaN(tokens[0].Entropy),
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|