260 lines
5.9 KiB
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",
|
|
)
|
|
}
|