375 lines
8.4 KiB
Go
375 lines
8.4 KiB
Go
// ©AngelaMos | 2026
|
|
// integration_test.go
|
|
|
|
package engine_test
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/CarterPerez-dev/portia/internal/engine"
|
|
"github.com/CarterPerez-dev/portia/internal/rules"
|
|
"github.com/CarterPerez-dev/portia/internal/source"
|
|
"github.com/CarterPerez-dev/portia/pkg/types"
|
|
)
|
|
|
|
func testdataDir(t *testing.T) string {
|
|
t.Helper()
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
t.Fatal("unable to determine test file location")
|
|
}
|
|
return filepath.Join(
|
|
filepath.Dir(filename), "..", "..", "testdata", "fixtures",
|
|
)
|
|
}
|
|
|
|
func setupPipeline(t *testing.T) (*engine.Pipeline, *rules.Registry) {
|
|
t.Helper()
|
|
reg := rules.NewRegistry()
|
|
rules.RegisterBuiltins(reg)
|
|
return engine.NewPipeline(reg), reg
|
|
}
|
|
|
|
func findByRuleID(
|
|
findings []types.Finding, ruleID string,
|
|
) *types.Finding {
|
|
for i := range findings {
|
|
if findings[i].RuleID == ruleID {
|
|
return &findings[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestIntegrationFullScan(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
t.Skip("testdata/fixtures not found")
|
|
}
|
|
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
if len(result.Findings) == 0 {
|
|
t.Fatal("expected findings, got none")
|
|
}
|
|
|
|
ruleIDs := make(map[string]bool)
|
|
for _, f := range result.Findings {
|
|
ruleIDs[f.RuleID] = true
|
|
}
|
|
|
|
expectedRules := []string{
|
|
"aws-access-key-id",
|
|
"stripe-live-secret",
|
|
"ssh-private-key-rsa",
|
|
"generic-password",
|
|
}
|
|
|
|
for _, id := range expectedRules {
|
|
if !ruleIDs[id] {
|
|
t.Errorf("expected finding for rule %q, not found", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAWSKeyDetection(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
f := findByRuleID(result.Findings, "aws-access-key-id")
|
|
if f == nil {
|
|
t.Fatal("aws-access-key-id finding not found")
|
|
}
|
|
|
|
if f.Severity != types.SeverityCritical {
|
|
t.Errorf("expected CRITICAL, got %s", f.Severity)
|
|
}
|
|
|
|
if f.FilePath != "config.py" {
|
|
t.Errorf("expected config.py, got %s", f.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationStripeKeyDetection(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
f := findByRuleID(result.Findings, "stripe-live-secret")
|
|
if f == nil {
|
|
t.Fatal("stripe-live-secret finding not found")
|
|
}
|
|
|
|
if f.Severity != types.SeverityCritical {
|
|
t.Errorf("expected CRITICAL, got %s", f.Severity)
|
|
}
|
|
|
|
if f.FilePath != "payment.env" {
|
|
t.Errorf("expected payment.env, got %s", f.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationPrivateKeyDetection(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
f := findByRuleID(result.Findings, "ssh-private-key-rsa")
|
|
if f == nil {
|
|
t.Fatal("ssh-private-key-rsa finding not found")
|
|
}
|
|
|
|
if f.Severity != types.SeverityCritical {
|
|
t.Errorf("expected CRITICAL, got %s", f.Severity)
|
|
}
|
|
|
|
if f.FilePath != "key.pem" {
|
|
t.Errorf("expected key.pem, got %s", f.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationPasswordDetection(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
f := findByRuleID(result.Findings, "generic-password")
|
|
if f == nil {
|
|
t.Fatal("generic-password finding not found")
|
|
}
|
|
|
|
if f.Severity != types.SeverityHigh {
|
|
t.Errorf("expected HIGH, got %s", f.Severity)
|
|
}
|
|
|
|
if f.Entropy <= 0 {
|
|
t.Error("expected positive entropy for password")
|
|
}
|
|
}
|
|
|
|
func TestIntegrationGitHubTokenDetection(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
f := findByRuleID(result.Findings, "github-pat-classic")
|
|
if f == nil {
|
|
f = findByRuleID(result.Findings, "github-pat-fine")
|
|
if f == nil {
|
|
t.Fatal(
|
|
"github-pat-classic or github-pat-fine finding not found",
|
|
)
|
|
}
|
|
}
|
|
|
|
if f.Severity != types.SeverityCritical {
|
|
t.Errorf("expected CRITICAL, got %s", f.Severity)
|
|
}
|
|
|
|
if f.FilePath != "github_token.js" {
|
|
t.Errorf("expected github_token.js, got %s", f.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationConnectionStringDetection(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
f := findByRuleID(result.Findings, "postgres-connection")
|
|
if f == nil {
|
|
t.Fatal("postgres-connection finding not found")
|
|
}
|
|
|
|
if f.FilePath != "connection.yml" {
|
|
t.Errorf("expected connection.yml, got %s", f.FilePath)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationNoFalsePositivesOnSafeFiles(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
for _, f := range result.Findings {
|
|
if f.FilePath == "safe.txt" {
|
|
t.Errorf(
|
|
"unexpected finding in safe.txt: %s (%s)",
|
|
f.RuleID, f.Secret,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationTemplatesFiltered(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
for _, f := range result.Findings {
|
|
if f.FilePath == "template.env" {
|
|
t.Errorf(
|
|
"unexpected finding in template.env: %s (%s)",
|
|
f.RuleID, f.Secret,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationExcludePaths(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, []string{"*.py"})
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
for _, f := range result.Findings {
|
|
if f.FilePath == "config.py" {
|
|
t.Error("config.py should be excluded")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationDisableRules(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
reg := rules.NewRegistry()
|
|
rules.RegisterBuiltins(reg)
|
|
reg.Disable("aws-access-key-id", "stripe-live-secret")
|
|
|
|
p := engine.NewPipeline(reg)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
for _, f := range result.Findings {
|
|
if f.RuleID == "aws-access-key-id" {
|
|
t.Error("aws-access-key-id should be disabled")
|
|
}
|
|
if f.RuleID == "stripe-live-secret" {
|
|
t.Error("stripe-live-secret should be disabled")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationFindingsSortBySeverity(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
sorted := make([]types.Finding, len(result.Findings))
|
|
copy(sorted, result.Findings)
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return sorted[i].Severity.Rank() < sorted[j].Severity.Rank()
|
|
})
|
|
|
|
if len(sorted) > 0 &&
|
|
sorted[0].Severity != types.SeverityCritical {
|
|
t.Errorf(
|
|
"expected first sorted finding to be CRITICAL, got %s",
|
|
sorted[0].Severity,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationResultMetadata(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, reg := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
result, err := p.Run(context.Background(), src)
|
|
if err != nil {
|
|
t.Fatalf("pipeline run failed: %v", err)
|
|
}
|
|
|
|
if result.TotalRules != reg.Len() {
|
|
t.Errorf(
|
|
"expected %d rules, got %d",
|
|
reg.Len(), result.TotalRules,
|
|
)
|
|
}
|
|
|
|
if result.TotalFindings < len(result.Findings) {
|
|
t.Errorf(
|
|
"total findings %d < unique findings %d",
|
|
result.TotalFindings, len(result.Findings),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationContextCancellation(t *testing.T) {
|
|
dir := testdataDir(t)
|
|
p, _ := setupPipeline(t)
|
|
src := source.NewDirectory(dir, 0, nil)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := p.Run(ctx, src)
|
|
if err == nil {
|
|
t.Log("expected error or empty result from cancelled context")
|
|
}
|
|
}
|