Files
Charon/backend/internal/services/security_headers_service.go
T
GitHub Actions 8cf762164f feat: implement HTTP Security Headers management (Issue #20)
Add comprehensive security header management system with reusable
profiles, interactive builders, and security scoring.

Features:
- SecurityHeaderProfile model with 11+ header types
- CRUD API with 10 endpoints (/api/v1/security/headers/*)
- Caddy integration for automatic header injection
- 3 built-in presets (Basic, Strict, Paranoid)
- Security score calculator (0-100) with suggestions
- Interactive CSP builder with validation
- Permissions-Policy builder
- Real-time security score preview
- Per-host profile assignment

Headers Supported:
- HSTS with preload support
- Content-Security-Policy with report-only mode
- X-Frame-Options, X-Content-Type-Options
- Referrer-Policy, Permissions-Policy
- Cross-Origin-Opener/Resource/Embedder-Policy
- X-XSS-Protection, Cache-Control security

Implementation:
- Backend: models, handlers, services (85% coverage)
- Frontend: React components, hooks (87.46% coverage)
- Tests: 1,163 total tests passing
- Docs: Comprehensive feature documentation

Closes #20
2025-12-19 18:55:48 +00:00

145 lines
5.6 KiB
Go

package services
import (
"fmt"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
// SecurityHeadersService manages security header profiles
type SecurityHeadersService struct {
db *gorm.DB
}
// NewSecurityHeadersService creates a new security headers service
func NewSecurityHeadersService(db *gorm.DB) *SecurityHeadersService {
return &SecurityHeadersService{db: db}
}
// GetPresets returns the built-in presets
func (s *SecurityHeadersService) GetPresets() []models.SecurityHeaderProfile {
return []models.SecurityHeaderProfile{
{
UUID: "preset-basic",
Name: "Basic Security",
PresetType: "basic",
IsPreset: true,
Description: "Essential security headers for most websites. Safe defaults that won't break functionality.",
HSTSEnabled: true,
HSTSMaxAge: 31536000, // 1 year
HSTSIncludeSubdomains: false,
HSTSPreload: false,
CSPEnabled: false, // CSP can break sites
XFrameOptions: "SAMEORIGIN",
XContentTypeOptions: true,
ReferrerPolicy: "strict-origin-when-cross-origin",
XSSProtection: true,
SecurityScore: 65,
},
{
UUID: "preset-strict",
Name: "Strict Security",
PresetType: "strict",
IsPreset: true,
Description: "Strong security for applications handling sensitive data. May require CSP adjustments.",
HSTSEnabled: true,
HSTSMaxAge: 31536000,
HSTSIncludeSubdomains: true,
HSTSPreload: false,
CSPEnabled: true,
CSPDirectives: `{"default-src":["'self'"],"script-src":["'self'"],"style-src":["'self'","'unsafe-inline'"],"img-src":["'self'","data:","https:"],"font-src":["'self'","data:"],"connect-src":["'self'"],"frame-src":["'none'"],"object-src":["'none'"]}`,
XFrameOptions: "DENY",
XContentTypeOptions: true,
ReferrerPolicy: "strict-origin-when-cross-origin",
PermissionsPolicy: `[{"feature":"camera","allowlist":[]},{"feature":"microphone","allowlist":[]},{"feature":"geolocation","allowlist":[]}]`,
XSSProtection: true,
CrossOriginOpenerPolicy: "same-origin",
CrossOriginResourcePolicy: "same-origin",
SecurityScore: 85,
},
{
UUID: "preset-paranoid",
Name: "Paranoid Security",
PresetType: "paranoid",
IsPreset: true,
Description: "Maximum security for high-risk applications. May break some functionality. Test thoroughly.",
HSTSEnabled: true,
HSTSMaxAge: 63072000, // 2 years
HSTSIncludeSubdomains: true,
HSTSPreload: true,
CSPEnabled: true,
CSPDirectives: `{"default-src":["'none'"],"script-src":["'self'"],"style-src":["'self'"],"img-src":["'self'"],"font-src":["'self'"],"connect-src":["'self'"],"frame-src":["'none'"],"object-src":["'none'"],"base-uri":["'self'"],"form-action":["'self'"],"frame-ancestors":["'none'"]}`,
XFrameOptions: "DENY",
XContentTypeOptions: true,
ReferrerPolicy: "no-referrer",
PermissionsPolicy: `[{"feature":"camera","allowlist":[]},{"feature":"microphone","allowlist":[]},{"feature":"geolocation","allowlist":[]},{"feature":"payment","allowlist":[]},{"feature":"usb","allowlist":[]}]`,
XSSProtection: true,
CrossOriginOpenerPolicy: "same-origin",
CrossOriginResourcePolicy: "same-origin",
CrossOriginEmbedderPolicy: "require-corp",
CacheControlNoStore: true,
SecurityScore: 100,
},
}
}
// EnsurePresetsExist creates default presets if they don't exist
func (s *SecurityHeadersService) EnsurePresetsExist() error {
presets := s.GetPresets()
for _, preset := range presets {
var existing models.SecurityHeaderProfile
err := s.db.Where("uuid = ?", preset.UUID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
// Create preset with a fresh UUID for the ID field
if err := s.db.Create(&preset).Error; err != nil {
return fmt.Errorf("failed to create preset %s: %w", preset.Name, err)
}
} else if err != nil {
return fmt.Errorf("failed to check preset %s: %w", preset.Name, err)
} else {
// Update existing preset to ensure it has latest values
preset.ID = existing.ID // Keep the existing ID
if err := s.db.Save(&preset).Error; err != nil {
return fmt.Errorf("failed to update preset %s: %w", preset.Name, err)
}
}
}
return nil
}
// ApplyPreset creates a new profile based on a preset
func (s *SecurityHeadersService) ApplyPreset(presetType, name string) (*models.SecurityHeaderProfile, error) {
presets := s.GetPresets()
var selectedPreset *models.SecurityHeaderProfile
for i := range presets {
if presets[i].PresetType == presetType {
selectedPreset = &presets[i]
break
}
}
if selectedPreset == nil {
return nil, fmt.Errorf("preset type %s not found", presetType)
}
// Create a copy with custom name and UUID
newProfile := *selectedPreset
newProfile.ID = 0 // Clear ID so GORM creates a new record
newProfile.UUID = uuid.New().String()
newProfile.Name = name
newProfile.IsPreset = false // User-created profiles are not presets
newProfile.PresetType = "" // Clear preset type for custom profiles
if err := s.db.Create(&newProfile).Error; err != nil {
return nil, fmt.Errorf("failed to create profile from preset: %w", err)
}
return &newProfile, nil
}