chore: GORM remediation
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -301,3 +301,6 @@ docs/plans/supply_chain_security_implementation.md.backup
|
||||
/playwright/.cache/
|
||||
/playwright/.auth/
|
||||
test-data/**
|
||||
|
||||
# GORM Security Scanner Reports
|
||||
docs/reports/gorm-scan-*.txt
|
||||
|
||||
@@ -34,10 +34,8 @@ func (h *DNSProviderHandler) List(c *gin.Context) {
|
||||
// Convert to response format with has_credentials indicator
|
||||
responses := make([]services.DNSProviderResponse, len(providers))
|
||||
for i, p := range providers {
|
||||
responses[i] = services.DNSProviderResponse{
|
||||
DNSProvider: p,
|
||||
HasCredentials: p.CredentialsEncrypted != "",
|
||||
}
|
||||
pCopy := p // Create a copy to take address of
|
||||
responses[i] = services.NewDNSProviderResponse(&pCopy)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -65,10 +63,7 @@ func (h *DNSProviderHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response := services.DNSProviderResponse{
|
||||
DNSProvider: *provider,
|
||||
HasCredentials: provider.CredentialsEncrypted != "",
|
||||
}
|
||||
response := services.NewDNSProviderResponse(provider)
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
@@ -101,10 +96,7 @@ func (h *DNSProviderHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response := services.DNSProviderResponse{
|
||||
DNSProvider: *provider,
|
||||
HasCredentials: provider.CredentialsEncrypted != "",
|
||||
}
|
||||
response := services.NewDNSProviderResponse(provider)
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
@@ -144,10 +136,7 @@ func (h *DNSProviderHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
response := services.DNSProviderResponse{
|
||||
DNSProvider: *provider,
|
||||
HasCredentials: provider.CredentialsEncrypted != "",
|
||||
}
|
||||
response := services.NewDNSProviderResponse(provider)
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ func TestDNSProviderHandler_Get(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint(1), response.ID)
|
||||
assert.Equal(t, "uuid-1", response.UUID)
|
||||
assert.Equal(t, "Test Provider", response.Name)
|
||||
assert.True(t, response.HasCredentials)
|
||||
|
||||
@@ -282,7 +282,7 @@ func TestDNSProviderHandler_Create(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, uint(1), response.ID)
|
||||
assert.Equal(t, "uuid-1", response.UUID)
|
||||
assert.Equal(t, "Test Provider", response.Name)
|
||||
assert.True(t, response.HasCredentials)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -27,9 +28,82 @@ type ProxyHostWarning struct {
|
||||
}
|
||||
|
||||
// ProxyHostResponse wraps a proxy host with optional advisory warnings.
|
||||
// Uses explicit fields to avoid exposing internal database IDs.
|
||||
type ProxyHostResponse struct {
|
||||
models.ProxyHost
|
||||
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
DomainNames string `json:"domain_names"`
|
||||
ForwardScheme string `json:"forward_scheme"`
|
||||
ForwardHost string `json:"forward_host"`
|
||||
ForwardPort int `json:"forward_port"`
|
||||
SSLForced bool `json:"ssl_forced"`
|
||||
HTTP2Support bool `json:"http2_support"`
|
||||
HSTSEnabled bool `json:"hsts_enabled"`
|
||||
HSTSSubdomains bool `json:"hsts_subdomains"`
|
||||
BlockExploits bool `json:"block_exploits"`
|
||||
WebsocketSupport bool `json:"websocket_support"`
|
||||
Application string `json:"application"`
|
||||
Enabled bool `json:"enabled"`
|
||||
CertificateID *uint `json:"certificate_id"`
|
||||
Certificate *models.SSLCertificate `json:"certificate,omitempty"`
|
||||
AccessListID *uint `json:"access_list_id"`
|
||||
AccessList *models.AccessList `json:"access_list,omitempty"`
|
||||
Locations []models.Location `json:"locations"`
|
||||
AdvancedConfig string `json:"advanced_config"`
|
||||
AdvancedConfigBackup string `json:"advanced_config_backup"`
|
||||
ForwardAuthEnabled bool `json:"forward_auth_enabled"`
|
||||
WAFDisabled bool `json:"waf_disabled"`
|
||||
SecurityHeaderProfileID *uint `json:"security_header_profile_id"`
|
||||
SecurityHeaderProfile *models.SecurityHeaderProfile `json:"security_header_profile,omitempty"`
|
||||
SecurityHeadersEnabled bool `json:"security_headers_enabled"`
|
||||
SecurityHeadersCustom string `json:"security_headers_custom"`
|
||||
EnableStandardHeaders *bool `json:"enable_standard_headers,omitempty"`
|
||||
DNSProviderID *uint `json:"dns_provider_id,omitempty"`
|
||||
DNSProvider *models.DNSProvider `json:"dns_provider,omitempty"`
|
||||
UseDNSChallenge bool `json:"use_dns_challenge"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
// NewProxyHostResponse creates a ProxyHostResponse from a ProxyHost model.
|
||||
func NewProxyHostResponse(host *models.ProxyHost, warnings []ProxyHostWarning) ProxyHostResponse {
|
||||
return ProxyHostResponse{
|
||||
UUID: host.UUID,
|
||||
Name: host.Name,
|
||||
DomainNames: host.DomainNames,
|
||||
ForwardScheme: host.ForwardScheme,
|
||||
ForwardHost: host.ForwardHost,
|
||||
ForwardPort: host.ForwardPort,
|
||||
SSLForced: host.SSLForced,
|
||||
HTTP2Support: host.HTTP2Support,
|
||||
HSTSEnabled: host.HSTSEnabled,
|
||||
HSTSSubdomains: host.HSTSSubdomains,
|
||||
BlockExploits: host.BlockExploits,
|
||||
WebsocketSupport: host.WebsocketSupport,
|
||||
Application: host.Application,
|
||||
Enabled: host.Enabled,
|
||||
CertificateID: host.CertificateID,
|
||||
Certificate: host.Certificate,
|
||||
AccessListID: host.AccessListID,
|
||||
AccessList: host.AccessList,
|
||||
Locations: host.Locations,
|
||||
AdvancedConfig: host.AdvancedConfig,
|
||||
AdvancedConfigBackup: host.AdvancedConfigBackup,
|
||||
ForwardAuthEnabled: host.ForwardAuthEnabled,
|
||||
WAFDisabled: host.WAFDisabled,
|
||||
SecurityHeaderProfileID: host.SecurityHeaderProfileID,
|
||||
SecurityHeaderProfile: host.SecurityHeaderProfile,
|
||||
SecurityHeadersEnabled: host.SecurityHeadersEnabled,
|
||||
SecurityHeadersCustom: host.SecurityHeadersCustom,
|
||||
EnableStandardHeaders: host.EnableStandardHeaders,
|
||||
DNSProviderID: host.DNSProviderID,
|
||||
DNSProvider: host.DNSProvider,
|
||||
UseDNSChallenge: host.UseDNSChallenge,
|
||||
CreatedAt: host.CreatedAt,
|
||||
UpdatedAt: host.UpdatedAt,
|
||||
Warnings: warnings,
|
||||
}
|
||||
}
|
||||
|
||||
// generateForwardHostWarnings checks the forward_host value and returns advisory warnings.
|
||||
@@ -176,10 +250,7 @@ func (h *ProxyHostHandler) Create(c *gin.Context) {
|
||||
|
||||
// Return response with warnings if any
|
||||
if len(warnings) > 0 {
|
||||
c.JSON(http.StatusCreated, ProxyHostResponse{
|
||||
ProxyHost: host,
|
||||
Warnings: warnings,
|
||||
})
|
||||
c.JSON(http.StatusCreated, NewProxyHostResponse(&host, warnings))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -448,10 +519,7 @@ func (h *ProxyHostHandler) Update(c *gin.Context) {
|
||||
|
||||
// Return response with warnings if any
|
||||
if len(warnings) > 0 {
|
||||
c.JSON(http.StatusOK, ProxyHostResponse{
|
||||
ProxyHost: *host,
|
||||
Warnings: warnings,
|
||||
})
|
||||
c.JSON(http.StatusOK, NewProxyHostResponse(host, warnings))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// AccessList defines IP-based or auth-based access control rules
|
||||
// that can be applied to proxy hosts.
|
||||
type AccessList struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Description string `json:"description"`
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
|
||||
// CaddyConfig stores an audit trail of Caddy configuration changes.
|
||||
type CaddyConfig struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ConfigHash string `json:"config_hash" gorm:"index"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
ConfigHash string `json:"-" gorm:"index"`
|
||||
AppliedAt time.Time `json:"applied_at"`
|
||||
Success bool `json:"success"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
|
||||
@@ -4,7 +4,7 @@ import "time"
|
||||
|
||||
// CrowdsecConsoleEnrollment stores enrollment status and secrets for console registration.
|
||||
type CrowdsecConsoleEnrollment struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Status string `json:"status" gorm:"index"`
|
||||
Tenant string `json:"tenant"`
|
||||
|
||||
@@ -4,7 +4,7 @@ import "time"
|
||||
|
||||
// CrowdsecPresetEvent captures audit trail for preset pull/apply events.
|
||||
type CrowdsecPresetEvent struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
ID uint `gorm:"primarykey" json:"-"`
|
||||
Slug string `json:"slug"`
|
||||
Action string `json:"action"`
|
||||
Status string `json:"status"`
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
// DNSProvider represents a DNS provider configuration for ACME DNS-01 challenges.
|
||||
// Credentials are stored encrypted at rest using AES-256-GCM.
|
||||
type DNSProvider struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;size:36"`
|
||||
Name string `json:"name" gorm:"index;not null;size:255"`
|
||||
ProviderType string `json:"provider_type" gorm:"index;not null;size:50"`
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
// DNSProviderCredential represents a zone-specific credential set for a DNS provider.
|
||||
// This allows different credentials to be used for different domains/zones within the same provider.
|
||||
type DNSProviderCredential struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;size:36"`
|
||||
DNSProviderID uint `json:"dns_provider_id" gorm:"index;not null"`
|
||||
DNSProvider *DNSProvider `json:"dns_provider,omitempty" gorm:"foreignKey:DNSProviderID"`
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
type Domain struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
||||
Name string `json:"name" gorm:"uniqueIndex;not null"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// EmergencyToken stores metadata for database-backed emergency access tokens.
|
||||
// Tokens are stored as bcrypt hashes for security.
|
||||
type EmergencyToken struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
TokenHash string `json:"-" gorm:"type:text;not null"` // bcrypt hash, never exposed in JSON
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt *time.Time `json:"expires_at"` // NULL = never expires
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// ImportSession tracks Caddyfile import operations with pending state
|
||||
// until user reviews and confirms via UI.
|
||||
type ImportSession struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
SourceFile string `json:"source_file"` // Path to original Caddyfile
|
||||
Status string `json:"status" gorm:"default:'pending'"` // "pending", "reviewing", "committed", "rejected", "failed"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// Location represents a custom path-based proxy configuration within a ProxyHost.
|
||||
type Location struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
||||
ProxyHostID uint `json:"proxy_host_id" gorm:"not null;index"`
|
||||
Path string `json:"path" gorm:"not null;index"` // e.g., /api, /admin
|
||||
|
||||
@@ -39,8 +39,8 @@ type ManualChallenge struct {
|
||||
// Example: "_acme-challenge.example.com"
|
||||
FQDN string `json:"fqdn" gorm:"index;not null;size:255"`
|
||||
|
||||
// Token is the ACME challenge token (for identification).
|
||||
Token string `json:"token" gorm:"size:255"`
|
||||
// Token is the ACME challenge token (for identification), never exposed in JSON.
|
||||
Token string `json:"-" gorm:"size:255"`
|
||||
|
||||
// Value is the TXT record value that must be created.
|
||||
Value string `json:"value" gorm:"not null;size:255"`
|
||||
|
||||
@@ -5,7 +5,7 @@ import "time"
|
||||
// Plugin represents an installed DNS provider plugin.
|
||||
// This tracks both external .so plugins and their load status.
|
||||
type Plugin struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;size:36"`
|
||||
Name string `json:"name" gorm:"not null;size:255"`
|
||||
Type string `json:"type" gorm:"uniqueIndex;not null;size:100"`
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// ProxyHost represents a reverse proxy configuration.
|
||||
type ProxyHost struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
DomainNames string `json:"domain_names" gorm:"not null;index"` // Comma-separated list
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// RemoteServer represents a known backend server that can be selected
|
||||
// when creating proxy hosts, eliminating manual IP/port entry.
|
||||
type RemoteServer struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Provider string `json:"provider" gorm:"index"` // e.g., "docker", "vm", "cloud", "manual"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// SecurityAudit records admin actions or important changes related to security.
|
||||
type SecurityAudit struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Actor string `json:"actor" gorm:"index"`
|
||||
Action string `json:"action"`
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// SecurityConfig represents global Cerberus/CrowdSec/WAF/RateLimit settings
|
||||
// used by the server and propagated into the generated Caddy config.
|
||||
type SecurityConfig struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Enabled bool `json:"enabled" gorm:"index"`
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// SecurityDecision stores a decision/action taken by CrowdSec/WAF/RateLimit or manual
|
||||
// override so it can be audited and surfaced in the UI.
|
||||
type SecurityDecision struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Source string `json:"source" gorm:"index"` // e.g., crowdsec, waf, ratelimit, manual
|
||||
Action string `json:"action" gorm:"index"` // allow, block, challenge
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// SecurityHeaderProfile stores reusable security header configurations.
|
||||
// Users can create profiles and assign them to proxy hosts.
|
||||
type SecurityHeaderProfile struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
||||
Name string `json:"name" gorm:"index;not null"`
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
// SecurityRuleSet stores metadata about WAF/CrowdSec rule sets that the server can download and apply.
|
||||
type SecurityRuleSet struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
SourceURL string `json:"source_url" gorm:"type:text"`
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// Setting stores global application configuration as key-value pairs.
|
||||
// Used for system-wide preferences, feature flags, and runtime config.
|
||||
type Setting struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
Key string `json:"key" gorm:"uniqueIndex"`
|
||||
Value string `json:"value" gorm:"type:text"`
|
||||
Type string `json:"type" gorm:"index"` // "string", "int", "bool", "json"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
// SSLCertificate represents TLS certificates managed by Charon.
|
||||
// Can be Let's Encrypt auto-generated or custom uploaded certs.
|
||||
type SSLCertificate struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name" gorm:"index"`
|
||||
Provider string `json:"provider" gorm:"index"` // "letsencrypt", "custom", "self-signed"
|
||||
|
||||
@@ -36,7 +36,7 @@ type UptimeMonitor struct {
|
||||
}
|
||||
|
||||
type UptimeHeartbeat struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ID uint `gorm:"primaryKey" json:"-"`
|
||||
MonitorID string `json:"monitor_id" gorm:"index"`
|
||||
Status string `json:"status"` // up, down
|
||||
Latency int64 `json:"latency"`
|
||||
|
||||
@@ -20,10 +20,10 @@ const (
|
||||
// User represents authenticated users with role-based access control.
|
||||
// Supports local auth, SSO integration, and invite-based onboarding.
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Email string `json:"email" gorm:"uniqueIndex"`
|
||||
APIKey string `json:"api_key" gorm:"uniqueIndex"` // For external API access
|
||||
APIKey string `json:"-" gorm:"uniqueIndex"` // For external API access, never exposed in JSON
|
||||
PasswordHash string `json:"-"` // Never serialize password hash
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role" gorm:"default:'user'"` // "admin", "user", "viewer"
|
||||
|
||||
@@ -52,9 +52,46 @@ type UpdateDNSProviderRequest struct {
|
||||
}
|
||||
|
||||
// DNSProviderResponse represents the API response for a DNS provider.
|
||||
// Uses explicit fields to avoid exposing internal database IDs.
|
||||
type DNSProviderResponse struct {
|
||||
models.DNSProvider
|
||||
HasCredentials bool `json:"has_credentials"`
|
||||
UUID string `json:"uuid"`
|
||||
Name string `json:"name"`
|
||||
ProviderType string `json:"provider_type"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
UseMultiCredentials bool `json:"use_multi_credentials"`
|
||||
KeyVersion int `json:"key_version"`
|
||||
PropagationTimeout int `json:"propagation_timeout"`
|
||||
PollingInterval int `json:"polling_interval"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
HasCredentials bool `json:"has_credentials"`
|
||||
}
|
||||
|
||||
// NewDNSProviderResponse creates a DNSProviderResponse from a DNSProvider model.
|
||||
func NewDNSProviderResponse(provider *models.DNSProvider) DNSProviderResponse {
|
||||
return DNSProviderResponse{
|
||||
UUID: provider.UUID,
|
||||
Name: provider.Name,
|
||||
ProviderType: provider.ProviderType,
|
||||
Enabled: provider.Enabled,
|
||||
IsDefault: provider.IsDefault,
|
||||
UseMultiCredentials: provider.UseMultiCredentials,
|
||||
KeyVersion: provider.KeyVersion,
|
||||
PropagationTimeout: provider.PropagationTimeout,
|
||||
PollingInterval: provider.PollingInterval,
|
||||
LastUsedAt: provider.LastUsedAt,
|
||||
SuccessCount: provider.SuccessCount,
|
||||
FailureCount: provider.FailureCount,
|
||||
LastError: provider.LastError,
|
||||
CreatedAt: provider.CreatedAt,
|
||||
UpdatedAt: provider.UpdatedAt,
|
||||
HasCredentials: provider.CredentialsEncrypted != "",
|
||||
}
|
||||
}
|
||||
|
||||
// TestResult represents the result of testing DNS provider credentials.
|
||||
|
||||
486
docs/plans/gorm_security_remediation_plan.md
Normal file
486
docs/plans/gorm_security_remediation_plan.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# GORM Security Issues Remediation Plan
|
||||
|
||||
**Status:** 🟡 **READY TO START**
|
||||
**Created:** 2026-01-28
|
||||
**Scanner Report:** 60 issues detected (28 CRITICAL, 2 HIGH, 33 MEDIUM)
|
||||
**Estimated Total Time:** 8-12 hours
|
||||
**Target Completion:** 2026-01-29
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The GORM Security Scanner detected **60 pre-existing security issues** in the codebase. This plan provides a systematic approach to fix all issues, organized by priority and type.
|
||||
|
||||
**Issue Breakdown:**
|
||||
- 🔴 **28 CRITICAL**: 22 ID leaks + 3 exposed secrets + 2 DTO issues + 1 emergency field
|
||||
- 🔵 **33 MEDIUM**: Missing primary key tags (informational)
|
||||
|
||||
**Approach:**
|
||||
1. Fix all CRITICAL issues (models, secrets, DTOs)
|
||||
2. Optionally address MEDIUM issues (missing tags)
|
||||
3. Verify with scanner
|
||||
4. Run full test suite
|
||||
5. Update CI to blocking mode
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Fix ID Leaks in Models (6-8 hours)
|
||||
|
||||
### Priority: 🔴 CRITICAL
|
||||
### Estimated Time: 6-8 hours (22 models × 15-20 min each)
|
||||
|
||||
**Pattern:**
|
||||
```go
|
||||
// BEFORE (Vulnerable):
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
|
||||
// AFTER (Secure):
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
```
|
||||
|
||||
### Task Checklist
|
||||
|
||||
#### Core Models (6 models)
|
||||
- [ ] **User** (`internal/models/user.go:23`)
|
||||
- [ ] **ProxyHost** (`internal/models/proxy_host.go:9`)
|
||||
- [ ] **Domain** (`internal/models/domain.go:11`)
|
||||
- [ ] **DNSProvider** (`internal/models/dns_provider.go:11`)
|
||||
- [ ] **SSLCertificate** (`internal/models/ssl_certificate.go:10`)
|
||||
- [ ] **AccessList** (`internal/models/access_list.go:10`)
|
||||
|
||||
#### Security Models (5 models)
|
||||
- [ ] **SecurityConfig** (`internal/models/security_config.go:10`)
|
||||
- [ ] **SecurityAudit** (`internal/models/security_audit.go:9`)
|
||||
- [ ] **SecurityDecision** (`internal/models/security_decision.go:10`)
|
||||
- [ ] **SecurityHeaderProfile** (`internal/models/security_header_profile.go:10`)
|
||||
- [ ] **SecurityRuleset** (`internal/models/security_ruleset.go:9`)
|
||||
|
||||
#### Infrastructure Models (5 models)
|
||||
- [ ] **Location** (`internal/models/location.go:9`)
|
||||
- [ ] **Plugin** (`internal/models/plugin.go:8`)
|
||||
- [ ] **RemoteServer** (`internal/models/remote_server.go:10`)
|
||||
- [ ] **ImportSession** (`internal/models/import_session.go:10`)
|
||||
- [ ] **Setting** (`internal/models/setting.go:10`)
|
||||
|
||||
#### Integration Models (3 models)
|
||||
- [ ] **CrowdsecConsoleEnrollment** (`internal/models/crowdsec_console_enrollment.go:7`)
|
||||
- [ ] **CrowdsecPresetEvent** (`internal/models/crowdsec_preset_event.go:7`)
|
||||
- [ ] **CaddyConfig** (`internal/models/caddy_config.go:9`)
|
||||
|
||||
#### Provider & Monitoring Models (3 models)
|
||||
- [ ] **DNSProviderCredential** (`internal/models/dns_provider_credential.go:11`)
|
||||
- [ ] **EmergencyToken** (`internal/models/emergency_token.go:10`)
|
||||
- [ ] **UptimeHeartbeat** (`internal/models/uptime.go:39`)
|
||||
|
||||
### Post-Model-Update Tasks
|
||||
|
||||
After changing each model:
|
||||
|
||||
1. **Update API handlers** that reference `.ID`:
|
||||
```bash
|
||||
# Find usages:
|
||||
grep -r "\.ID" backend/internal/api/handlers/
|
||||
grep -r "\"id\":" backend/internal/api/handlers/
|
||||
```
|
||||
|
||||
2. **Update service layer** queries using `.ID`:
|
||||
```bash
|
||||
# Find usages:
|
||||
grep -r "\.ID" backend/internal/services/
|
||||
```
|
||||
|
||||
3. **Verify UUID field exists and is exposed**:
|
||||
```go
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
```
|
||||
|
||||
4. **Update tests** referencing `.ID`:
|
||||
```bash
|
||||
# Find test failures:
|
||||
go test ./... -run TestModel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Fix Exposed Secrets (30 minutes)
|
||||
|
||||
### Priority: 🔴 CRITICAL
|
||||
### Estimated Time: 30 minutes (3 fields)
|
||||
|
||||
**Pattern:**
|
||||
```go
|
||||
// BEFORE (Vulnerable):
|
||||
APIKey string `json:"api_key"`
|
||||
|
||||
// AFTER (Secure):
|
||||
APIKey string `json:"-"`
|
||||
```
|
||||
|
||||
### Task Checklist
|
||||
|
||||
- [ ] **User.APIKey** - Change to `json:"-"`
|
||||
- Location: `internal/models/user.go`
|
||||
- Verify: API key is never serialized to JSON
|
||||
|
||||
- [ ] **ManualChallenge.Token** - Change to `json:"-"`
|
||||
- Location: `internal/services/manual_challenge_service.go:337`
|
||||
- Verify: Challenge token is never exposed
|
||||
|
||||
- [ ] **CaddyConfig.ConfigHash** - Change to `json:"-"`
|
||||
- Location: `internal/models/caddy_config.go`
|
||||
- Verify: Config hash is never exposed
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# Ensure no secrets are exposed:
|
||||
./scripts/scan-gorm-security.sh --report | grep "CRITICAL.*Secret"
|
||||
# Should return: 0 results
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Fix Response DTO Embedding (1-2 hours)
|
||||
|
||||
### Priority: 🟡 HIGH
|
||||
### Estimated Time: 1-2 hours (2 structs)
|
||||
|
||||
**Problem:** Response structs embed models, inheriting exposed IDs
|
||||
|
||||
**Pattern:**
|
||||
```go
|
||||
// BEFORE (Inherits ID exposure):
|
||||
type ProxyHostResponse struct {
|
||||
models.ProxyHost // Embeds entire model
|
||||
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
// AFTER (Explicit fields):
|
||||
type ProxyHostResponse struct {
|
||||
UUID string `json:"uuid"`
|
||||
DomainNames []string `json:"domain_names"`
|
||||
ForwardHost string `json:"forward_host"`
|
||||
// ... other fields explicitly defined
|
||||
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### Task Checklist
|
||||
|
||||
- [ ] **ProxyHostResponse** (`internal/api/handlers/proxy_host_handler.go:31`)
|
||||
- [ ] Replace `models.ProxyHost` embedding with explicit fields
|
||||
- [ ] Include `UUID string json:"uuid"` (expose external ID)
|
||||
- [ ] Exclude `ID uint` (hide internal ID)
|
||||
- [ ] Update all handler functions creating ProxyHostResponse
|
||||
- [ ] Update tests
|
||||
|
||||
- [ ] **DNSProviderResponse** (`internal/services/dns_provider_service.go:56`)
|
||||
- [ ] Replace `models.DNSProvider` embedding with explicit fields
|
||||
- [ ] Include `UUID string json:"uuid"` (expose external ID)
|
||||
- [ ] Exclude `ID uint` (hide internal ID)
|
||||
- [ ] Keep `HasCredentials bool json:"has_credentials"`
|
||||
- [ ] Update all service functions creating DNSProviderResponse
|
||||
- [ ] Update tests
|
||||
|
||||
### Post-DTO-Update Tasks
|
||||
|
||||
1. **Update handler logic**:
|
||||
```go
|
||||
// Map model to response:
|
||||
response := ProxyHostResponse{
|
||||
UUID: model.UUID,
|
||||
DomainNames: model.DomainNames,
|
||||
// ... explicit field mapping
|
||||
}
|
||||
```
|
||||
|
||||
2. **Frontend coordination** (if needed):
|
||||
- Frontend likely uses `uuid` already, not `id`
|
||||
- Verify API client expectations
|
||||
- Update TypeScript types if needed
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Fix Emergency Break Glass Field (15 minutes)
|
||||
|
||||
### Priority: 🔴 CRITICAL
|
||||
### Estimated Time: 15 minutes (1 field)
|
||||
|
||||
**Issue:** `EmergencyConfig.BreakGlassEnabled` exposed
|
||||
|
||||
### Task Checklist
|
||||
|
||||
- [ ] **EmergencyConfig.BreakGlassEnabled**
|
||||
- Location: Find in security models
|
||||
- Change: Add `json:"-"` or verify it's informational only
|
||||
- Verify: Emergency status not leaking sensitive info
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Optional - Fix Missing Primary Key Tags (1 hour)
|
||||
|
||||
### Priority: 🔵 MEDIUM (Informational)
|
||||
### Estimated Time: 1 hour (33 fields)
|
||||
### Decision: **OPTIONAL** - Can defer to separate issue
|
||||
|
||||
**Pattern:**
|
||||
```go
|
||||
// BEFORE (Missing tag):
|
||||
ID uint `json:"-"`
|
||||
|
||||
// AFTER (Explicit tag):
|
||||
ID uint `json:"-" gorm:"primaryKey"`
|
||||
```
|
||||
|
||||
**Impact:** Missing tags don't cause security issues, but explicit tags improve:
|
||||
- Query optimizer performance
|
||||
- Code clarity
|
||||
- GORM auto-migration accuracy
|
||||
|
||||
**Recommendation:** Create separate issue for this backlog item.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Verification & Testing (1-2 hours)
|
||||
|
||||
### Task Checklist
|
||||
|
||||
#### 1. Scanner Verification (5 minutes)
|
||||
- [ ] Run scanner in report mode:
|
||||
```bash
|
||||
./scripts/scan-gorm-security.sh --report
|
||||
```
|
||||
- [ ] Verify: **0 CRITICAL** and **0 HIGH** issues remain
|
||||
- [ ] Optional: Verify **0 MEDIUM** if Phase 5 completed
|
||||
|
||||
#### 2. Backend Tests (30 minutes)
|
||||
- [ ] Run full backend test suite:
|
||||
```bash
|
||||
cd backend && go test ./... -v
|
||||
```
|
||||
- [ ] Verify: All tests pass
|
||||
- [ ] Fix any test failures related to ID → UUID changes
|
||||
|
||||
#### 3. Backend Coverage (15 minutes)
|
||||
- [ ] Run coverage tests:
|
||||
```bash
|
||||
.github/skills/scripts/skill-runner.sh test-backend-coverage
|
||||
```
|
||||
- [ ] Verify: Coverage ≥85%
|
||||
- [ ] Address any coverage drops
|
||||
|
||||
#### 4. Frontend Tests (if API changes) (30 minutes)
|
||||
- [ ] TypeScript type check:
|
||||
```bash
|
||||
cd frontend && npm run type-check
|
||||
```
|
||||
- [ ] Run frontend tests:
|
||||
```bash
|
||||
.github/skills/scripts/skill-runner.sh test-frontend-coverage
|
||||
```
|
||||
- [ ] Verify: All tests pass
|
||||
|
||||
#### 5. Integration Tests (15 minutes)
|
||||
- [ ] Start Docker environment:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
- [ ] Test affected endpoints:
|
||||
- GET /api/proxy-hosts (verify UUID, no ID)
|
||||
- GET /api/dns-providers (verify UUID, no ID)
|
||||
- GET /api/users/me (verify no APIKey exposed)
|
||||
|
||||
#### 6. Final Scanner Check (5 minutes)
|
||||
- [ ] Run scanner in check mode:
|
||||
```bash
|
||||
./scripts/scan-gorm-security.sh --check
|
||||
echo "Exit code: $?" # Must be 0
|
||||
```
|
||||
- [ ] Verify: Exit code **0** (no issues)
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Enable Blocking in Pre-commit (5 minutes)
|
||||
|
||||
### Task Checklist
|
||||
|
||||
- [ ] Update `.pre-commit-config.yaml`:
|
||||
```yaml
|
||||
# CHANGE FROM:
|
||||
stages: [manual]
|
||||
|
||||
# CHANGE TO:
|
||||
stages: [commit]
|
||||
```
|
||||
|
||||
- [ ] Test pre-commit hook:
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
- [ ] Verify: Scanner runs on commit and passes
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Update Documentation (15 minutes)
|
||||
|
||||
### Task Checklist
|
||||
|
||||
- [ ] Update `docs/plans/gorm_security_remediation_plan.md`:
|
||||
- Mark all tasks as complete
|
||||
- Add "COMPLETED" status and completion date
|
||||
|
||||
- [ ] Update `docs/implementation/gorm_security_scanner_complete.md`:
|
||||
- Update "Current Findings" section to show 0 issues
|
||||
- Update pre-commit status to "Blocking"
|
||||
|
||||
- [ ] Update `docs/reports/gorm_scanner_qa_report.md`:
|
||||
- Add remediation completion note
|
||||
|
||||
- [ ] Update `CHANGELOG.md`:
|
||||
- Add entry: "Fixed 60 GORM security issues (ID leaks, exposed secrets)"
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Technical Success ✅
|
||||
- [ ] Scanner reports **0 CRITICAL** and **0 HIGH** issues
|
||||
- [ ] All backend tests pass
|
||||
- [ ] Coverage maintained (≥85%)
|
||||
- [ ] Pre-commit hook in blocking mode
|
||||
- [ ] CI pipeline passes
|
||||
|
||||
### Security Success ✅
|
||||
- [ ] No internal database IDs exposed via JSON
|
||||
- [ ] No API keys/tokens/secrets exposed
|
||||
- [ ] Response DTOs use explicit fields
|
||||
- [ ] UUID fields used for all external references
|
||||
|
||||
### Documentation Success ✅
|
||||
- [ ] Remediation plan marked complete
|
||||
- [ ] Scanner documentation updated
|
||||
- [ ] CHANGELOG updated
|
||||
- [ ] Blocking mode documented
|
||||
|
||||
---
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
### Risk: Breaking API Changes
|
||||
|
||||
**Mitigation:**
|
||||
- Frontend likely already uses `uuid` field, not `id`
|
||||
- Test all API endpoints after changes
|
||||
- Check frontend API client for `id` references
|
||||
- Coordinate with frontend team if breaking changes needed
|
||||
|
||||
### Risk: Test Failures
|
||||
|
||||
**Mitigation:**
|
||||
- Tests may hardcode `.ID` references
|
||||
- Search and replace `.ID` → `.UUID` in tests
|
||||
- Verify test fixtures use UUIDs
|
||||
- Run tests incrementally after each model update
|
||||
|
||||
### Risk: Handler/Service Breakage
|
||||
|
||||
**Mitigation:**
|
||||
- Use grep to find all `.ID` references before changing models
|
||||
- Update handlers/services at the same time as models
|
||||
- Test each endpoint after updating
|
||||
- Use compiler errors as a guide (`.ID` will fail to serialize)
|
||||
|
||||
---
|
||||
|
||||
## Time Estimates Summary
|
||||
|
||||
| Phase | Priority | Time | Can Defer? |
|
||||
|-------|----------|------|------------|
|
||||
| Phase 1: ID Leaks (22 models) | 🔴 CRITICAL | 6-8 hours | ❌ No |
|
||||
| Phase 2: Secrets (3 fields) | 🔴 CRITICAL | 30 min | ❌ No |
|
||||
| Phase 3: DTO Embedding (2 structs) | 🟡 HIGH | 1-2 hours | ❌ No |
|
||||
| Phase 4: Emergency Field (1 field) | 🔴 CRITICAL | 15 min | ❌ No |
|
||||
| Phase 5: Missing Tags (33 fields) | 🔵 MEDIUM | 1 hour | ✅ Yes |
|
||||
| Phase 6: Verification & Testing | Required | 1-2 hours | ❌ No |
|
||||
| Phase 7: Enable Blocking | Required | 5 min | ❌ No |
|
||||
| Phase 8: Documentation | Required | 15 min | ❌ No |
|
||||
| **TOTAL (Required)** | | **9.5-13 hours** | |
|
||||
| **TOTAL (with optional)** | | **10.5-14 hours** | |
|
||||
|
||||
---
|
||||
|
||||
## Quick Start Guide for Tomorrow
|
||||
|
||||
### Morning Session (4-5 hours)
|
||||
|
||||
1. **Start Scanner** to see baseline:
|
||||
```bash
|
||||
./scripts/scan-gorm-security.sh --report | tee gorm-before.txt
|
||||
```
|
||||
|
||||
2. **Tackle Core Models** (User, ProxyHost, Domain, DNSProvider, SSLCertificate, AccessList):
|
||||
- For each model:
|
||||
- Change `json:"id"` → `json:"-"`
|
||||
- Verify `UUID` field exists
|
||||
- Run: `go test ./internal/models/`
|
||||
- Fix compilation errors
|
||||
- Batch commit: "fix: hide internal IDs in core GORM models"
|
||||
|
||||
3. **Coffee Break** ☕
|
||||
|
||||
### Afternoon Session (4-5 hours)
|
||||
|
||||
4. **Tackle Remaining Models** (Security, Infrastructure, Integration models):
|
||||
- Same pattern as morning
|
||||
- Batch commit: "fix: hide internal IDs in remaining GORM models"
|
||||
|
||||
5. **Fix Exposed Secrets** (Phase 2):
|
||||
- Quick 30-minute task
|
||||
- Commit: "fix: hide API keys and sensitive fields"
|
||||
|
||||
6. **Fix Response DTOs** (Phase 3):
|
||||
- ProxyHostResponse
|
||||
- DNSProviderResponse
|
||||
- Commit: "refactor: use explicit fields in response DTOs"
|
||||
|
||||
7. **Final Verification** (Phase 6):
|
||||
- Run scanner: `./scripts/scan-gorm-security.sh --check`
|
||||
- Run tests: `go test ./...`
|
||||
- Run coverage: `.github/skills/scripts/skill-runner.sh test-backend-coverage`
|
||||
|
||||
8. **Enable & Document** (Phases 7-8):
|
||||
- Update pre-commit config to blocking mode
|
||||
- Update all docs
|
||||
- Final commit: "chore: enable GORM security scanner blocking mode"
|
||||
|
||||
---
|
||||
|
||||
## Post-Remediation
|
||||
|
||||
After all fixes are complete:
|
||||
|
||||
1. **Create PR** with all remediation commits
|
||||
2. **CI will pass** (scanner finds 0 issues)
|
||||
3. **Merge to main**
|
||||
4. **Close chore item** in `docs/plans/chores.md`
|
||||
5. **Celebrate** 🎉 - Charon is now more secure!
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Current Branch:** `feature/beta-release`
|
||||
- **Scanner Location:** `scripts/scan-gorm-security.sh`
|
||||
- **Documentation:** `docs/implementation/gorm_security_scanner_complete.md`
|
||||
- **QA Report:** `docs/reports/gorm_scanner_qa_report.md`
|
||||
|
||||
**Tips:**
|
||||
- Work in small batches (5-6 models at a time)
|
||||
- Test incrementally (don't fix everything before testing)
|
||||
- Use grep to find all `.ID` references before each change
|
||||
- Commit frequently to make rollback easier if needed
|
||||
- Take breaks - this is tedious but important work!
|
||||
|
||||
**Good luck tomorrow! 💪**
|
||||
@@ -331,7 +331,8 @@ detect_missing_primary_key() {
|
||||
report_issue "MEDIUM" "MISSING-PK" "$file" "$line_num" "${struct_name:-Unknown}" \
|
||||
"ID Field Missing Primary Key Tag" \
|
||||
"💡 Fix: Add 'primaryKey' to gorm tag: gorm:\"primaryKey\""
|
||||
done < <(grep -n 'ID.*gorm:' "$file" 2>/dev/null || true)
|
||||
# Only match primary key ID field (not foreign keys like CertificateID, AccessListID, etc.)
|
||||
done < <(grep -n -E '^\s+ID\s+' "$file" 2>/dev/null || true)
|
||||
done < <(find "$SCAN_DIR/internal/models" -name "*.go" -type f 2>/dev/null || true)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user