diff --git a/backend/go.sum b/backend/go.sum index 997d39e5..5c74288b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -205,14 +205,8 @@ go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/backend/internal/api/handlers/dns_provider_handler.go b/backend/internal/api/handlers/dns_provider_handler.go index 39f4daf4..f41aa309 100644 --- a/backend/internal/api/handlers/dns_provider_handler.go +++ b/backend/internal/api/handlers/dns_provider_handler.go @@ -2,9 +2,11 @@ package handlers import ( "net/http" + "sort" "strconv" "github.com/Wikid82/charon/backend/internal/services" + "github.com/Wikid82/charon/backend/pkg/dnsprovider" "github.com/gin-gonic/gin" ) @@ -214,211 +216,81 @@ func (h *DNSProviderHandler) TestCredentials(c *gin.Context) { // GetTypes handles GET /api/v1/dns-providers/types // Returns the list of supported DNS provider types with their required fields. +// Types are sourced from the provider registry (built-in, custom, and external plugins). func (h *DNSProviderHandler) GetTypes(c *gin.Context) { - types := []gin.H{ - { - "type": "cloudflare", - "name": "Cloudflare", - "fields": []gin.H{ - { - "name": "api_token", - "label": "API Token", - "type": "password", - "required": true, - "hint": "Token with Zone:DNS:Edit permissions", - }, - }, - "documentation_url": "https://developers.cloudflare.com/api/tokens/", - }, - { - "type": "route53", - "name": "Amazon Route 53", - "fields": []gin.H{ - { - "name": "access_key_id", - "label": "Access Key ID", - "type": "text", - "required": true, - }, - { - "name": "secret_access_key", - "label": "Secret Access Key", - "type": "password", - "required": true, - }, - { - "name": "region", - "label": "AWS Region", - "type": "text", - "required": true, - "default": "us-east-1", - }, - }, - "documentation_url": "https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-routing-traffic.html", - }, - { - "type": "digitalocean", - "name": "DigitalOcean", - "fields": []gin.H{ - { - "name": "auth_token", - "label": "API Token", - "type": "password", - "required": true, - "hint": "Personal Access Token with read/write scope", - }, - }, - "documentation_url": "https://docs.digitalocean.com/reference/api/api-reference/", - }, - { - "type": "googleclouddns", - "name": "Google Cloud DNS", - "fields": []gin.H{ - { - "name": "service_account_json", - "label": "Service Account JSON", - "type": "textarea", - "required": true, - "hint": "JSON key file for service account with DNS Administrator role", - }, - { - "name": "project", - "label": "Project ID", - "type": "text", - "required": true, - }, - }, - "documentation_url": "https://cloud.google.com/dns/docs/", - }, - { - "type": "namecheap", - "name": "Namecheap", - "fields": []gin.H{ - { - "name": "api_user", - "label": "API Username", - "type": "text", - "required": true, - }, - { - "name": "api_key", - "label": "API Key", - "type": "password", - "required": true, - }, - { - "name": "client_ip", - "label": "Client IP Address", - "type": "text", - "required": true, - "hint": "Your server's public IP address (whitelisted in Namecheap)", - }, - }, - "documentation_url": "https://www.namecheap.com/support/api/intro/", - }, - { - "type": "godaddy", - "name": "GoDaddy", - "fields": []gin.H{ - { - "name": "api_key", - "label": "API Key", - "type": "text", - "required": true, - }, - { - "name": "api_secret", - "label": "API Secret", - "type": "password", - "required": true, - }, - }, - "documentation_url": "https://developer.godaddy.com/", - }, - { - "type": "azure", - "name": "Azure DNS", - "fields": []gin.H{ - { - "name": "tenant_id", - "label": "Tenant ID", - "type": "text", - "required": true, - }, - { - "name": "client_id", - "label": "Client ID", - "type": "text", - "required": true, - }, - { - "name": "client_secret", - "label": "Client Secret", - "type": "password", - "required": true, - }, - { - "name": "subscription_id", - "label": "Subscription ID", - "type": "text", - "required": true, - }, - { - "name": "resource_group", - "label": "Resource Group", - "type": "text", - "required": true, - }, - }, - "documentation_url": "https://docs.microsoft.com/en-us/azure/dns/", - }, - { - "type": "hetzner", - "name": "Hetzner", - "fields": []gin.H{ - { - "name": "api_key", - "label": "API Key", - "type": "password", - "required": true, - }, - }, - "documentation_url": "https://docs.hetzner.com/dns-console/dns/general/dns-overview/", - }, - { - "type": "vultr", - "name": "Vultr", - "fields": []gin.H{ - { - "name": "api_key", - "label": "API Key", - "type": "password", - "required": true, - }, - }, - "documentation_url": "https://www.vultr.com/api/", - }, - { - "type": "dnsimple", - "name": "DNSimple", - "fields": []gin.H{ - { - "name": "oauth_token", - "label": "OAuth Token", - "type": "password", - "required": true, - }, - { - "name": "account_id", - "label": "Account ID", - "type": "text", - "required": true, - }, - }, - "documentation_url": "https://developer.dnsimple.com/", - }, + // Get all registered providers from the global registry + providers := dnsprovider.Global().List() + + // Build response with provider metadata and fields + types := make([]gin.H, 0, len(providers)) + for _, provider := range providers { + metadata := provider.Metadata() + + // Combine required and optional fields + requiredFields := provider.RequiredCredentialFields() + optionalFields := provider.OptionalCredentialFields() + + // Convert fields to response format with required flag + fields := make([]gin.H, 0, len(requiredFields)+len(optionalFields)) + + for _, f := range requiredFields { + field := gin.H{ + "name": f.Name, + "label": f.Label, + "type": f.Type, + "required": true, + } + if f.Placeholder != "" { + field["placeholder"] = f.Placeholder + } + if f.Hint != "" { + field["hint"] = f.Hint + } + if len(f.Options) > 0 { + field["options"] = f.Options + } + fields = append(fields, field) + } + + for _, f := range optionalFields { + field := gin.H{ + "name": f.Name, + "label": f.Label, + "type": f.Type, + "required": false, + } + if f.Placeholder != "" { + field["placeholder"] = f.Placeholder + } + if f.Hint != "" { + field["hint"] = f.Hint + } + if len(f.Options) > 0 { + field["options"] = f.Options + } + fields = append(fields, field) + } + + providerType := gin.H{ + "type": metadata.Type, + "name": metadata.Name, + "description": metadata.Description, + "is_built_in": metadata.IsBuiltIn, + "fields": fields, + } + + if metadata.DocumentationURL != "" { + providerType["documentation_url"] = metadata.DocumentationURL + } + + types = append(types, providerType) } + // Sort by type for stable, predictable output (registry.List already sorts, but explicit for safety) + sort.Slice(types, func(i, j int) bool { + return types[i]["type"].(string) < types[j]["type"].(string) + }) + c.JSON(http.StatusOK, gin.H{ "types": types, }) diff --git a/backend/internal/api/handlers/dns_provider_handler_test.go b/backend/internal/api/handlers/dns_provider_handler_test.go index dae46dc3..aa118a90 100644 --- a/backend/internal/api/handlers/dns_provider_handler_test.go +++ b/backend/internal/api/handlers/dns_provider_handler_test.go @@ -7,12 +7,14 @@ import ( "errors" "net/http" "net/http/httptest" + "sort" "testing" "github.com/Wikid82/charon/backend/internal/models" "github.com/Wikid82/charon/backend/internal/services" "github.com/Wikid82/charon/backend/pkg/dnsprovider" _ "github.com/Wikid82/charon/backend/pkg/dnsprovider/builtin" // Auto-register DNS providers + _ "github.com/Wikid82/charon/backend/pkg/dnsprovider/custom" // Auto-register custom providers (manual) "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -546,41 +548,175 @@ func TestDNSProviderHandler_TestCredentials(t *testing.T) { func TestDNSProviderHandler_GetTypes(t *testing.T) { router, _ := setupDNSProviderTestRouter() - w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil) - router.ServeHTTP(w, req) + t.Run("returns registry-driven types", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil) + router.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, http.StatusOK, w.Code) - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - require.NoError(t, err) + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) - types := response["types"].([]interface{}) - assert.NotEmpty(t, types) + types := response["types"].([]interface{}) + assert.NotEmpty(t, types) - // Verify structure of first type - cloudflare := types[0].(map[string]interface{}) - assert.Equal(t, "cloudflare", cloudflare["type"]) - assert.Equal(t, "Cloudflare", cloudflare["name"]) - assert.NotEmpty(t, cloudflare["fields"]) - assert.NotEmpty(t, cloudflare["documentation_url"]) + // Build a map for easier lookup + typeMap := make(map[string]map[string]interface{}) + for _, providerData := range types { + typeData := providerData.(map[string]interface{}) + typeMap[typeData["type"].(string)] = typeData + } - // Verify all expected provider types are present - providerTypes := make(map[string]bool) - for _, t := range types { - typeMap := t.(map[string]interface{}) - providerTypes[typeMap["type"].(string)] = true - } + // Verify cloudflare (built-in) is present with correct metadata + cloudflare, exists := typeMap["cloudflare"] + assert.True(t, exists, "cloudflare provider should exist") + if exists { + assert.Equal(t, "Cloudflare", cloudflare["name"]) + assert.Equal(t, true, cloudflare["is_built_in"]) + assert.NotEmpty(t, cloudflare["description"]) + assert.NotEmpty(t, cloudflare["documentation_url"]) + assert.NotEmpty(t, cloudflare["fields"]) - expectedTypes := []string{ - "cloudflare", "route53", "digitalocean", "googleclouddns", - "namecheap", "godaddy", "azure", "hetzner", "vultr", "dnsimple", - } + // Verify field structure + fields := cloudflare["fields"].([]interface{}) + assert.NotEmpty(t, fields) + firstField := fields[0].(map[string]interface{}) + assert.NotEmpty(t, firstField["name"]) + assert.NotEmpty(t, firstField["label"]) + assert.NotEmpty(t, firstField["type"]) + _, hasRequired := firstField["required"] + assert.True(t, hasRequired, "field should have required attribute") + } - for _, expected := range expectedTypes { - assert.True(t, providerTypes[expected], "Missing provider type: "+expected) - } + // Verify manual (custom, non-built-in) is present + manual, exists := typeMap["manual"] + assert.True(t, exists, "manual provider should exist") + if exists { + assert.Equal(t, "Manual (No Automation)", manual["name"]) + assert.Equal(t, false, manual["is_built_in"]) + assert.NotEmpty(t, manual["description"]) + } + + // Verify types are sorted alphabetically + var typeNames []string + for _, providerData := range types { + typeData := providerData.(map[string]interface{}) + typeNames = append(typeNames, typeData["type"].(string)) + } + sortedNames := make([]string, len(typeNames)) + copy(sortedNames, typeNames) + sort.Strings(sortedNames) + assert.Equal(t, sortedNames, typeNames, "Types should be sorted alphabetically") + }) + + t.Run("includes all expected built-in providers", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + + types := response["types"].([]interface{}) + + // Build type map + providerTypes := make(map[string]bool) + for _, providerData := range types { + typeData := providerData.(map[string]interface{}) + providerTypes[typeData["type"].(string)] = true + } + + // Expected built-in types + expectedTypes := []string{ + "cloudflare", "route53", "digitalocean", "googleclouddns", + "namecheap", "godaddy", "azure", "hetzner", "vultr", "dnsimple", + } + + for _, expected := range expectedTypes { + assert.True(t, providerTypes[expected], "Missing provider type: "+expected) + } + + // Verify manual provider is included (custom, not built-in) + assert.True(t, providerTypes["manual"], "Missing manual provider type") + }) + + t.Run("fields include required flag", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + + types := response["types"].([]interface{}) + + // Find cloudflare to test required fields + for _, providerData := range types { + typeData := providerData.(map[string]interface{}) + if typeData["type"] == "cloudflare" { + fields := typeData["fields"].([]interface{}) + // Cloudflare has at least one required field (api_token) + foundRequired := false + for _, f := range fields { + field := f.(map[string]interface{}) + if field["name"] == "api_token" { + assert.Equal(t, true, field["required"]) + foundRequired = true + } + } + assert.True(t, foundRequired, "Cloudflare should have required api_token field") + } + + // Manual provider has optional fields + if typeData["type"] == "manual" { + fields := typeData["fields"].([]interface{}) + for _, f := range fields { + field := f.(map[string]interface{}) + // Manual provider's fields are optional + assert.Equal(t, false, field["required"]) + } + } + } + }) + + t.Run("optional field attributes are included when present", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/dns-providers/types", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response map[string]interface{} + err := json.Unmarshal(w.Body.Bytes(), &response) + require.NoError(t, err) + + types := response["types"].([]interface{}) + + // Find a provider with hint/placeholder to verify optional fields + for _, providerData := range types { + typeData := providerData.(map[string]interface{}) + if typeData["type"] == "cloudflare" { + fields := typeData["fields"].([]interface{}) + for _, f := range fields { + field := f.(map[string]interface{}) + if field["name"] == "api_token" { + // Cloudflare api_token should have hint and/or placeholder + _, hasHint := field["hint"] + _, hasPlaceholder := field["placeholder"] + assert.True(t, hasHint || hasPlaceholder, "api_token field should have hint or placeholder") + } + } + } + } + }) } func TestDNSProviderHandler_CredentialsNeverExposed(t *testing.T) { diff --git a/backend/internal/api/handlers/security_handler_waf_test.go b/backend/internal/api/handlers/security_handler_waf_test.go index 5a247478..6ce440f1 100644 --- a/backend/internal/api/handlers/security_handler_waf_test.go +++ b/backend/internal/api/handlers/security_handler_waf_test.go @@ -3,14 +3,21 @@ package handlers import ( "bytes" "encoding/json" + "fmt" "net/http" "net/http/httptest" + "os" + "path/filepath" "strings" "testing" + "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" "github.com/Wikid82/charon/backend/internal/config" "github.com/Wikid82/charon/backend/internal/models" @@ -499,7 +506,29 @@ func TestSecurityHandler_DeleteWAFExclusion_NegativeRuleID(t *testing.T) { // Integration test: Full WAF exclusion workflow func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { gin.SetMode(gin.TestMode) - db := setupTestDB(t) + + // Create a temporary file-based SQLite database for complete isolation + // This avoids all the shared memory locking issues with in-memory databases + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, fmt.Sprintf("waf_test_%d.db", time.Now().UnixNano())) + dsn := fmt.Sprintf("%s?_journal_mode=WAL&_busy_timeout=10000", dbPath) + db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + require.NoError(t, err, "failed to open test database") + + // Ensure cleanup + t.Cleanup(func() { + sqlDB, _ := db.DB() + if sqlDB != nil { + sqlDB.Close() + } + os.Remove(dbPath) + os.Remove(dbPath + "-wal") + os.Remove(dbPath + "-shm") + }) + + // Migrate the required models require.NoError(t, db.AutoMigrate(&models.SecurityConfig{}, &models.SecurityAudit{})) handler := NewSecurityHandler(config.SecurityConfig{}, db, nil) @@ -512,10 +541,12 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/security/waf/exclusions", http.NoBody) router.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusOK, w.Code, "Step 1: GET should return 200") var resp map[string]any - _ = json.Unmarshal(w.Body.Bytes(), &resp) - assert.Len(t, resp["exclusions"].([]any), 0) + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp), "Step 1: response should be valid JSON") + exclusions, ok := resp["exclusions"].([]any) + require.True(t, ok, "Step 1: exclusions should be an array") + require.Len(t, exclusions, 0, "Step 1: should start with 0 exclusions") // Step 2: Add first exclusion (full rule removal) payload := map[string]any{ @@ -527,7 +558,7 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { req, _ = http.NewRequest("POST", "/security/waf/exclusions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusOK, w.Code, "Step 2: POST first exclusion should return 200, got: %s", w.Body.String()) // Step 3: Add second exclusion (targeted) payload = map[string]any{ @@ -540,28 +571,33 @@ func TestSecurityHandler_WAFExclusion_FullWorkflow(t *testing.T) { req, _ = http.NewRequest("POST", "/security/waf/exclusions", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusOK, w.Code, "Step 3: POST second exclusion should return 200, got: %s", w.Body.String()) // Step 4: Verify both exclusions exist w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/security/waf/exclusions", http.NoBody) router.ServeHTTP(w, req) - json.Unmarshal(w.Body.Bytes(), &resp) - assert.Len(t, resp["exclusions"].([]any), 2) + require.Equal(t, http.StatusOK, w.Code, "Step 4: GET should return 200") + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp), "Step 4: response should be valid JSON") + exclusions, ok = resp["exclusions"].([]any) + require.True(t, ok, "Step 4: exclusions should be an array") + require.Len(t, exclusions, 2, "Step 4: should have 2 exclusions after adding 2") // Step 5: Delete first exclusion w = httptest.NewRecorder() req, _ = http.NewRequest("DELETE", "/security/waf/exclusions/942100", http.NoBody) router.ServeHTTP(w, req) - assert.Equal(t, http.StatusOK, w.Code) + require.Equal(t, http.StatusOK, w.Code, "Step 5: DELETE should return 200, got: %s", w.Body.String()) // Step 6: Verify only second exclusion remains w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/security/waf/exclusions", http.NoBody) router.ServeHTTP(w, req) - json.Unmarshal(w.Body.Bytes(), &resp) - exclusions := resp["exclusions"].([]any) - assert.Len(t, exclusions, 1) + require.Equal(t, http.StatusOK, w.Code, "Step 6: GET should return 200") + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp), "Step 6: response should be valid JSON") + exclusions, ok = resp["exclusions"].([]any) + require.True(t, ok, "Step 6: exclusions should be an array") + require.Len(t, exclusions, 1, "Step 6: should have 1 exclusion after deleting 1") first := exclusions[0].(map[string]any) assert.Equal(t, float64(941100), first["rule_id"]) assert.Equal(t, "ARGS:content", first["target"]) diff --git a/docs/features/custom-plugins.md b/docs/features/custom-plugins.md index 4099050d..434a9f8d 100644 --- a/docs/features/custom-plugins.md +++ b/docs/features/custom-plugins.md @@ -151,6 +151,47 @@ When running Charon in Docker: Once a plugin is installed and loaded, it appears in the DNS provider list alongside built-in providers. +### Discovering Loaded Plugins via API + +Query available provider types to see all registered providers (built-in and plugins): + +```bash +curl https://charon.example.com/api/v1/dns-providers/types \ + -H "Authorization: Bearer YOUR-TOKEN" +``` + +**Response:** + +```json +{ + "types": [ + { + "type": "cloudflare", + "name": "Cloudflare", + "description": "Cloudflare DNS provider", + "documentation_url": "https://developers.cloudflare.com/api/", + "is_built_in": true, + "fields": [...] + }, + { + "type": "powerdns", + "name": "PowerDNS", + "description": "PowerDNS Authoritative Server with HTTP API", + "documentation_url": "https://doc.powerdns.com/authoritative/http-api/", + "is_built_in": false, + "fields": [...] + } + ] +} +``` + +**Key fields:** + +| Field | Description | +|-------|-------------| +| `is_built_in` | `true` = compiled into Charon, `false` = external plugin | +| `fields` | Credential field specifications for the UI form | + ### Via Web UI 1. Navigate to **Settings** → **DNS Providers** @@ -224,6 +265,19 @@ The plugin automatically configures Caddy's DNS challenge for Let's Encrypt: ### Listing Loaded Plugins +**Via Types Endpoint (Recommended):** + +Filter for plugins using `is_built_in: false`: + +```bash +curl https://charon.example.com/api/v1/dns-providers/types \ + -H "Authorization: Bearer YOUR-TOKEN" | jq '.types[] | select(.is_built_in == false)' +``` + +**Via Plugins Endpoint:** + +Get detailed plugin metadata including version and author: + ```bash curl https://charon.example.com/api/admin/plugins \ -H "Authorization: Bearer YOUR-TOKEN" diff --git a/docs/guides/dns-providers.md b/docs/guides/dns-providers.md index 2b2694fc..dac52f8a 100644 --- a/docs/guides/dns-providers.md +++ b/docs/guides/dns-providers.md @@ -12,7 +12,13 @@ DNS providers enable Charon to obtain SSL/TLS certificates for wildcard domains ## Supported DNS Providers -Charon supports the following DNS providers through Caddy's libdns modules: +Charon dynamically discovers available DNS provider types from an internal registry. This registry includes: + +- **Built-in providers** — Compiled into Charon (Cloudflare, Route 53, etc.) +- **Custom providers** — Special-purpose providers like `manual` for unsupported DNS services +- **External plugins** — Third-party `.so` plugin files loaded at runtime + +### Built-in Providers | Provider | Type | Setup Guide | |----------|------|-------------| @@ -27,6 +33,84 @@ Charon supports the following DNS providers through Caddy's libdns modules: | Vultr | `vultr` | [Documentation](https://caddyserver.com/docs/modules/dns.providers.vultr) | | DNSimple | `dnsimple` | [Documentation](https://caddyserver.com/docs/modules/dns.providers.dnsimple) | +### Custom Providers + +| Provider | Type | Description | +|----------|------|-------------| +| Manual DNS | `manual` | For DNS providers without API support. Displays TXT record for manual creation. | + +### Discovering Available Provider Types + +Query available provider types programmatically via the API: + +```bash +curl https://your-charon-instance/api/v1/dns-providers/types \ + -H "Authorization: Bearer YOUR_TOKEN" +``` + +**Example Response:** + +```json +{ + "types": [ + { + "type": "cloudflare", + "name": "Cloudflare", + "description": "Cloudflare DNS provider", + "documentation_url": "https://developers.cloudflare.com/api/", + "is_built_in": true, + "fields": [...] + }, + { + "type": "manual", + "name": "Manual DNS", + "description": "Manually create DNS TXT records", + "documentation_url": "", + "is_built_in": false, + "fields": [] + } + ] +} +``` + +**Response fields:** + +| Field | Description | +|-------|-------------| +| `type` | Unique identifier used in API requests | +| `name` | Human-readable display name | +| `description` | Brief description of the provider | +| `documentation_url` | Link to provider's API documentation | +| `is_built_in` | `true` for compiled providers, `false` for plugins/custom | +| `fields` | Required credential fields and their specifications | + +> **Tip:** Use `is_built_in` to distinguish official providers from external plugins in your automation workflows. + +## Adding External Plugins + +Extend Charon with third-party DNS provider plugins by placing `.so` files in the plugin directory. + +### Installation + +1. Set the plugin directory environment variable: + + ```bash + export CHARON_PLUGINS_DIR=/etc/charon/plugins + ``` + +2. Copy plugin files: + + ```bash + cp powerdns.so /etc/charon/plugins/ + chmod 755 /etc/charon/plugins/powerdns.so + ``` + +3. Restart Charon — plugins load automatically at startup. + +4. Verify the plugin appears in `GET /api/v1/dns-providers/types` with `is_built_in: false`. + +For detailed plugin installation and security guidance, see [Custom Plugins](../features/custom-plugins.md). + ## General Setup Workflow ### 1. Prerequisites diff --git a/docs/plans/current_spec.md b/docs/plans/current_spec.md index 7bb31d6f..735070e3 100644 --- a/docs/plans/current_spec.md +++ b/docs/plans/current_spec.md @@ -1,965 +1,260 @@ -# PR #461 Remediation Plan: Coverage Gap & Vulnerability Resolution -**Date**: 2026-01-13 -**PR**: [#461 - DNS Challenge Support](https://github.com/Wikid82/Charon/pull/461) -**Commit**: 69f7498 -**Status**: ✅ **COMPLETE** - Ready for Merge +# Custom DNS Provider Plugin Support — Remaining Work Plan + +**Date**: 2026-01-14 + +This document is a phased completion plan for the remaining work on “Custom DNS Provider Plugin Support” on branch `feature/beta-release` (see PR #461 context in `CHANGELOG.md`). + +## What’s Already Implemented (Verified) + +- **Provider plugin registry**: `dnsprovider.Global()` registry and `dnsprovider.ProviderPlugin` interface in [backend/pkg/dnsprovider](backend/pkg/dnsprovider). +- **Built-in providers moved behind the registry**: 10 built-ins live in [backend/pkg/dnsprovider/builtin](backend/pkg/dnsprovider/builtin) and are registered via the blank import in [backend/cmd/api/main.go](backend/cmd/api/main.go). +- **External plugin loader**: `PluginLoaderService` in [backend/internal/services/plugin_loader.go](backend/internal/services/plugin_loader.go) (loads `.so`, validates metadata/interface version, optional SHA-256 allowlist, secure dir perms). +- **Plugin management backend** (Phase 5): admin endpoints in `backend/internal/api/handlers/plugin_handler.go` mounted under `/api/admin/plugins` via [backend/internal/api/routes/routes.go](backend/internal/api/routes/routes.go). +- **Example external plugin**: PowerDNS reference implementation in [plugins/powerdns](plugins/powerdns). +- **Registry-driven provider CRUD and Caddy config**: + - Provider validation/testing uses registry providers via [backend/internal/services/dns_provider_service.go](backend/internal/services/dns_provider_service.go) + - Caddy config generation is registry-driven (per Phase 5 docs) +- **Manual provider type**: `manual` provider plugin in [backend/pkg/dnsprovider/custom/manual_provider.go](backend/pkg/dnsprovider/custom/manual_provider.go). +- **Manual DNS challenge flow (UI + API)**: + - API handler: [backend/internal/api/handlers/manual_challenge_handler.go](backend/internal/api/handlers/manual_challenge_handler.go) + - Routes wired in [backend/internal/api/routes/routes.go](backend/internal/api/routes/routes.go) + - Frontend API/types: [frontend/src/api/manualChallenge.ts](frontend/src/api/manualChallenge.ts) + - Frontend UI: [frontend/src/components/dns-providers/ManualDNSChallenge.tsx](frontend/src/components/dns-providers/ManualDNSChallenge.tsx) +- **Playwright coverage exists** for manual provider flows: [tests/manual-dns-provider.spec.ts](tests/manual-dns-provider.spec.ts) + +## What’s Missing (Verified) + +- **Types endpoint is not registry-driven yet**: `GET /api/v1/dns-providers/types` is currently hardcoded in [backend/internal/api/handlers/dns_provider_handler.go](backend/internal/api/handlers/dns_provider_handler.go) and will not surface: + - the `manual` provider’s field specs + - any externally loaded plugin types (e.g., PowerDNS) + - any future custom providers registered in `dnsprovider.Global()` +- **Plugin signature allowlist is not wired**: `PluginLoaderService` supports an optional SHA-256 allowlist map, but [backend/cmd/api/main.go](backend/cmd/api/main.go) passes `nil`. +- **Sandboxing limitation is structural**: Go plugins run in-process (no OS sandbox). The only practical controls are deny-by-default plugin loading + allowlisting + secure deployment guidance. +- **No first-party webhook/script/rfc2136 provider types** exist as built-in `dnsprovider.ProviderPlugin` implementations (this is optional and should be treated as a separate feature, because external plugins already cover the extensibility goal). --- -## Executive Summary +## Scope -PR #461 has **2 blocking issues** preventing merge: +- Make DNS provider type discovery and UI configuration **registry-driven** so built-in + manual + externally loaded plugins show up correctly. +- Close the key security gap for external plugins by wiring an **operator-controlled allowlist** for plugin SHA-256 signatures. +- Keep the scope aligned to repo conventions: no Python, minimal new files, and follow the repository structure rules for any new docs. -1. **Coverage Gap**: Patch coverage at 80% (need 100%) - 7 lines missing across 2 files - - 6 lines: Audit failure error handling in encryption_handler.go - - 1 line: **BUG DISCOVERED** - undefined function call `sanitizeForLog()` on line 667 of import_handler.go (should be `util.SanitizeForLog()`) +## Non-Goals -2. **Vulnerabilities**: 8 Medium + 1 Low severity issues identified (all Alpine OS package CVEs) - - ✅ **golang.org/x/crypto already v0.47.0** (no action needed) - - 9 Alpine CVEs: 3x busybox, 6x curl (no fixes available from upstream) +- No Python scripts or example servers. +- No unrelated refactors of existing built-in providers. +- No “script execution provider” inside Charon (in-process shell execution is a separate high-risk feature and is explicitly out of scope here). +- No broad redesign of certificate issuance beyond what’s required for correct provider type discovery and safe plugin loading. -**Estimated Time**: 4-7 hours total (2-3 hours coverage + bug fix, 1-2 hours documentation, 1-2 hours validation) +## Dependencies -**Priority**: HIGH - Both must be resolved before merge +- Backend provider registry: [backend/pkg/dnsprovider/plugin.go](backend/pkg/dnsprovider/plugin.go) +- Provider loader: [backend/internal/services/plugin_loader.go](backend/internal/services/plugin_loader.go) +- DNS provider UI/API type fetch: [frontend/src/api/dnsProviders.ts](frontend/src/api/dnsProviders.ts) +- Manual challenge API (used as a reference pattern for “non-Caddy” flows): [backend/internal/api/handlers/manual_challenge_handler.go](backend/internal/api/handlers/manual_challenge_handler.go) +- Container build pipeline: [Dockerfile](Dockerfile) (Caddy built via `xcaddy`) -**Key Changes from Original Analysis**: -- ✅ Discovered actual bug in import_handler.go (not just missing test) -- ✅ Verified golang.org/x/crypto v0.47.0 already installed (removed Task 2.3) -- ✅ Got exact CVE list from CI scan (9 Alpine CVEs, not speculative) +## Risks + +- **Type discovery mismatch**: UI uses `/api/v1/dns-providers/types`; if backend remains hardcoded, registry/manual/external plugin types won’t be configurable. +- **Supply-chain risk (plugins)**: `.so` loading is inherently sensitive; SHA-256 allowlist must be operator-controlled and deny-by-default in hardened deployments. +- **No sandbox**: Go plugins execute in-process with full memory access. Treat plugins as trusted code; document this clearly and avoid implying sandboxing. +- **SSRF / outbound calls**: plugins may implement `TestCredentials()` with outbound HTTP. Core cannot reliably enforce SSRF policy inside plugin code; mitigate via operational controls (restricted egress, allowlisted outbound via infra) and guidance for plugin authors to reuse Charon URL validators. +- **Patch coverage gate**: any production changes must maintain 100% patch coverage for modified lines. --- -## Issue 1: Coverage Gap Analysis +## Definition of Done (DoD) Verification Gates (Per Phase) -### Problem Statement +Repository testing protocol requires Playwright E2E **before** unit tests. -Codecov reports **80% patch coverage** with 7 lines missing coverage: +- **E2E (first)**: `npx playwright test --project=chromium` +- **Backend tests**: VS Code task `shell: Test: Backend with Coverage` +- **Frontend tests**: VS Code task `shell: Test: Frontend with Coverage` +- **TypeScript**: VS Code task `shell: Lint: TypeScript Check` +- **Pre-commit**: VS Code task `shell: Lint: Pre-commit (All Files)` +- **Security scans**: + - VS Code tasks `shell: Security: CodeQL Go Scan (CI-Aligned) [~60s]` and `shell: Security: CodeQL JS Scan (CI-Aligned) [~90s]` + - VS Code task `shell: Security: Trivy Scan` + - VS Code task `shell: Security: Go Vulnerability Check` -| File | Patch Coverage | Missing Lines | Partial Lines | -|------|----------------|---------------|---------------| -| \`backend/internal/api/handlers/encryption_handler.go\` | 60% | 4 | 2 | -| \`backend/internal/api/handlers/import_handler.go\` | 50% | 1 | 0 | - -**Requirement**: 100% patch coverage on all modified lines - -### Coverage Analysis - encryption_handler.go - -**Current Test Coverage**: Existing tests cover happy paths and admin checks, but miss error scenarios. - -#### Missing Coverage Lines (Analysis) - -Based on the handler code and test file analysis: - -**1. Line ~77: Audit Log Error in Rotate (first audit failure)** -\`\`\`go -if err := h.securityService.LogAudit(&models.SecurityAudit{...}); err != nil { - logger.Log().WithError(err).Warn("Failed to log audit event") // NOT COVERED -} -\`\`\` -**Test Case Needed**: Simulate audit logging failure during rotation start - -**2. Lines ~87-88: Audit Log Error in Rotate (rotation failure audit)** -\`\`\`go -if auditErr := h.securityService.LogAudit(&models.SecurityAudit{...}); auditErr != nil { - logger.Log().WithError(auditErr).Warn("Failed to log audit event") // NOT COVERED -} -\`\`\` -**Test Case Needed**: Simulate audit logging failure during rotation failure logging - -**3. Line ~108: Audit Log Error in Rotate (completion audit)** -\`\`\`go -if err := h.securityService.LogAudit(&models.SecurityAudit{...}); err != nil { - logger.Log().WithError(err).Warn("Failed to log audit event") // NOT COVERED -} -\`\`\` -**Test Case Needed**: Simulate audit logging failure during successful rotation completion - -**4. Lines ~181-182: Partial Coverage - Audit Log Error in Validate (validation failure)** -\`\`\`go -if auditErr := h.securityService.LogAudit(&models.SecurityAudit{...}); auditErr != nil { - logger.Log().WithError(auditErr).Warn("Failed to log audit event") // PARTIAL -} -\`\`\` -**Test Case Needed**: Simulate audit logging failure during validation failure - -**5. Lines ~200-201: Partial Coverage - Audit Log Error in Validate (validation success)** -\`\`\`go -if err := h.securityService.LogAudit(&models.SecurityAudit{...}); err != nil { - logger.Log().WithError(err).Warn("Failed to log audit event") // PARTIAL -} -\`\`\` -**Test Case Needed**: Simulate audit logging failure during validation success - -**6. Line ~178: Validate failure path with bad request response** -\`\`\`go -c.JSON(http.StatusBadRequest, gin.H{ - "valid": false, - "error": err.Error(), -}) -\`\`\` -**Test Case Needed**: Already exists (TestEncryptionHandler_Validate → "validation fails with invalid key configuration") but may need SecurityService close to trigger audit failure - -### Coverage Analysis - import_handler.go - -**Current Test Coverage**: Extensive tests for Upload, Commit, Cancel flows. Missing one specific error path. - -#### Missing Coverage Line (Analysis) - -**⚠️ CORRECTED: Line 667 - Undefined Function Call** - -**Actual Issue** (verified via code inspection): -\`\`\`go -middleware.GetRequestLogger(c).WithField("host", util.SanitizeForLog(host.DomainNames)).WithField("error", sanitizeForLog(errMsg)).Error("Import Commit Error (update)") -\`\`\` - -**Problem**: Line 667 calls `sanitizeForLog(errMsg)` but this function **does not exist** in the file. The correct function is `util.SanitizeForLog()`. - -**This is a BUG, not a test coverage issue.** - -**Required Fix**: Change line 667 from: -\`\`\`go -.WithField("error", sanitizeForLog(errMsg)) -\`\`\` -To: -\`\`\`go -.WithField("error", util.SanitizeForLog(errMsg)) -\`\`\` - -**Test Case Needed**: Test the "overwrite" action commit path that exercises line 667 with an update error to ensure the logging works correctly after the fix +**Patch coverage requirement**: 100% for modified lines. --- -## Issue 2: Vulnerability Analysis +## Phase 1 — Registry-Driven Type Discovery (Unblocks UI + plugins) -### Problem Statement +### Deliverables -Supply chain scan detected **9 vulnerabilities** in PR #461: +- Backend `GET /api/v1/dns-providers/types` returns **registry-driven** types, names, fields, and docs URLs. +- The types list includes: built-in providers, `manual`, and any external plugins loaded from `CHARON_PLUGINS_DIR`. +- Unit tests cover the new type discovery logic with 100% patch coverage on modified lines. -| Severity | Count | Status | -|----------|-------|--------| -| 🔴 Critical | 0 | ✅ Clean | -| 🟠 High | 0 | ✅ Clean | -| 🟡 Medium | 8 | ⚠️ Action Required | -| 🟢 Low | 1 | ⚠️ Review Required | +### Tasks & Owners -**Source**: [Supply Chain PR Comment](https://github.com/Wikid82/Charon/pull/461#issuecomment-3746737390) +- **Backend_Dev** + - Replace hardcoded type list behavior in [backend/internal/api/handlers/dns_provider_handler.go](backend/internal/api/handlers/dns_provider_handler.go) with registry output. + - Use the service as the abstraction boundary: + - `h.service.GetSupportedProviderTypes()` for the type list + - `h.service.GetProviderCredentialFields(type)` for field specs + - `dnsprovider.Global().Get(type).Metadata()` for display name + docs URL + - Ensure the handler returns a stable, sorted list for predictable UI rendering. + - Add/adjust tests for the types endpoint. +- **Frontend_Dev** + - Confirm `getDNSProviderTypes()` is used as the single source of truth where appropriate. + - Keep the fallback schemas in `frontend/src/data/dnsProviderSchemas.ts` as a defensive measure, but prefer server-provided fields. +- **QA_Security** + - Validate that a newly registered provider type becomes visible in the UI without a frontend deploy. +- **Docs_Writer** + - Update operator docs explaining how types are surfaced and how plugins affect the UI. -### Vulnerability Breakdown (VERIFIED from CI Scan) +### Acceptance Criteria -**All vulnerabilities are Alpine OS package CVEs - NO application-level vulnerabilities:** +- Creating a `manual` provider is possible end-to-end using the types endpoint output. +- `/api/v1/dns-providers/types` includes `manual` and any externally loaded provider types (when present). +- 100% patch coverage for modified lines. -#### Medium Severity (8 total) +### Verification Gates -**1. CVE-2025-60876: busybox (3 packages affected)** -- **Package**: busybox, busybox-binsh, ssl_client -- **Version**: 1.37.0-r20 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) -- **Description**: Heap buffer overflow requiring local shell access -- **Risk**: LOW - Charon doesn't expose shell access to users - -**2. CVE-2025-15079: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) - -**3. CVE-2025-14819: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) - -**4. CVE-2025-14524: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) - -**5. CVE-2025-13034: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) - -**6. CVE-2025-10966: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) -- **Description**: Cookie bypass vulnerability -- **Risk**: MEDIUM - curl only used for internal healthcheck scripts, no user-controllable URLs - -#### Low Severity (1 total) - -**7. CVE-2025-15224: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) - -#### Unknown Severity (1 total) - -**8. CVE-2025-14017: curl** -- **Package**: curl -- **Version**: 8.14.1-r2 -- **Fixed**: None available yet -- **Type**: apk (Alpine package) - -**Total**: 8 Medium + 1 Low + 1 Unknown = **10 CVE findings** (9 counted as actionable) - -### Critical Finding: golang.org/x/crypto Already Fixed ✅ - -**Verification Result** (from `go mod graph`): -\`\`\` -github.com/Wikid82/charon/backend golang.org/x/crypto@v0.47.0 ✅ (SAFE) -github.com/go-playground/validator/v10@v10.30.1 golang.org/x/crypto@v0.46.0 ✅ (SAFE) -\`\`\` - -**Status**: golang.org/x/crypto v0.47.0 is already installed and is well above the v0.45.0 minimum required to fix GHSA-j5w8-q4qc-rx2x and GHSA-f6x5-jh6r-wrfv. - -**Action**: ✅ **NO ACTION REQUIRED** - Remove Task 2.3 from remediation plan - -### Remediation Strategy - -**All 9 vulnerabilities are Alpine OS-level packages with no available fixes:** -- 3x busybox-related (CVE-2025-60876) -- 6x curl-related (CVE-2025-15079, CVE-2025-14819, CVE-2025-14524, CVE-2025-13034, CVE-2025-10966, CVE-2025-15224, CVE-2025-14017) - -**Recommended Approach**: ACCEPT with documented mitigations and review date (consistent with Alpine CVE acceptance pattern) +- If UI changed: run Playwright E2E first. +- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans. --- -## Phase 1: Coverage Remediation (2-3 hours) +## Phase 2 — Provider Implementations: `rfc2136`, `webhook`, `script` -### Task 1.1: Add Audit Failure Tests to encryption_handler_test.go +This phase is **optional** and should only proceed if we explicitly want “first-party” provider types inside Charon (instead of shipping these as external `.so` plugins). External plugins already satisfy the extensibility goal. -**File**: \`backend/internal/api/handlers/encryption_handler_test.go\` +### Deliverables -**New Test Cases Required**: +- New provider plugins implemented (as `dnsprovider.ProviderPlugin`): + - `rfc2136` + - `webhook` + - `script` +- Each provider defines: + - `Metadata()` (name/description/docs) + - `CredentialFields()` (field definitions for UI) + - Validation (required fields, value constraints) + - `BuildCaddyConfig()` (or explicit alternate flow) with deterministic JSON output -1. **TestEncryptionHandler_Rotate_AuditStartFailure** - - Simulate audit logging failure when logging "encryption_key_rotation_started" - - Verify rotation proceeds despite audit failure - - Verify warning logged +### Tasks & Owners -2. **TestEncryptionHandler_Rotate_AuditFailureFailure** - - Simulate audit logging failure when logging "encryption_key_rotation_failed" - - Verify rotation failure is still returned to client - - Verify warning logged +- **Backend_Dev** + - Add provider plugin files under [backend/pkg/dnsprovider/custom](backend/pkg/dnsprovider/custom) (pattern matches `manual_provider.go`). + - Define clear field schemas for each type (avoid guessing provider-specific parameters not supported by the underlying runtime; keep minimal + extensible). + - Implement validation errors that are actionable (which field, what’s wrong). + - Add unit tests for each provider plugin: + - metadata + - fields + - validation + - config generation +- **Frontend_Dev** + - Ensure provider forms render correctly from server-provided field definitions. + - Ensure any provider-specific help text uses the docs URL from the server type info. +- **Docs_Writer** + - Add/update docs pages for each provider type describing required fields and operational expectations. -3. **TestEncryptionHandler_Rotate_AuditCompletionFailure** - - Simulate audit logging failure when logging "encryption_key_rotation_completed" - - Verify rotation result is still returned successfully - - Verify warning logged +### Docker/Caddy Decision Checkpoint (Only if needed) -4. **TestEncryptionHandler_Validate_AuditFailureOnError** - - Simulate audit logging failure when validation fails - - Verify validation error response still returned - - Verify audit warning logged +Before changing Docker/Caddy: -5. **TestEncryptionHandler_Validate_AuditFailureOnSuccess** - - Simulate audit logging failure when validation succeeds - - Verify success response still returned - - Verify audit warning logged +- Confirm whether the running Caddy build includes the required DNS modules for the new types. +- If a module is required and not present, update [Dockerfile](Dockerfile) `xcaddy build` arguments to include it. -**Implementation Strategy**: -\`\`\`go -// Create a mock SecurityService that returns errors on LogAudit -// OR close the SecurityService database connection mid-test -// OR use a full integration test with database closed -\`\`\` +### Acceptance Criteria -**Acceptance Criteria**: -- All 5 new tests pass -- Patch coverage for encryption_handler.go reaches 100% -- No regressions in existing tests +- `rfc2136`, `webhook`, and `script` show up in `/dns-providers/types` with complete field definitions. +- Creating and saving a provider of each type succeeds with validation. +- 100% patch coverage for modified lines. -### Task 1.2: Fix Bug and Add Test for import_handler.go +### Verification Gates -**File**: \`backend/internal/api/handlers/import_handler.go\` - -**⚠️ CRITICAL BUG FIX REQUIRED** - -**Current Code (Line 667)**: -\`\`\`go -middleware.GetRequestLogger(c).WithField("host", util.SanitizeForLog(host.DomainNames)).WithField("error", sanitizeForLog(errMsg)).Error("Import Commit Error (update)") -\`\`\` - -**Problem**: `sanitizeForLog()` function does not exist - should be `util.SanitizeForLog()` - -**Fix**: -\`\`\`go -middleware.GetRequestLogger(c).WithField("host", util.SanitizeForLog(host.DomainNames)).WithField("error", util.SanitizeForLog(errMsg)).Error("Import Commit Error (update)") -\`\`\` - -**New Test Case Required**: - -1. **TestImportHandler_Commit_OverwriteUpdateError** - - Set up "overwrite" action for existing host - - Mock proxyHostSvc.Update() to return error - - Verify error logging calls util.SanitizeForLog correctly - - Verify error message added to response - - This will cover line 667 after the fix - -**Implementation Strategy**: -\`\`\`go -func TestImportHandler_Commit_OverwriteUpdateError(t *testing.T) { - // Create existing host in mock database - existingHost := &models.ProxyHost{ - DomainNames: "example.com", - // ... other fields - } - - // Mock proxyHostSvc.Update() to return error - mockProxyHostSvc.On("Update", mock.Anything).Return(errors.New("database error")) - - // Commit with "overwrite" action - req := ImportCommitRequest{ - SessionUUID: sessionID, - Resolutions: map[string]string{ - "example.com": "overwrite", - }, - } - - // Execute request - w := httptest.NewRecorder() - router.ServeHTTP(w, makeRequest(req)) - - // Verify error in response - assert.Equal(t, http.StatusOK, w.Code) - var resp map[string]interface{} - json.Unmarshal(w.Body.Bytes(), &resp) - assert.Contains(t, resp["errors"].([]interface{}), "example.com: database error") -} -\`\`\` - -**Acceptance Criteria**: -- Bug fix applied to line 667 -- New test passes -- Patch coverage for import_handler.go reaches 100% -- No regressions in existing tests - -### Task 1.3: Run Backend Test Suite with Coverage - -**Command**: -\`\`\`bash -cd /projects/Charon -.github/skills/scripts/skill-runner.sh test-backend-coverage -\`\`\` - -**Validation**: -- Check coverage report: \`backend/coverage.html\` -- Verify encryption_handler.go: 100% of new lines -- Verify import_handler.go: 100% of new lines -- Overall backend coverage: ≥85% +- If UI changed: run Playwright E2E first. +- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans. --- -## Phase 2: Vulnerability Remediation (1-2 hours) +## Phase 3 — Plugin Security Hardening & Operator Controls -### Task 2.1: Document Alpine OS Vulnerabilities (Accept Risk) +### Deliverables -**Action**: All 9 vulnerabilities in PR #461 are Alpine OS package CVEs with no available fixes +- Documented and configurable plugin loading policy: + - plugin directory (`CHARON_PLUGINS_DIR` already used by startup in [backend/cmd/api/main.go](backend/cmd/api/main.go)) + - optional SHA-256 allowlist support wired end-to-end (from config/env → `NewPluginLoaderService(..., allowedSignatures)`) +- Minimal operator guidance for secure deployment. -**Files**: -- \`docs/security/VULNERABILITY_ACCEPTANCE.md\` (update existing or create) -- \`SECURITY.md\` (update) +### Tasks & Owners -**Alpine OS CVEs to Document**: +- **Backend_Dev** + - Wire a configuration source for plugin signatures into the `PluginLoaderService` creation path (currently passed `nil` in [backend/cmd/api/main.go](backend/cmd/api/main.go)). + - Prefer a single env var to stay minimal (example format: JSON map of `pluginName` → `sha256:...`). + - Add tests covering: + - allowlist reject (plugin not in allowlist) + - signature mismatch + - insecure directory permissions rejection +- **DevOps** + - Ensure the plugin directory is mounted read-only where feasible. + - Validate container permissions align with `verifyDirectoryPermissions()` expectations. +- **QA_Security** + - Threat model review focused on `.so` loading risks and expected mitigations. +- **Docs_Writer** + - Update plugin operator docs to explain allowlisting, signatures, and safe deployment patterns. -**Busybox CVEs (3 packages):** -- CVE-2025-60876 (busybox, busybox-binsh, ssl_client) - Heap buffer overflow +### Acceptance Criteria -**Curl CVEs (6 vulnerabilities):** -- CVE-2025-15079 -- CVE-2025-14819 -- CVE-2025-14524 -- CVE-2025-13034 -- CVE-2025-10966 (cookie bypass) -- CVE-2025-15224 (LOW) -- CVE-2025-14017 (UNKNOWN) +- Plugins can be loaded successfully when allowed, and rejected when disallowed. +- Misconfigured (world-writable) plugin directory is detected and prevents loading. +- 100% patch coverage for modified lines. -**Content Template**: +### Verification Gates -\`\`\`markdown -# Accepted Vulnerabilities - PR #461 - -## Alpine Base Image CVEs (as of 2026-01-13) - -**Decision Date**: 2026-01-13 -**Reviewed By**: [Team/Individual] -**Status**: ACCEPTED (No fix available from Alpine upstream) - -### CVE-2025-60876: busybox utilities (3 packages) -- **Severity**: MEDIUM -- **Affected**: busybox 1.37.0-r20, busybox-binsh 1.37.0-r20, ssl_client 1.37.0-r20 -- **Fixed Version**: None available -- **Exploitability**: LOW (requires local shell access) - -**Rationale**: -- Heap buffer overflow requires local shell access -- Charon doesn't expose shell access to users -- Container runs with minimal privileges -- Alpine upstream has not released patch yet - -**Mitigation**: -- Container runs as non-root user -- No shell access exposed through application -- Container isolation provides defense-in-depth -- Monitoring Alpine security advisories for updates - -**Review Date**: 2026-02-13 (30 days) +- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans. --- -### CVE-2025-15079, CVE-2025-14819, CVE-2025-14524, CVE-2025-13034, CVE-2025-10966: curl -- **Severity**: MEDIUM (5 CVEs), LOW (1 CVE: CVE-2025-15224) -- **Affected**: curl 8.14.1-r2 -- **Fixed Version**: None available -- **Exploitability**: MEDIUM (requires network access, user-controlled URLs) +## Phase 4 — E2E Coverage + Regression Safety -**Rationale**: -- Alpine upstream has not released patches yet -- curl only used for internal healthcheck scripts -- No user-controllable URLs passed to curl -- Limited attack surface in containerized environment +### Deliverables -**Mitigation**: -- Healthcheck URLs are hardcoded in configuration -- No user input in curl commands -- Container network segmentation -- Monitoring Alpine security advisories for updates +- Playwright coverage for: + - DNS provider types rendering and required-field validation (including plugin types) + - Manual DNS challenge flow regression (existing spec: `tests/manual-dns-provider.spec.ts`) + - Creating a provider for at least one external plugin type (e.g., `powerdns`) when a plugin is present +- Documented smoke test steps for operators. -**Review Date**: 2026-02-13 (30 days) -\`\`\` +### Tasks & Owners -**SECURITY.md Update**: -\`\`\`markdown -## Known Issues (Under Monitoring) +- **QA_Security** + - Add/extend Playwright specs under [tests](tests). + - Validate keyboard navigation and form errors are accessible (screen reader friendly) where tests touch UI. +- **Frontend_Dev** + - Fix any UI issues uncovered by E2E (focus order, error announcements, labels). +- **Backend_Dev** + - Fix any API contract mismatches discovered by E2E. -### Alpine Base Image Vulnerabilities (2026-01-13) +### Acceptance Criteria -Nine vulnerabilities in Alpine 3.23.0 base image are being monitored: +- E2E passes reliably in Chromium. +- No regressions to manual challenge flow. -- **CVE-2025-60876** (busybox): No patch available. Low exploitability (requires shell access). -- **6x curl CVEs** (CVE-2025-15079, 14819, 14524, 13034, 10966, 15224): No patches available. Limited attack surface (healthchecks only, hardcoded URLs). +### Verification Gates -**Status**: Accepted with documented mitigations. Will update when Alpine releases patches. -**Review Date**: 2026-02-13 -**Details**: See [VULNERABILITY_ACCEPTANCE.md](docs/security/VULNERABILITY_ACCEPTANCE.md) -\`\`\` - -**Acceptance Criteria**: -- All 9 Alpine CVEs documented -- Rationale clearly explained -- Review date set (30 days) -- Team/security approval obtained (if required) - -### Task 2.2: Update CHANGELOG.md - -**File**: \`CHANGELOG.md\` - -**Add Entry**: -\`\`\`markdown -## [Unreleased] - -### Security - -- **DOCUMENTED**: Alpine base image vulnerabilities (9 CVEs total): - - CVE-2025-60876 (busybox, 3 packages) - - 6x curl CVEs (CVE-2025-15079, CVE-2025-14819, CVE-2025-14524, CVE-2025-13034, CVE-2025-10966, CVE-2025-15224) - - Status: No patches available from Alpine upstream, risk accepted with mitigations - - Review scheduled: 2026-02-13 -- **VERIFIED**: golang.org/x/crypto already at v0.47.0 (well above v0.45.0 minimum required) -\`\`\` +- Run Playwright E2E first. +- Run backend + frontend coverage tasks, TypeScript check, pre-commit, and security scans. --- -## Phase 3: Validation & Testing ✅ **COMPLETE** +## Open Questions (Need Explicit Decisions) -### Task 3.1: Run Full Test Suite ✅ - -**Backend Tests**: -\`\`\`bash -cd /projects/Charon -.github/skills/scripts/skill-runner.sh test-backend-coverage -\`\`\` - -**Expected**: -- All tests pass (including 6 new coverage tests) -- Coverage ≥85% -- Patch coverage: 100% - -**Frontend Tests**: -\`\`\`bash -cd /projects/Charon -.github/skills/scripts/skill-runner.sh test-frontend-coverage -\`\`\` - -**Expected**: -- All tests pass -- No regressions - -### Task 3.2: Rebuild and Scan Image - -**Commands**: -\`\`\`bash -# Rebuild image with fixes -docker build -t charon:test-461 . - -# Run Trivy scan -trivy image charon:test-461 --severity MEDIUM,HIGH,CRITICAL --format table - -# Run govulncheck on backend -cd backend && govulncheck ./... -\`\`\` - -**Expected Results**: -- 0 Critical vulnerabilities -- 0 High vulnerabilities -- 2 Medium vulnerabilities (Alpine - accepted) -- 0 Low vulnerabilities (or documented) - -### Task 3.3: Integration Tests - -**Commands**: -\`\`\`bash -# Start test environment -.github/skills/scripts/skill-runner.sh docker-start-dev - -# Run integration tests -.github/skills/scripts/skill-runner.sh integration-test-all - -# Stop environment -.github/skills/scripts/skill-runner.sh docker-stop-dev -\`\`\` - -**Expected**: -- All integration tests pass -- No regressions in functionality - -### Task 3.2: Run Security Scans ✅ - -**Pre-commit Hooks**: ✅ All passed -**Go Vulnerabilities**: ✅ No vulnerabilities found in application code - -### Task 3.3: Integration Tests ⏭️ **SKIPPED** - -Not required for patch coverage validation. Will run in CI. - -### Task 3.4: Generate Validation Report ✅ - -**File**: `docs/reports/pr_461_remediation_complete.md` ✨ **CREATED** - -**Summary**: -- ✅ All backend tests pass (85.4% coverage, 100% patch) -- ✅ All frontend tests pass (85.93% coverage) -- ✅ Pre-commit hooks pass -- ✅ No Go vulnerabilities in application code -- ✅ Bug fix verified (import_handler.go line 667) -- ✅ 6 new audit failure tests added -- ✅ All 9 Alpine CVEs documented and accepted - -**Report Location**: `docs/reports/pr_461_remediation_complete.md` +- For plugin signature allowlisting: what is the desired configuration shape? + - **Option A (minimal)**: env var JSON map `pluginName` → `sha256:...` parsed by [backend/cmd/api/main.go](backend/cmd/api/main.go) + - **Option B (operator-friendly)**: load from a mounted file path (adds new config surface) +- For “first-party” providers (`webhook`, `script`, `rfc2136`): are these still required given external plugins already exist? --- -## Remediation Complete ✅ +## Notes on Accessibility -**Date Completed**: 2026-01-13 22:30 UTC -**Total Time**: ~4 hours (within estimate) -**Status**: **READY FOR MERGE** - -### What Was Accomplished - -**Phase 1: Coverage Remediation** ✅ -- Fixed bug in import_handler.go (line 667: undefined function) -- Added 6 audit failure test cases to encryption_handler_test.go -- Achieved 100% patch coverage (was 80%) - -**Phase 2: Vulnerability Remediation** ✅ -- Verified golang.org/x/crypto v0.47.0 (safe) -- Documented all 9 Alpine OS CVEs with acceptance rationale -- Updated SECURITY.md and created VULNERABILITY_ACCEPTANCE.md -- Set review date: 2026-02-13 - -**Phase 3: Final Validation** ✅ -- All backend tests pass (85.4% coverage) -- All frontend tests pass (85.93% coverage) -- Pre-commit hooks pass -- Go vulnerability check pass (no app vulnerabilities) -- Final report generated - -### Files Modified/Created (8) - -**Modified**: -1. `backend/internal/api/handlers/import_handler.go` (bug fix) -2. `backend/internal/api/handlers/encryption_handler_test.go` (6 new tests) -3. `docs/plans/current_spec.md` (status updates) -4. `SECURITY.md` (CVE documentation) -5. `.github/renovate.json` (trailing whitespace fix) - -**Created**: -6. `docs/security/VULNERABILITY_ACCEPTANCE.md` ✨ -7. `docs/reports/pr_461_remediation_complete.md` ✨ -8. `docs/reports/pr_461_vulnerability_comment.md` (earlier) - -### Approval Status - -- [x] **Phase 1**: Coverage remediation complete -- [x] **Phase 2**: Vulnerability documentation complete -- [x] **Phase 3**: Final validation complete -- [x] **Tests**: All passing -- [x] **Coverage**: 100% patch, ≥85% overall -- [x] **Security**: All scans passing -- [x] **Documentation**: Complete -- [ ] **Code Review**: Pending maintainer approval -- [ ] **Security Approval**: Pending sign-off on CVE acceptance - -### Commit Message - -See `docs/reports/pr_461_remediation_complete.md` for suggested commit message. - ---- - -## Archive Note - -This specification has been successfully completed. All phases executed as planned with no major deviations. Final report available at `docs/reports/pr_461_remediation_complete.md`. - -**Next Action**: Submit for code review and merge when approved. - ---- -\`\`\`markdown -# PR #461 Remediation Validation Report - -**Date**: 2026-01-13 -**PR**: #461 -**Validator**: [Name] - -## Coverage Validation - -✅ **PASS**: Patch coverage 100% -- encryption_handler.go: 100% (6 missing lines fixed with audit failure tests) -- import_handler.go: 100% (1 bug fixed + test added for line 667) - -**Test Results**: -- Backend: 548/548 passing (+6 new tests) -- Frontend: 128/128 passing -- Coverage: 85.4% (above 85% threshold) - -## Bug Fix Validation - -✅ **FIXED**: Line 667 undefined function call -- **Before**: `sanitizeForLog(errMsg)` (function doesn't exist) -- **After**: `util.SanitizeForLog(errMsg)` (correct utility function) -- **Test**: TestImportHandler_Commit_OverwriteUpdateError passes - -## Vulnerability Validation - -✅ **PASS**: All vulnerabilities addressed - -**Verified as Already Fixed**: -- golang.org/x/crypto: v0.47.0 installed (well above v0.45.0 minimum) ✅ - -**Documented & Accepted** (No fix available from Alpine): -- CVE-2025-60876 (busybox, 3 packages): Heap buffer overflow - LOW risk -- CVE-2025-15079 (curl): MEDIUM -- CVE-2025-14819 (curl): MEDIUM -- CVE-2025-14524 (curl): MEDIUM -- CVE-2025-13034 (curl): MEDIUM -- CVE-2025-10966 (curl): Cookie bypass - MEDIUM -- CVE-2025-15224 (curl): LOW -- CVE-2025-14017 (curl): UNKNOWN - -**Final Scan Results**: -| Severity | Count | Status | -|----------|-------|--------| -| Critical | 0 | ✅ | -| High | 0 | ✅ | -| Medium | 8 | ✅ Accepted (Alpine OS, no upstream fix) | -| Low | 1 | ✅ Accepted | - -**Documentation**: -- ✅ VULNERABILITY_ACCEPTANCE.md updated -- ✅ SECURITY.md updated -- ✅ CHANGELOG.md updated -- ✅ Review date set: 2026-02-13 - -## Functionality Validation - -✅ **PASS**: All tests passing -- Unit tests: 676/676 ✅ -- Integration tests: 12/12 ✅ -- E2E tests: Pending (manual) - -## Backward Compatibility - -✅ **PASS**: No breaking changes -- API endpoints: No changes -- Database schema: No changes -- Configuration: No changes -- Docker image: Same base, no functional changes - -## Approval Checklist - -This PR is ready for merge after: -- [ ] Code review approval -- [ ] Security team sign-off on Alpine CVE acceptance -- [ ] E2E test validation (Playwright) -- [ ] Final CI checks pass - -## Rollback Plan - -If issues arise post-merge: -1. Revert PR: `git revert -m 1` -2. Investigate locally -3. Create fix-forward PR with root cause analysis - -## Review Notes - -**Supervisor Corrections Applied**: -1. ✅ Fixed import_handler.go analysis - found actual bug on line 667 -2. ✅ Verified golang.org/x/crypto v0.47.0 already installed -3. ✅ Got exact CVE list from CI scan (9 Alpine CVEs) -4. ✅ Added risk mitigation strategies -5. ✅ Added rollback procedures -6. ✅ Added backward compatibility validation -\`\`\` - ---- - -## Risk Mitigation Strategies - -### Coverage Remediation Risks - -**Risk 1: Test Flakiness** -- **Mitigation**: Run tests 3 times locally before committing -- **Mitigation**: Use deterministic mocks and avoid time-based assertions -- **Mitigation**: Ensure proper test isolation and cleanup - -**Risk 2: Test Coverage Doesn't Catch Real Bugs** -- **Mitigation**: Combine unit tests with integration tests -- **Mitigation**: Verify actual error handling behavior, not just code paths -- **Mitigation**: Review test quality during code review - -**Risk 3: Bug Fix (Line 667) Introduces Regressions** -- **Mitigation**: Run full test suite after fix -- **Mitigation**: Verify import handler integration tests pass -- **Mitigation**: Test in local Docker environment before merge - -### Vulnerability Remediation Risks - -**Risk 1: Alpine Vulnerabilities Become Exploitable** -- **Mitigation**: Monthly review schedule (2026-02-13) -- **Mitigation**: Subscribe to Alpine security advisories -- **Mitigation**: Monitor for proof-of-concept exploits -- **Mitigation**: Have upgrade path ready if critical - -**Risk 2: New Vulnerabilities Appear After Merge** -- **Mitigation**: CI scans every PR and commit -- **Mitigation**: Automated alerts via GitHub Security -- **Mitigation**: Weekly Renovate updates - -**Risk 3: False Sense of Security from Acceptance** -- **Mitigation**: Document clear review dates -- **Mitigation**: Require security team sign-off -- **Mitigation**: Monitor real-world attack trends - ---- - -## Rollback Procedures - -### If Tests Fail in CI After Merge - -1. **Immediate**: Revert PR via GitHub web interface - \`\`\`bash - git revert -m 1 - git push origin main - \`\`\` - -2. **Investigation**: Run tests locally to reproduce - \`\`\`bash - cd /projects/Charon - .github/skills/scripts/skill-runner.sh test-backend-coverage - \`\`\` - -3. **Fix Forward**: Create new PR with fix - - Reference original PR - - Include root cause analysis - -### If Vulnerabilities Worsen Post-Merge - -1. **Immediate**: Check if new CVEs are critical/high - \`\`\`bash - trivy image ghcr.io/wikid82/charon:latest --severity CRITICAL,HIGH - \`\`\` - -2. **If Critical**: Create hotfix branch - - Upgrade affected packages immediately - - Fast-track through CI - - Deploy emergency patch - -3. **If High**: Follow normal remediation process - - Create issue - - Schedule fix in next sprint - - Update risk documentation - -### If Integration Breaks After Merge - -1. **Immediate**: Verify Docker compose still works - \`\`\`bash - docker compose -f .docker/compose/docker-compose.yml up -d - curl http://localhost:8080/health - \`\`\` - -2. **If Broken**: Revert PR immediately - - Post incident report - - Update test coverage gaps - -3. **Recovery**: Run integration test suite - \`\`\`bash - .github/skills/scripts/skill-runner.sh integration-test-all - \`\`\` - ---- - -## Backward Compatibility Validation - -### API Compatibility - -**Affected Endpoints**: None - changes are internal (test coverage + logging fix) - -**Validation**: -- All existing API tests pass -- No changes to request/response schemas -- No changes to authentication/authorization - -### Database Compatibility - -**Affected Tables**: None - no schema changes - -**Validation**: -- No migrations added -- Existing data unaffected -- Import sessions continue to work - -### Configuration Compatibility - -**Affected Config**: None - no new environment variables or settings - -**Validation**: -- Existing Caddyfiles load correctly -- Import feature works as before -- CrowdSec integration unchanged - -### Docker Image Compatibility - -**Affected Layers**: Alpine base image (same version) - -**Validation**: -- Image size remains similar -- Startup time unchanged -- Healthchecks pass -- Volume mounts work - -**Compatibility Matrix**: - -| Component | Change | Breaking? | Action | -|-----------|--------|-----------|--------| -| Backend API | Test coverage only | ❌ No | None | -| Import Handler | Bug fix (logging) | ❌ No | None | -| Dependencies | None (golang.org/x/crypto already v0.47.0) | ❌ No | None | -| Alpine CVEs | Documented acceptance | ❌ No | None | -| Docker Image | No functional changes | ❌ No | None | - -**Upgrade Path**: Direct upgrade, no special steps required - ---- - -## Success Criteria - -### Coverage Requirements -- [ ] All 7 missing lines have test coverage -- [ ] Patch coverage reaches 100% -- [ ] No test regressions -- [ ] Overall coverage ≥85% - -### Vulnerability Requirements -- [ ] CVE-2025-68156 fixed (CrowdSec expr upgrade) -- [ ] golang.org/x/crypto vulnerabilities fixed -- [ ] Alpine vulnerabilities documented and accepted -- [ ] All Critical/High vulnerabilities resolved -- [ ] SECURITY.md and CHANGELOG.md updated -- [ ] Validation report generated - -### Testing Requirements -- [ ] All unit tests pass -- [ ] All integration tests pass -- [ ] Security scans pass (with documented exceptions) -- [ ] No breaking changes -- [ ] Docker image builds successfully - ---- - -## Timeline - -| Phase | Tasks | Duration | Dependencies | -|-------|-------|----------|--------------| -| Phase 1 | Coverage Remediation + Bug Fix | 2-3 hours | None | -| Phase 2 | Vulnerability Documentation | 1-2 hours | None (can parallelize) | -| Phase 3 | Validation & Testing | 1-2 hours | Phase 1 & 2 complete | - -**Total Estimated Time**: 4-7 hours - -**Breakdown**: -- Task 1.1 (Encryption Handler Tests): 1.5-2 hours -- Task 1.2 (Import Handler Bug Fix + Test): 0.5-1 hour -- Task 2.1 (Alpine CVE Documentation): 0.5-1 hour -- Task 2.2 (CHANGELOG Update): 0.5 hour -- Phase 3 (Full Validation): 1-2 hours - ---- - -## Risk Assessment - -### Low Risk ✅ -- Adding test coverage (isolated changes) -- Documenting Alpine CVEs (no code changes) -- Bug fix on line 667 (simple function name correction) - -### Medium Risk ⚠️ -- Accepting Alpine vulnerabilities (requires security approval) -- Test quality (must actually verify error handling, not just coverage) - -### High Risk ❌ -- None identified - -### Mitigation Summary -- All changes tested in isolation -- Full regression test suite required -- Documentation of all decisions -- Peer review before merge -- Security team sign-off on vulnerability acceptance - -**Overall Risk Level**: LOW-MEDIUM - ---- - -## Next Steps - -1. **Immediate**: Execute Phase 1 (Coverage Remediation) -2. **Parallel**: Execute Phase 2 (Vulnerability Remediation) -3. **Sequential**: Execute Phase 3 (Validation) -4. **Final**: Submit for review and merge - ---- - -## References - -- [Codecov Report](https://github.com/Wikid82/Charon/pull/461#issuecomment-3719387466) -- [Supply Chain Scan](https://github.com/Wikid82/Charon/pull/461#issuecomment-3746737390) -- [Testing Instructions](/.github/instructions/testing.instructions.md) -- [Security Instructions](/.github/instructions/security-and-owasp.instructions.md) -- [Supply Chain Documentation](/docs/security/supply-chain-no-cache-solution.md) +UI work in this plan is built with accessibility in mind, but likely still requires manual review and testing (e.g., with Accessibility Insights) as changes land. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 98d5d013..2f24e4d4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -167,7 +167,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -526,7 +525,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -573,7 +571,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3284,7 +3281,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3361,7 +3359,6 @@ "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3372,7 +3369,6 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3383,7 +3379,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3423,7 +3418,6 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -3802,7 +3796,6 @@ "integrity": "sha512-hRDjg6dlDz7JlZAvjbiCdAJ3SDG+NH8tjZe21vjxfvT2ssYAn72SRXMge3dKKABm3bIJ3C+3wdunIdur8PHEAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.17", "fflate": "^0.8.2", @@ -3838,7 +3831,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4070,7 +4062,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4277,8 +4268,7 @@ "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "peer": true + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" }, "node_modules/data-urls": { "version": "6.0.0", @@ -4367,7 +4357,8 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -4531,7 +4522,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5255,7 +5245,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -5449,7 +5438,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -5896,6 +5884,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -6309,7 +6298,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -6339,6 +6327,7 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -6353,6 +6342,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -6362,6 +6352,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -6409,7 +6400,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6419,7 +6409,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6491,7 +6480,8 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "dev": true, + "peer": true }, "node_modules/react-refresh": { "version": "0.18.0", @@ -7041,7 +7031,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7179,7 +7168,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -7255,7 +7243,6 @@ "integrity": "sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.17", "@vitest/mocker": "4.0.17", @@ -7490,7 +7477,6 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/api/dnsProviders.ts b/frontend/src/api/dnsProviders.ts index 186c5ceb..84141941 100644 --- a/frontend/src/api/dnsProviders.ts +++ b/frontend/src/api/dnsProviders.ts @@ -56,24 +56,29 @@ export interface DNSTestResult { propagation_time_ms?: number } +/** Field definition for DNS provider credentials */ +export interface DNSProviderField { + name: string + label: string + type: 'text' | 'password' | 'textarea' | 'select' + required: boolean + default?: string + hint?: string + placeholder?: string + options?: Array<{ + value: string + label: string + }> +} + /** DNS provider type information with field definitions */ export interface DNSProviderTypeInfo { type: DNSProviderType name: string - fields: Array<{ - name: string - label: string - type: 'text' | 'password' | 'textarea' | 'select' - required: boolean - default?: string - hint?: string - placeholder?: string - options?: Array<{ - value: string - label: string - }> - }> - documentation_url: string + description?: string + documentation_url?: string + is_built_in?: boolean + fields: DNSProviderField[] } /** Response for list endpoint */ diff --git a/frontend/src/hooks/useDNSProviders.ts b/frontend/src/hooks/useDNSProviders.ts index 9b3077ac..0a1f25f1 100644 --- a/frontend/src/hooks/useDNSProviders.ts +++ b/frontend/src/hooks/useDNSProviders.ts @@ -11,6 +11,7 @@ import { type DNSProvider, type DNSProviderRequest, type DNSProviderTypeInfo, + type DNSProviderField, type DNSTestResult, } from '../api/dnsProviders' @@ -111,5 +112,6 @@ export type { DNSProvider, DNSProviderRequest, DNSProviderTypeInfo, + DNSProviderField, DNSTestResult, }