Cybersecurity-Projects/PROJECTS/intermediate/secrets-scanner/internal/rules/entropy_test.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),
)
}
})
}
}