Cybersecurity-Projects/PROJECTS/intermediate/secrets-scanner/internal/engine/detector_test.go

260 lines
5.9 KiB
Go

// ©AngelaMos | 2026
// detector_test.go
package engine
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/CarterPerez-dev/portia/internal/rules"
"github.com/CarterPerez-dev/portia/pkg/types"
)
func testRegistry() *rules.Registry {
reg := rules.NewRegistry()
reg.Register(&types.Rule{
ID: "test-aws-key",
Description: "AWS Access Key ID",
Severity: types.SeverityCritical,
Keywords: []string{"AKIA"},
Pattern: regexp.MustCompile(
`\b((?:AKIA)[0-9A-Z]{16})\b`,
),
SecretGroup: 1,
SecretType: types.SecretTypeAPIKey,
})
reg.Register(&types.Rule{
ID: "test-generic-password",
Description: "Generic Password",
Severity: types.SeverityHigh,
Keywords: []string{"password"},
Pattern: regexp.MustCompile(
`(?i)password\s*[:=]\s*['"]([^'"]{8,})['"]`,
),
SecretGroup: 1,
Entropy: func() *float64 { f := 3.0; return &f }(),
SecretType: types.SecretTypePassword,
})
reg.Register(&types.Rule{
ID: "test-stripe-key",
Description: "Stripe Live Key",
Severity: types.SeverityCritical,
Keywords: []string{"sk_live_"},
Pattern: regexp.MustCompile(
`\b(sk_live_[a-zA-Z0-9]{24,})\b`,
),
SecretGroup: 1,
SecretType: types.SecretTypeAPIKey,
})
return reg
}
func TestDetectAWSKey(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{ //nolint:gosec
Content: `aws_key = "AKIAIOSFODNN7EXAMPLE"`,
FilePath: "config.py",
LineStart: 1,
}
findings := d.Detect(chunk)
require.Len(t, findings, 1)
assert.Equal(t, "test-aws-key", findings[0].RuleID)
assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", findings[0].Secret) //nolint:gosec
assert.Equal(t, "config.py", findings[0].FilePath)
assert.Equal(t, 1, findings[0].LineNumber)
assert.Equal(t, types.SeverityCritical, findings[0].Severity)
}
func TestDetectStripeKey(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{ //nolint:gosec
Content: `STRIPE_KEY=sk_live_4eC39HqLyjWDarjtT1zdp7dc`,
FilePath: "env.sh",
LineStart: 10,
}
findings := d.Detect(chunk)
require.Len(t, findings, 1)
assert.Equal(t, "test-stripe-key", findings[0].RuleID)
assert.Equal(t,
"sk_live_4eC39HqLyjWDarjtT1zdp7dc",
findings[0].Secret,
)
}
func TestDetectPasswordWithEntropy(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: `password = "xK9mP2vL5nQ8jR3t"`,
FilePath: "config.ini",
LineStart: 5,
}
findings := d.Detect(chunk)
require.Len(t, findings, 1)
assert.Equal(t, "test-generic-password", findings[0].RuleID)
assert.Greater(t, findings[0].Entropy, 3.0)
}
func TestDetectPasswordLowEntropyFiltered(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: `password = "aaaaaaaaaa"`,
FilePath: "config.ini",
LineStart: 1,
}
findings := d.Detect(chunk)
assert.Empty(t, findings)
}
func TestDetectNoKeywords(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: "just some normal code here",
FilePath: "main.go",
LineStart: 1,
}
findings := d.Detect(chunk)
assert.Empty(t, findings)
}
func TestDetectPlaceholderFiltered(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: `password = "your-password-here"`,
FilePath: "config.py",
LineStart: 1,
}
findings := d.Detect(chunk)
assert.Empty(t, findings)
}
func TestDetectMultipleFindings(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: `aws_key = "AKIAIOSFODNN7EXAMPLE"` + "\n" +
`stripe = sk_live_4eC39HqLyjWDarjtT1zdp7dc`,
FilePath: "config.py",
LineStart: 1,
}
findings := d.Detect(chunk)
require.Len(t, findings, 2)
ruleIDs := map[string]bool{}
for _, f := range findings {
ruleIDs[f.RuleID] = true
}
assert.True(t, ruleIDs["test-aws-key"])
assert.True(t, ruleIDs["test-stripe-key"])
}
func TestDetectLineNumbers(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: "line 1\n" +
"line 2\n" +
`AKIAIOSFODNN7EXAMPLE` + "\n" +
"line 4\n",
FilePath: "data.txt",
LineStart: 10,
}
findings := d.Detect(chunk)
require.Len(t, findings, 1)
assert.Equal(t, 12, findings[0].LineNumber)
}
func TestDetectAllowedPathFiltered(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{ //nolint:gosec
Content: `AKIAIOSFODNN7EXAMPLE`,
FilePath: "go.sum",
LineStart: 1,
}
findings := d.Detect(chunk)
assert.Empty(t, findings)
}
func TestExtractSecret(t *testing.T) {
t.Parallel()
line := `key = "AKIAIOSFODNN7EXAMPLE"` //nolint:gosec
loc := []int{7, 27, 7, 27}
got := extractSecret(line, loc, 1)
assert.Equal(t, "AKIAIOSFODNN7EXAMPLE", got)
got = extractSecret(line, loc, 0)
assert.Equal(t, `AKIAIOSFODNN7EXAMPLE`, got)
got = extractSecret(line, []int{}, 1)
assert.Empty(t, got)
}
func TestDetectHighEntropy(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{
Content: `secret = "aB1cD2eF3gH4iJ5kL6mN7oP8qR9sT0u"`,
FilePath: "config.py",
LineStart: 1,
}
findings := d.Detect(chunk)
require.Len(t, findings, 1)
assert.Equal(t, "high-entropy-string", findings[0].RuleID)
assert.Equal(t, types.SeverityMedium, findings[0].Severity)
assert.Greater(t, findings[0].Entropy, 4.0)
}
func TestDetectHighEntropyNotDuplicated(t *testing.T) {
t.Parallel()
d := NewDetector(testRegistry())
chunk := types.Chunk{ //nolint:gosec
Content: `aws_key = "AKIAIOSFODNN7EXAMPLE"`,
FilePath: "config.py",
LineStart: 1,
}
findings := d.Detect(chunk)
ruleIDs := map[string]bool{}
for _, f := range findings {
ruleIDs[f.RuleID] = true
}
assert.True(t, ruleIDs["test-aws-key"])
assert.False(
t,
ruleIDs["high-entropy-string"],
"entropy detector should not duplicate rule findings",
)
}