diff --git a/common/authprovider/authx/file_test.go b/common/authprovider/authx/file_test.go new file mode 100644 index 0000000..b42fc59 --- /dev/null +++ b/common/authprovider/authx/file_test.go @@ -0,0 +1,408 @@ +package authx + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCookieParse(t *testing.T) { + tests := []struct { + name string + raw string + wantKey string + wantValue string + wantErr bool + }{ + { + name: "simple cookie", + raw: "session=abc123", + wantKey: "session", + wantValue: "abc123", + wantErr: false, + }, + { + name: "cookie with Set-Cookie prefix", + raw: "Set-Cookie: session=abc123; Path=/", + wantKey: "session", + wantValue: "abc123", + wantErr: false, + }, + { + name: "cookie with equals in value", + raw: "token=eyJhbGciOiJIUzI1NiJ9==; Path=/", + wantKey: "token", + wantValue: "eyJhbGciOiJIUzI1NiJ9==", + wantErr: false, + }, + { + name: "cookie with spaces", + raw: " session = abc123 ; Path=/", + wantKey: "session", + wantValue: "abc123", + wantErr: false, + }, + { + name: "empty raw", + raw: "", + wantErr: true, + }, + { + name: "missing equals", + raw: "sessionabc123; Path=/", + wantErr: true, + }, + { + name: "empty key", + raw: "=abc123; Path=/", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Cookie{Raw: tt.raw} + err := c.Parse() + + if (err != nil) != tt.wantErr { + t.Errorf("Cookie.Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr { + if c.Key != tt.wantKey { + t.Errorf("Cookie.Parse() Key = %v, want %v", c.Key, tt.wantKey) + } + if c.Value != tt.wantValue { + t.Errorf("Cookie.Parse() Value = %v, want %v", c.Value, tt.wantValue) + } + } + }) + } +} + +func TestSecretValidate(t *testing.T) { + tests := []struct { + name string + secret Secret + wantErr bool + }{ + { + name: "valid basic auth", + secret: Secret{ + Type: "BasicAuth", + Domains: []string{"example.com"}, + Username: "user", + Password: "pass", + }, + wantErr: false, + }, + { + name: "valid bearer token", + secret: Secret{ + Type: "BearerToken", + Domains: []string{"example.com"}, + Token: "abc123", + }, + wantErr: false, + }, + { + name: "valid header auth", + secret: Secret{ + Type: "Header", + Domains: []string{"example.com"}, + Headers: []KV{{Key: "X-API-Key", Value: "secret"}}, + }, + wantErr: false, + }, + { + name: "valid cookie auth", + secret: Secret{ + Type: "Cookie", + Domains: []string{"example.com"}, + Cookies: []Cookie{{Key: "session", Value: "abc123"}}, + }, + wantErr: false, + }, + { + name: "valid query auth", + secret: Secret{ + Type: "Query", + Domains: []string{"example.com"}, + Params: []KV{{Key: "api_key", Value: "secret"}}, + }, + wantErr: false, + }, + { + name: "invalid type", + secret: Secret{ + Type: "InvalidType", + Domains: []string{"example.com"}, + }, + wantErr: true, + }, + { + name: "missing domains", + secret: Secret{ + Type: "BasicAuth", + Username: "user", + Password: "pass", + }, + wantErr: true, + }, + { + name: "basic auth missing username", + secret: Secret{ + Type: "BasicAuth", + Domains: []string{"example.com"}, + Password: "pass", + }, + wantErr: true, + }, + { + name: "basic auth missing password", + secret: Secret{ + Type: "BasicAuth", + Domains: []string{"example.com"}, + Username: "user", + }, + wantErr: true, + }, + { + name: "bearer auth missing token", + secret: Secret{ + Type: "BearerToken", + Domains: []string{"example.com"}, + }, + wantErr: true, + }, + { + name: "header auth missing headers", + secret: Secret{ + Type: "Header", + Domains: []string{"example.com"}, + }, + wantErr: true, + }, + { + name: "cookie auth missing cookies", + secret: Secret{ + Type: "Cookie", + Domains: []string{"example.com"}, + }, + wantErr: true, + }, + { + name: "query auth missing params", + secret: Secret{ + Type: "Query", + Domains: []string{"example.com"}, + }, + wantErr: true, + }, + { + name: "valid domain regex", + secret: Secret{ + Type: "BearerToken", + DomainsRegex: []string{".*\\.example\\.com"}, + Token: "abc123", + }, + wantErr: false, + }, + { + name: "invalid domain regex", + secret: Secret{ + Type: "BearerToken", + DomainsRegex: []string{"[invalid"}, + Token: "abc123", + }, + wantErr: true, + }, + { + name: "case insensitive type", + secret: Secret{ + Type: "basicauth", + Domains: []string{"example.com"}, + Username: "user", + Password: "pass", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.secret.Validate() + if (err != nil) != tt.wantErr { + t.Errorf("Secret.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGetAuthDataFromFile(t *testing.T) { + // Create temp directory for test files + tmpDir := t.TempDir() + + yamlContent := `id: test +info: + name: test +static: + - type: BasicAuth + domains: + - example.com + username: user + password: pass +` + + jsonContent := `{ + "id": "test", + "info": {"name": "test"}, + "static": [ + { + "type": "BasicAuth", + "domains": ["example.com"], + "username": "user", + "password": "pass" + } + ] +}` + + tests := []struct { + name string + filename string + content string + wantErr bool + }{ + { + name: "yaml file lowercase", + filename: "secrets.yaml", + content: yamlContent, + wantErr: false, + }, + { + name: "yml file lowercase", + filename: "secrets.yml", + content: yamlContent, + wantErr: false, + }, + { + name: "json file lowercase", + filename: "secrets.json", + content: jsonContent, + wantErr: false, + }, + { + name: "yaml file uppercase", + filename: "secrets.YAML", + content: yamlContent, + wantErr: false, + }, + { + name: "json file uppercase", + filename: "secrets.JSON", + content: jsonContent, + wantErr: false, + }, + { + name: "invalid extension", + filename: "secrets.txt", + content: yamlContent, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test file + filePath := filepath.Join(tmpDir, tt.filename) + err := os.WriteFile(filePath, []byte(tt.content), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + _, err = GetAuthDataFromFile(filePath) + if (err != nil) != tt.wantErr { + t.Errorf("GetAuthDataFromFile() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSecretGetStrategy(t *testing.T) { + tests := []struct { + name string + secret Secret + wantType string + wantNil bool + }{ + { + name: "basic auth strategy", + secret: Secret{ + Type: "BasicAuth", + Username: "user", + Password: "pass", + }, + wantType: "*authx.BasicAuthStrategy", + wantNil: false, + }, + { + name: "bearer token strategy", + secret: Secret{ + Type: "BearerToken", + Token: "abc123", + }, + wantType: "*authx.BearerTokenAuthStrategy", + wantNil: false, + }, + { + name: "header strategy", + secret: Secret{ + Type: "Header", + Headers: []KV{{Key: "X-API-Key", Value: "secret"}}, + }, + wantType: "*authx.HeadersAuthStrategy", + wantNil: false, + }, + { + name: "cookie strategy", + secret: Secret{ + Type: "Cookie", + Cookies: []Cookie{{Key: "session", Value: "abc123"}}, + }, + wantType: "*authx.CookiesAuthStrategy", + wantNil: false, + }, + { + name: "query strategy", + secret: Secret{ + Type: "Query", + Params: []KV{{Key: "api_key", Value: "secret"}}, + }, + wantType: "*authx.QueryAuthStrategy", + wantNil: false, + }, + { + name: "unknown type returns nil", + secret: Secret{ + Type: "UnknownType", + }, + wantNil: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strategy := tt.secret.GetStrategy() + if tt.wantNil { + if strategy != nil { + t.Errorf("GetStrategy() = %T, want nil", strategy) + } + } else { + if strategy == nil { + t.Errorf("GetStrategy() = nil, want %s", tt.wantType) + } + } + }) + } +} diff --git a/common/authprovider/authx/strategy_test.go b/common/authprovider/authx/strategy_test.go new file mode 100644 index 0000000..3beeadd --- /dev/null +++ b/common/authprovider/authx/strategy_test.go @@ -0,0 +1,215 @@ +package authx + +import ( + "net/http" + "testing" + + "github.com/projectdiscovery/retryablehttp-go" +) + +func TestBasicAuthStrategy(t *testing.T) { + secret := &Secret{ + Username: "user", + Password: "pass", + } + strategy := NewBasicAuthStrategy(secret) + + t.Run("Apply", func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + strategy.Apply(req) + + user, pass, ok := req.BasicAuth() + if !ok { + t.Error("Basic auth not set") + } + if user != "user" { + t.Errorf("Username = %v, want user", user) + } + if pass != "pass" { + t.Errorf("Password = %v, want pass", pass) + } + }) + + t.Run("ApplyOnRR", func(t *testing.T) { + req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil) + strategy.ApplyOnRR(req) + + user, pass, ok := req.BasicAuth() + if !ok { + t.Error("Basic auth not set") + } + if user != "user" { + t.Errorf("Username = %v, want user", user) + } + if pass != "pass" { + t.Errorf("Password = %v, want pass", pass) + } + }) +} + +func TestBearerTokenAuthStrategy(t *testing.T) { + secret := &Secret{ + Token: "mytoken123", + } + strategy := NewBearerTokenAuthStrategy(secret) + + t.Run("Apply", func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + strategy.Apply(req) + + auth := req.Header.Get("Authorization") + expected := "Bearer mytoken123" + if auth != expected { + t.Errorf("Authorization = %v, want %v", auth, expected) + } + }) + + t.Run("ApplyOnRR", func(t *testing.T) { + req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil) + strategy.ApplyOnRR(req) + + auth := req.Header.Get("Authorization") + expected := "Bearer mytoken123" + if auth != expected { + t.Errorf("Authorization = %v, want %v", auth, expected) + } + }) +} + +func TestHeadersAuthStrategy(t *testing.T) { + // Headers strategy preserves exact casing, so use exact key names + secret := &Secret{ + Headers: []KV{ + {Key: "X-API-Key", Value: "secret123"}, + {Key: "X-Custom", Value: "value"}, + }, + } + strategy := NewHeadersAuthStrategy(secret) + + t.Run("Apply", func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + strategy.Apply(req) + + // Use direct map access since headers preserve exact casing + if got := req.Header["X-API-Key"]; len(got) == 0 || got[0] != "secret123" { + t.Errorf("X-API-Key = %v, want [secret123]", got) + } + if got := req.Header["X-Custom"]; len(got) == 0 || got[0] != "value" { + t.Errorf("X-Custom = %v, want [value]", got) + } + }) + + t.Run("ApplyOnRR", func(t *testing.T) { + req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil) + strategy.ApplyOnRR(req) + + // Use direct map access since headers preserve exact casing + if got := req.Header["X-API-Key"]; len(got) == 0 || got[0] != "secret123" { + t.Errorf("X-API-Key = %v, want [secret123]", got) + } + if got := req.Header["X-Custom"]; len(got) == 0 || got[0] != "value" { + t.Errorf("X-Custom = %v, want [value]", got) + } + }) +} + +func TestCookiesAuthStrategy(t *testing.T) { + secret := &Secret{ + Cookies: []Cookie{ + {Key: "session", Value: "abc123"}, + {Key: "auth", Value: "xyz789"}, + }, + } + strategy := NewCookiesAuthStrategy(secret) + + t.Run("Apply", func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com", nil) + strategy.Apply(req) + + cookies := req.Cookies() + if len(cookies) != 2 { + t.Errorf("Expected 2 cookies, got %d", len(cookies)) + } + + found := make(map[string]string) + for _, c := range cookies { + found[c.Name] = c.Value + } + if found["session"] != "abc123" { + t.Errorf("session cookie = %v, want abc123", found["session"]) + } + if found["auth"] != "xyz789" { + t.Errorf("auth cookie = %v, want xyz789", found["auth"]) + } + }) + + t.Run("ApplyOnRR replaces existing cookies", func(t *testing.T) { + req, _ := retryablehttp.NewRequest("GET", "http://example.com", nil) + // Add existing cookie that should be replaced + req.AddCookie(&http.Cookie{Name: "session", Value: "old_value"}) + // Add existing cookie that should be kept + req.AddCookie(&http.Cookie{Name: "other", Value: "keep_me"}) + + strategy.ApplyOnRR(req) + + cookies := req.Cookies() + found := make(map[string]string) + for _, c := range cookies { + found[c.Name] = c.Value + } + + // New cookie values should override old ones + if found["session"] != "abc123" { + t.Errorf("session cookie = %v, want abc123", found["session"]) + } + if found["auth"] != "xyz789" { + t.Errorf("auth cookie = %v, want xyz789", found["auth"]) + } + // Existing non-replaced cookie should be preserved + if found["other"] != "keep_me" { + t.Errorf("other cookie = %v, want keep_me", found["other"]) + } + }) +} + +func TestQueryAuthStrategy(t *testing.T) { + secret := &Secret{ + Params: []KV{ + {Key: "api_key", Value: "secret123"}, + {Key: "token", Value: "abc"}, + }, + } + strategy := NewQueryAuthStrategy(secret) + + t.Run("Apply", func(t *testing.T) { + req, _ := http.NewRequest("GET", "http://example.com/path?existing=value", nil) + strategy.Apply(req) + + query := req.URL.Query() + if got := query.Get("api_key"); got != "secret123" { + t.Errorf("api_key = %v, want secret123", got) + } + if got := query.Get("token"); got != "abc" { + t.Errorf("token = %v, want abc", got) + } + if got := query.Get("existing"); got != "value" { + t.Errorf("existing = %v, want value", got) + } + }) + + t.Run("ApplyOnRR", func(t *testing.T) { + req, _ := retryablehttp.NewRequest("GET", "http://example.com/path?existing=value", nil) + strategy.ApplyOnRR(req) + + query := req.Request.URL.Query() + if got := query.Get("api_key"); got != "secret123" { + t.Errorf("api_key = %v, want secret123", got) + } + if got := query.Get("token"); got != "abc" { + t.Errorf("token = %v, want abc", got) + } + if got := query.Get("existing"); got != "value" { + t.Errorf("existing = %v, want value", got) + } + }) +} diff --git a/common/authprovider/provider_test.go b/common/authprovider/provider_test.go new file mode 100644 index 0000000..a29cde5 --- /dev/null +++ b/common/authprovider/provider_test.go @@ -0,0 +1,256 @@ +package authprovider + +import ( + "net/url" + "os" + "path/filepath" + "testing" + + urlutil "github.com/projectdiscovery/utils/url" +) + +func createTestSecretsFile(t *testing.T, content string) string { + t.Helper() + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "secrets.yaml") + err := os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + t.Fatalf("Failed to create test secrets file: %v", err) + } + return filePath +} + +func TestFileAuthProviderLookupAddr(t *testing.T) { + content := `id: test +info: + name: test +static: + - type: BasicAuth + domains: + - example.com + - api.example.com:443 + username: user + password: pass + - type: BearerToken + domains-regex: + - ".*\\.test\\.com" + token: regextoken +` + filePath := createTestSecretsFile(t, content) + provider, err := NewFileAuthProvider(filePath) + if err != nil { + t.Fatalf("NewFileAuthProvider() error = %v", err) + } + + tests := []struct { + name string + addr string + wantCount int + }{ + { + name: "exact match", + addr: "example.com", + wantCount: 1, + }, + { + name: "exact match case insensitive", + addr: "EXAMPLE.COM", + wantCount: 1, + }, + { + name: "with port 443 normalized", + addr: "example.com:443", + wantCount: 1, + }, + { + name: "with port 80 normalized", + addr: "example.com:80", + wantCount: 1, + }, + { + name: "subdomain exact match", + addr: "api.example.com", + wantCount: 1, + }, + { + name: "regex match", + addr: "foo.test.com", + wantCount: 1, + }, + { + name: "regex match subdomain", + addr: "bar.baz.test.com", + wantCount: 1, + }, + { + name: "no match", + addr: "unknown.com", + wantCount: 0, + }, + { + name: "non-standard port not normalized", + addr: "example.com:8080", + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strategies := provider.LookupAddr(tt.addr) + if len(strategies) != tt.wantCount { + t.Errorf("LookupAddr(%q) returned %d strategies, want %d", tt.addr, len(strategies), tt.wantCount) + } + }) + } +} + +func TestFileAuthProviderLookupURL(t *testing.T) { + content := `id: test +info: + name: test +static: + - type: BasicAuth + domains: + - example.com + username: user + password: pass +` + filePath := createTestSecretsFile(t, content) + provider, err := NewFileAuthProvider(filePath) + if err != nil { + t.Fatalf("NewFileAuthProvider() error = %v", err) + } + + t.Run("LookupURL", func(t *testing.T) { + u, _ := url.Parse("https://example.com/path") + strategies := provider.LookupURL(u) + if len(strategies) != 1 { + t.Errorf("LookupURL() returned %d strategies, want 1", len(strategies)) + } + }) + + t.Run("LookupURLX", func(t *testing.T) { + u, _ := urlutil.Parse("https://example.com/path") + strategies := provider.LookupURLX(u) + if len(strategies) != 1 { + t.Errorf("LookupURLX() returned %d strategies, want 1", len(strategies)) + } + }) +} + +func TestMultiAuthProvider(t *testing.T) { + content1 := `id: test1 +info: + name: test1 +static: + - type: BasicAuth + domains: + - first.com + username: user1 + password: pass1 +` + content2 := `id: test2 +info: + name: test2 +static: + - type: BearerToken + domains: + - second.com + token: token2 +` + filePath1 := createTestSecretsFile(t, content1) + provider1, err := NewFileAuthProvider(filePath1) + if err != nil { + t.Fatalf("NewFileAuthProvider() error = %v", err) + } + + // Create second file in different temp dir + tmpDir2 := t.TempDir() + filePath2 := filepath.Join(tmpDir2, "secrets2.yaml") + err = os.WriteFile(filePath2, []byte(content2), 0644) + if err != nil { + t.Fatalf("Failed to create test secrets file: %v", err) + } + provider2, err := NewFileAuthProvider(filePath2) + if err != nil { + t.Fatalf("NewFileAuthProvider() error = %v", err) + } + + multi := NewMultiAuthProvider(provider1, provider2) + + tests := []struct { + name string + addr string + wantCount int + }{ + { + name: "match first provider", + addr: "first.com", + wantCount: 1, + }, + { + name: "match second provider", + addr: "second.com", + wantCount: 1, + }, + { + name: "no match", + addr: "third.com", + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + strategies := multi.LookupAddr(tt.addr) + if len(strategies) != tt.wantCount { + t.Errorf("LookupAddr(%q) returned %d strategies, want %d", tt.addr, len(strategies), tt.wantCount) + } + }) + } +} + +func TestNewFileAuthProviderErrors(t *testing.T) { + tests := []struct { + name string + content string + wantErr bool + }{ + { + name: "empty secrets", + content: `id: test`, + wantErr: true, + }, + { + name: "invalid secret type", + content: `id: test +static: + - type: InvalidType + domains: + - example.com +`, + wantErr: true, + }, + { + name: "missing required field", + content: `id: test +static: + - type: BasicAuth + domains: + - example.com + username: user +`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath := createTestSecretsFile(t, tt.content) + _, err := NewFileAuthProvider(filePath) + if (err != nil) != tt.wantErr { + t.Errorf("NewFileAuthProvider() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}