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
1225 lines
40 KiB
Markdown
1225 lines
40 KiB
Markdown
# HTTP Security Headers Implementation Plan (Issue #20)
|
|
|
|
**Created**: December 18, 2025
|
|
**Status**: Planning Complete - Ready for Implementation
|
|
**Issue**: GitHub Issue #20 - HTTP Security Headers
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
This plan implements automatic security header injection for proxy hosts in Charon. The feature enables users to configure HTTP security headers (HSTS, CSP, X-Frame-Options, etc.) through presets or custom configurations, with a security score calculator to help users understand their security posture.
|
|
|
|
**Key Goals:**
|
|
|
|
- Zero-config secure defaults for novice users
|
|
- Granular control for advanced users
|
|
- Visual security score feedback
|
|
- Per-host and global configuration options
|
|
- CSP builder that doesn't break sites
|
|
|
|
---
|
|
|
|
## Architecture Overview
|
|
|
|
### Data Flow
|
|
|
|
```text
|
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
│ Frontend │────▶│ Backend API │────▶│ Models │────▶│ Caddy │
|
|
│ UI/Config │ │ Handlers │ │ (SQLite) │ │ JSON Config │
|
|
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
│ │ │ │
|
|
│ │ │ │
|
|
┌────▼────┐ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
|
|
│ Presets │ │ CRUD │ │ Profile │ │ Headers │
|
|
│ Selector│ │ Profile │ │ Storage │ │ Handler │
|
|
│ Score │ │ Score │ │ Host FK │ │ Inject │
|
|
└─────────┘ └─────────┘ └─────────┘ └─────────┘
|
|
```
|
|
|
|
### Key Design Decisions
|
|
|
|
1. **Security Header Profiles** - Reusable configurations that can be shared across hosts
|
|
2. **Per-Host Override** - Each proxy host can reference a profile OR have inline settings
|
|
3. **Presets System** - Pre-built profiles (basic, strict, paranoid) for easy setup
|
|
4. **Score Calculator** - Visual feedback showing security level (0-100)
|
|
5. **CSP Builder** - Interactive tool to build Content-Security-Policy without breaking sites
|
|
|
|
---
|
|
|
|
## Phase 1: Backend Models and Migrations
|
|
|
|
### 1.1 New Model: `SecurityHeaderProfile`
|
|
|
|
**File**: `backend/internal/models/security_header_profile.go`
|
|
|
|
```go
|
|
package models
|
|
|
|
import (
|
|
"time"
|
|
)
|
|
|
|
// 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"`
|
|
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
|
Name string `json:"name" gorm:"index;not null"`
|
|
|
|
// HSTS Configuration
|
|
HSTSEnabled bool `json:"hsts_enabled" gorm:"default:true"`
|
|
HSTSMaxAge int `json:"hsts_max_age" gorm:"default:31536000"` // 1 year in seconds
|
|
HSTSIncludeSubdomains bool `json:"hsts_include_subdomains" gorm:"default:true"`
|
|
HSTSPreload bool `json:"hsts_preload" gorm:"default:false"`
|
|
|
|
// Content-Security-Policy
|
|
CSPEnabled bool `json:"csp_enabled" gorm:"default:false"`
|
|
CSPDirectives string `json:"csp_directives" gorm:"type:text"` // JSON object of CSP directives
|
|
CSPReportOnly bool `json:"csp_report_only" gorm:"default:false"`
|
|
CSPReportURI string `json:"csp_report_uri"`
|
|
|
|
// X-Frame-Options
|
|
XFrameOptions string `json:"x_frame_options" gorm:"default:DENY"` // DENY, SAMEORIGIN, or empty
|
|
|
|
// X-Content-Type-Options
|
|
XContentTypeOptions bool `json:"x_content_type_options" gorm:"default:true"` // nosniff
|
|
|
|
// Referrer-Policy
|
|
ReferrerPolicy string `json:"referrer_policy" gorm:"default:strict-origin-when-cross-origin"`
|
|
|
|
// Permissions-Policy (formerly Feature-Policy)
|
|
PermissionsPolicy string `json:"permissions_policy" gorm:"type:text"` // JSON array of policies
|
|
|
|
// Cross-Origin Headers
|
|
CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy" gorm:"default:same-origin"`
|
|
CrossOriginResourcePolicy string `json:"cross_origin_resource_policy" gorm:"default:same-origin"`
|
|
CrossOriginEmbedderPolicy string `json:"cross_origin_embedder_policy"` // require-corp or empty
|
|
|
|
// X-XSS-Protection (legacy but still useful)
|
|
XSSProtection bool `json:"xss_protection" gorm:"default:true"`
|
|
|
|
// Cache-Control for security
|
|
CacheControlNoStore bool `json:"cache_control_no_store" gorm:"default:false"`
|
|
|
|
// Computed Security Score (0-100)
|
|
SecurityScore int `json:"security_score" gorm:"default:0"`
|
|
|
|
// Metadata
|
|
IsPreset bool `json:"is_preset" gorm:"default:false"` // System presets can't be deleted
|
|
PresetType string `json:"preset_type"` // "basic", "strict", "paranoid", or empty for custom
|
|
Description string `json:"description" gorm:"type:text"`
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// CSPDirective represents a single CSP directive for the builder
|
|
type CSPDirective struct {
|
|
Directive string `json:"directive"` // e.g., "default-src", "script-src"
|
|
Values []string `json:"values"` // e.g., ["'self'", "https:"]
|
|
}
|
|
|
|
// PermissionsPolicyItem represents a single Permissions-Policy entry
|
|
type PermissionsPolicyItem struct {
|
|
Feature string `json:"feature"` // e.g., "camera", "microphone"
|
|
Allowlist []string `json:"allowlist"` // e.g., ["self"], ["*"], []
|
|
}
|
|
```
|
|
|
|
### 1.2 Update ProxyHost Model
|
|
|
|
**File**: `backend/internal/models/proxy_host.go`
|
|
|
|
Add the following fields to `ProxyHost`:
|
|
|
|
```go
|
|
// Security Headers Configuration
|
|
// Either reference a profile OR use inline settings
|
|
SecurityHeaderProfileID *uint `json:"security_header_profile_id"`
|
|
SecurityHeaderProfile *SecurityHeaderProfile `json:"security_header_profile" gorm:"foreignKey:SecurityHeaderProfileID"`
|
|
|
|
// Inline security header settings (used when no profile is selected)
|
|
// These override profile settings if both are set
|
|
SecurityHeadersEnabled bool `json:"security_headers_enabled" gorm:"default:true"`
|
|
SecurityHeadersCustom string `json:"security_headers_custom" gorm:"type:text"` // JSON for custom headers
|
|
```
|
|
|
|
### 1.3 Migration Updates
|
|
|
|
**File**: `backend/internal/api/routes/routes.go`
|
|
|
|
Add to `AutoMigrate`:
|
|
|
|
```go
|
|
&models.SecurityHeaderProfile{},
|
|
```
|
|
|
|
### 1.4 Unit Tests
|
|
|
|
**File**: `backend/internal/models/security_header_profile_test.go`
|
|
|
|
Test cases:
|
|
|
|
- Profile creation with default values
|
|
- JSON serialization/deserialization of CSPDirectives
|
|
- JSON serialization/deserialization of PermissionsPolicy
|
|
- UUID generation on creation
|
|
- Security score calculation triggers
|
|
|
|
---
|
|
|
|
## Phase 2: Backend API Handlers
|
|
|
|
### 2.1 Security Headers Handler
|
|
|
|
**File**: `backend/internal/api/handlers/security_headers_handler.go`
|
|
|
|
```go
|
|
package handlers
|
|
|
|
// SecurityHeadersHandler manages security header profiles
|
|
type SecurityHeadersHandler struct {
|
|
db *gorm.DB
|
|
caddyManager *caddy.Manager
|
|
}
|
|
|
|
// NewSecurityHeadersHandler creates a new handler
|
|
func NewSecurityHeadersHandler(db *gorm.DB, caddyManager *caddy.Manager) *SecurityHeadersHandler
|
|
|
|
// RegisterRoutes registers all security headers routes
|
|
func (h *SecurityHeadersHandler) RegisterRoutes(router *gin.RouterGroup)
|
|
|
|
// --- Profile CRUD ---
|
|
|
|
// ListProfiles returns all security header profiles
|
|
// GET /api/v1/security/headers/profiles
|
|
func (h *SecurityHeadersHandler) ListProfiles(c *gin.Context)
|
|
|
|
// GetProfile returns a single profile by ID or UUID
|
|
// GET /api/v1/security/headers/profiles/:id
|
|
func (h *SecurityHeadersHandler) GetProfile(c *gin.Context)
|
|
|
|
// CreateProfile creates a new security header profile
|
|
// POST /api/v1/security/headers/profiles
|
|
func (h *SecurityHeadersHandler) CreateProfile(c *gin.Context)
|
|
|
|
// UpdateProfile updates an existing profile
|
|
// PUT /api/v1/security/headers/profiles/:id
|
|
func (h *SecurityHeadersHandler) UpdateProfile(c *gin.Context)
|
|
|
|
// DeleteProfile deletes a profile (not presets)
|
|
// DELETE /api/v1/security/headers/profiles/:id
|
|
func (h *SecurityHeadersHandler) DeleteProfile(c *gin.Context)
|
|
|
|
// --- Presets ---
|
|
|
|
// GetPresets returns the list of built-in presets
|
|
// GET /api/v1/security/headers/presets
|
|
func (h *SecurityHeadersHandler) GetPresets(c *gin.Context)
|
|
|
|
// ApplyPreset applies a preset to create/update a profile
|
|
// POST /api/v1/security/headers/presets/apply
|
|
func (h *SecurityHeadersHandler) ApplyPreset(c *gin.Context)
|
|
|
|
// --- Security Score ---
|
|
|
|
// CalculateScore calculates security score for given settings
|
|
// POST /api/v1/security/headers/score
|
|
func (h *SecurityHeadersHandler) CalculateScore(c *gin.Context)
|
|
|
|
// --- CSP Builder ---
|
|
|
|
// ValidateCSP validates a CSP string
|
|
// POST /api/v1/security/headers/csp/validate
|
|
func (h *SecurityHeadersHandler) ValidateCSP(c *gin.Context)
|
|
|
|
// BuildCSP builds a CSP string from directives
|
|
// POST /api/v1/security/headers/csp/build
|
|
func (h *SecurityHeadersHandler) BuildCSP(c *gin.Context)
|
|
```
|
|
|
|
### 2.2 API Routes Registration
|
|
|
|
**File**: `backend/internal/api/routes/routes.go`
|
|
|
|
Add to the protected routes section:
|
|
|
|
```go
|
|
// Security Headers
|
|
securityHeadersHandler := handlers.NewSecurityHeadersHandler(db, caddyManager)
|
|
securityHeadersHandler.RegisterRoutes(protected)
|
|
```
|
|
|
|
### 2.3 Request/Response Types
|
|
|
|
```go
|
|
// CreateProfileRequest for creating a new profile
|
|
type CreateProfileRequest struct {
|
|
Name string `json:"name" binding:"required"`
|
|
HSTSEnabled bool `json:"hsts_enabled"`
|
|
HSTSMaxAge int `json:"hsts_max_age"`
|
|
HSTSIncludeSubdomains bool `json:"hsts_include_subdomains"`
|
|
HSTSPreload bool `json:"hsts_preload"`
|
|
CSPEnabled bool `json:"csp_enabled"`
|
|
CSPDirectives string `json:"csp_directives"`
|
|
CSPReportOnly bool `json:"csp_report_only"`
|
|
CSPReportURI string `json:"csp_report_uri"`
|
|
XFrameOptions string `json:"x_frame_options"`
|
|
XContentTypeOptions bool `json:"x_content_type_options"`
|
|
ReferrerPolicy string `json:"referrer_policy"`
|
|
PermissionsPolicy string `json:"permissions_policy"`
|
|
CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy"`
|
|
CrossOriginResourcePolicy string `json:"cross_origin_resource_policy"`
|
|
CrossOriginEmbedderPolicy string `json:"cross_origin_embedder_policy"`
|
|
XSSProtection bool `json:"xss_protection"`
|
|
CacheControlNoStore bool `json:"cache_control_no_store"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
// CalculateScoreRequest for scoring
|
|
type CalculateScoreRequest struct {
|
|
HSTSEnabled bool `json:"hsts_enabled"`
|
|
HSTSMaxAge int `json:"hsts_max_age"`
|
|
HSTSIncludeSubdomains bool `json:"hsts_include_subdomains"`
|
|
HSTSPreload bool `json:"hsts_preload"`
|
|
CSPEnabled bool `json:"csp_enabled"`
|
|
CSPDirectives string `json:"csp_directives"`
|
|
XFrameOptions string `json:"x_frame_options"`
|
|
XContentTypeOptions bool `json:"x_content_type_options"`
|
|
ReferrerPolicy string `json:"referrer_policy"`
|
|
PermissionsPolicy string `json:"permissions_policy"`
|
|
CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy"`
|
|
CrossOriginResourcePolicy string `json:"cross_origin_resource_policy"`
|
|
XSSProtection bool `json:"xss_protection"`
|
|
}
|
|
|
|
// ScoreResponse returns the calculated score
|
|
type ScoreResponse struct {
|
|
Score int `json:"score"`
|
|
MaxScore int `json:"max_score"`
|
|
Breakdown map[string]int `json:"breakdown"`
|
|
Suggestions []string `json:"suggestions"`
|
|
}
|
|
```
|
|
|
|
### 2.4 Unit Tests
|
|
|
|
**File**: `backend/internal/api/handlers/security_headers_handler_test.go`
|
|
|
|
Test cases:
|
|
|
|
- `TestListProfiles` - List all profiles
|
|
- `TestGetProfile` - Get profile by ID
|
|
- `TestGetProfileByUUID` - Get profile by UUID
|
|
- `TestCreateProfile` - Create new profile
|
|
- `TestUpdateProfile` - Update existing profile
|
|
- `TestDeleteProfile` - Delete custom profile
|
|
- `TestDeletePresetFails` - Cannot delete system presets
|
|
- `TestGetPresets` - List all presets
|
|
- `TestApplyPreset` - Apply preset to create profile
|
|
- `TestCalculateScore` - Score calculation
|
|
- `TestValidateCSP` - CSP validation
|
|
- `TestBuildCSP` - CSP building
|
|
|
|
---
|
|
|
|
## Phase 3: Caddy Integration for Header Injection
|
|
|
|
### 3.1 Header Builder Function
|
|
|
|
**File**: `backend/internal/caddy/config.go`
|
|
|
|
Add new function:
|
|
|
|
```go
|
|
// buildSecurityHeadersHandler creates a headers handler for security headers
|
|
// based on the profile configuration or host-level settings
|
|
func buildSecurityHeadersHandler(host *models.ProxyHost, profile *models.SecurityHeaderProfile) (Handler, error) {
|
|
if profile == nil && !host.SecurityHeadersEnabled {
|
|
return nil, nil
|
|
}
|
|
|
|
responseHeaders := make(map[string][]string)
|
|
|
|
// Use profile settings or host defaults
|
|
var cfg *models.SecurityHeaderProfile
|
|
if profile != nil {
|
|
cfg = profile
|
|
} else {
|
|
// Use host's inline settings or defaults
|
|
cfg = getDefaultSecurityHeaderProfile()
|
|
}
|
|
|
|
// HSTS
|
|
if cfg.HSTSEnabled {
|
|
hstsValue := fmt.Sprintf("max-age=%d", cfg.HSTSMaxAge)
|
|
if cfg.HSTSIncludeSubdomains {
|
|
hstsValue += "; includeSubDomains"
|
|
}
|
|
if cfg.HSTSPreload {
|
|
hstsValue += "; preload"
|
|
}
|
|
responseHeaders["Strict-Transport-Security"] = []string{hstsValue}
|
|
}
|
|
|
|
// CSP
|
|
if cfg.CSPEnabled && cfg.CSPDirectives != "" {
|
|
cspHeader := "Content-Security-Policy"
|
|
if cfg.CSPReportOnly {
|
|
cspHeader = "Content-Security-Policy-Report-Only"
|
|
}
|
|
responseHeaders[cspHeader] = []string{buildCSPString(cfg.CSPDirectives)}
|
|
}
|
|
|
|
// X-Frame-Options
|
|
if cfg.XFrameOptions != "" {
|
|
responseHeaders["X-Frame-Options"] = []string{cfg.XFrameOptions}
|
|
}
|
|
|
|
// X-Content-Type-Options
|
|
if cfg.XContentTypeOptions {
|
|
responseHeaders["X-Content-Type-Options"] = []string{"nosniff"}
|
|
}
|
|
|
|
// Referrer-Policy
|
|
if cfg.ReferrerPolicy != "" {
|
|
responseHeaders["Referrer-Policy"] = []string{cfg.ReferrerPolicy}
|
|
}
|
|
|
|
// Permissions-Policy
|
|
if cfg.PermissionsPolicy != "" {
|
|
responseHeaders["Permissions-Policy"] = []string{buildPermissionsPolicyString(cfg.PermissionsPolicy)}
|
|
}
|
|
|
|
// Cross-Origin headers
|
|
if cfg.CrossOriginOpenerPolicy != "" {
|
|
responseHeaders["Cross-Origin-Opener-Policy"] = []string{cfg.CrossOriginOpenerPolicy}
|
|
}
|
|
if cfg.CrossOriginResourcePolicy != "" {
|
|
responseHeaders["Cross-Origin-Resource-Policy"] = []string{cfg.CrossOriginResourcePolicy}
|
|
}
|
|
if cfg.CrossOriginEmbedderPolicy != "" {
|
|
responseHeaders["Cross-Origin-Embedder-Policy"] = []string{cfg.CrossOriginEmbedderPolicy}
|
|
}
|
|
|
|
// X-XSS-Protection
|
|
if cfg.XSSProtection {
|
|
responseHeaders["X-XSS-Protection"] = []string{"1; mode=block"}
|
|
}
|
|
|
|
// Cache-Control
|
|
if cfg.CacheControlNoStore {
|
|
responseHeaders["Cache-Control"] = []string{"no-store"}
|
|
}
|
|
|
|
if len(responseHeaders) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return Handler{
|
|
"handler": "headers",
|
|
"response": map[string]interface{}{
|
|
"set": responseHeaders,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// buildCSPString converts JSON CSP directives to a CSP string
|
|
func buildCSPString(directivesJSON string) string
|
|
|
|
// buildPermissionsPolicyString converts JSON permissions to policy string
|
|
func buildPermissionsPolicyString(permissionsJSON string) string
|
|
|
|
// getDefaultSecurityHeaderProfile returns secure defaults
|
|
func getDefaultSecurityHeaderProfile() *models.SecurityHeaderProfile
|
|
```
|
|
|
|
### 3.2 Update GenerateConfig
|
|
|
|
**File**: `backend/internal/caddy/config.go`
|
|
|
|
In the `GenerateConfig` function, add security headers handler to the handler chain:
|
|
|
|
```go
|
|
// After building securityHandlers and before the main reverse proxy handler:
|
|
|
|
// Add Security Headers handler
|
|
if secHeadersHandler, err := buildSecurityHeadersHandler(&host, host.SecurityHeaderProfile); err == nil && secHeadersHandler != nil {
|
|
handlers = append(handlers, secHeadersHandler)
|
|
}
|
|
```
|
|
|
|
### 3.3 Update Manager to Load Profiles
|
|
|
|
**File**: `backend/internal/caddy/manager.go`
|
|
|
|
Update `ApplyConfig` to preload security header profiles:
|
|
|
|
```go
|
|
// Preload security header profiles for hosts
|
|
var profiles []models.SecurityHeaderProfile
|
|
if err := m.db.Find(&profiles).Error; err != nil {
|
|
return fmt.Errorf("failed to load security header profiles: %w", err)
|
|
}
|
|
profileMap := make(map[uint]*models.SecurityHeaderProfile)
|
|
for i := range profiles {
|
|
profileMap[profiles[i].ID] = &profiles[i]
|
|
}
|
|
|
|
// Attach profiles to hosts
|
|
for i := range hosts {
|
|
if hosts[i].SecurityHeaderProfileID != nil {
|
|
hosts[i].SecurityHeaderProfile = profileMap[*hosts[i].SecurityHeaderProfileID]
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.4 Unit Tests
|
|
|
|
**File**: `backend/internal/caddy/config_security_headers_test.go`
|
|
|
|
Test cases:
|
|
|
|
- `TestBuildSecurityHeadersHandler_AllEnabled` - All headers enabled
|
|
- `TestBuildSecurityHeadersHandler_HSTSOnly` - Only HSTS
|
|
- `TestBuildSecurityHeadersHandler_CSPOnly` - Only CSP
|
|
- `TestBuildSecurityHeadersHandler_CSPReportOnly` - CSP in report-only mode
|
|
- `TestBuildSecurityHeadersHandler_NoProfile` - Default behavior
|
|
- `TestBuildSecurityHeadersHandler_Disabled` - Headers disabled
|
|
- `TestBuildCSPString` - CSP JSON to string conversion
|
|
- `TestBuildPermissionsPolicyString` - Permissions JSON to string
|
|
- `TestGenerateConfig_WithSecurityHeaders` - Full integration
|
|
|
|
---
|
|
|
|
## Phase 4: Frontend UI Components
|
|
|
|
### 4.1 API Client
|
|
|
|
**File**: `frontend/src/api/securityHeaders.ts`
|
|
|
|
```typescript
|
|
import client from './client'
|
|
|
|
// Types
|
|
export interface SecurityHeaderProfile {
|
|
id: number
|
|
uuid: string
|
|
name: string
|
|
hsts_enabled: boolean
|
|
hsts_max_age: number
|
|
hsts_include_subdomains: boolean
|
|
hsts_preload: boolean
|
|
csp_enabled: boolean
|
|
csp_directives: string
|
|
csp_report_only: boolean
|
|
csp_report_uri: string
|
|
x_frame_options: string
|
|
x_content_type_options: boolean
|
|
referrer_policy: string
|
|
permissions_policy: string
|
|
cross_origin_opener_policy: string
|
|
cross_origin_resource_policy: string
|
|
cross_origin_embedder_policy: string
|
|
xss_protection: boolean
|
|
cache_control_no_store: boolean
|
|
security_score: number
|
|
is_preset: boolean
|
|
preset_type: string
|
|
description: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
export interface SecurityHeaderPreset {
|
|
type: 'basic' | 'strict' | 'paranoid'
|
|
name: string
|
|
description: string
|
|
score: number
|
|
config: Partial<SecurityHeaderProfile>
|
|
}
|
|
|
|
export interface ScoreBreakdown {
|
|
score: number
|
|
max_score: number
|
|
breakdown: Record<string, number>
|
|
suggestions: string[]
|
|
}
|
|
|
|
export interface CSPDirective {
|
|
directive: string
|
|
values: string[]
|
|
}
|
|
|
|
// API Functions
|
|
export const listProfiles = async (): Promise<{ profiles: SecurityHeaderProfile[] }> => {
|
|
const response = await client.get('/security/headers/profiles')
|
|
return response.data
|
|
}
|
|
|
|
export const getProfile = async (id: number | string): Promise<{ profile: SecurityHeaderProfile }> => {
|
|
const response = await client.get(`/security/headers/profiles/${id}`)
|
|
return response.data
|
|
}
|
|
|
|
export const createProfile = async (data: Partial<SecurityHeaderProfile>): Promise<{ profile: SecurityHeaderProfile }> => {
|
|
const response = await client.post('/security/headers/profiles', data)
|
|
return response.data
|
|
}
|
|
|
|
export const updateProfile = async (id: number, data: Partial<SecurityHeaderProfile>): Promise<{ profile: SecurityHeaderProfile }> => {
|
|
const response = await client.put(`/security/headers/profiles/${id}`, data)
|
|
return response.data
|
|
}
|
|
|
|
export const deleteProfile = async (id: number): Promise<{ deleted: boolean }> => {
|
|
const response = await client.delete(`/security/headers/profiles/${id}`)
|
|
return response.data
|
|
}
|
|
|
|
export const getPresets = async (): Promise<{ presets: SecurityHeaderPreset[] }> => {
|
|
const response = await client.get('/security/headers/presets')
|
|
return response.data
|
|
}
|
|
|
|
export const applyPreset = async (presetType: string, name: string): Promise<{ profile: SecurityHeaderProfile }> => {
|
|
const response = await client.post('/security/headers/presets/apply', { preset_type: presetType, name })
|
|
return response.data
|
|
}
|
|
|
|
export const calculateScore = async (config: Partial<SecurityHeaderProfile>): Promise<ScoreBreakdown> => {
|
|
const response = await client.post('/security/headers/score', config)
|
|
return response.data
|
|
}
|
|
|
|
export const validateCSP = async (csp: string): Promise<{ valid: boolean; errors: string[] }> => {
|
|
const response = await client.post('/security/headers/csp/validate', { csp })
|
|
return response.data
|
|
}
|
|
|
|
export const buildCSP = async (directives: CSPDirective[]): Promise<{ csp: string }> => {
|
|
const response = await client.post('/security/headers/csp/build', { directives })
|
|
return response.data
|
|
}
|
|
```
|
|
|
|
### 4.2 React Query Hooks
|
|
|
|
**File**: `frontend/src/hooks/useSecurityHeaders.ts`
|
|
|
|
```typescript
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import * as api from '../api/securityHeaders'
|
|
import toast from 'react-hot-toast'
|
|
|
|
export function useSecurityHeaderProfiles() {
|
|
return useQuery({
|
|
queryKey: ['securityHeaderProfiles'],
|
|
queryFn: api.listProfiles,
|
|
})
|
|
}
|
|
|
|
export function useSecurityHeaderProfile(id: number | string) {
|
|
return useQuery({
|
|
queryKey: ['securityHeaderProfile', id],
|
|
queryFn: () => api.getProfile(id),
|
|
enabled: !!id,
|
|
})
|
|
}
|
|
|
|
export function useCreateSecurityHeaderProfile() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: api.createProfile,
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
|
|
toast.success('Profile created')
|
|
},
|
|
onError: (err: Error) => {
|
|
toast.error(`Failed to create profile: ${err.message}`)
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useUpdateSecurityHeaderProfile() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ id, data }: { id: number; data: Partial<api.SecurityHeaderProfile> }) =>
|
|
api.updateProfile(id, data),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
|
|
toast.success('Profile updated')
|
|
},
|
|
onError: (err: Error) => {
|
|
toast.error(`Failed to update profile: ${err.message}`)
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDeleteSecurityHeaderProfile() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: api.deleteProfile,
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
|
|
toast.success('Profile deleted')
|
|
},
|
|
onError: (err: Error) => {
|
|
toast.error(`Failed to delete profile: ${err.message}`)
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useSecurityHeaderPresets() {
|
|
return useQuery({
|
|
queryKey: ['securityHeaderPresets'],
|
|
queryFn: api.getPresets,
|
|
})
|
|
}
|
|
|
|
export function useApplySecurityHeaderPreset() {
|
|
const qc = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ presetType, name }: { presetType: string; name: string }) =>
|
|
api.applyPreset(presetType, name),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: ['securityHeaderProfiles'] })
|
|
toast.success('Preset applied')
|
|
},
|
|
onError: (err: Error) => {
|
|
toast.error(`Failed to apply preset: ${err.message}`)
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useCalculateSecurityScore() {
|
|
return useMutation({
|
|
mutationFn: api.calculateScore,
|
|
})
|
|
}
|
|
|
|
export function useValidateCSP() {
|
|
return useMutation({
|
|
mutationFn: api.validateCSP,
|
|
})
|
|
}
|
|
|
|
export function useBuildCSP() {
|
|
return useMutation({
|
|
mutationFn: api.buildCSP,
|
|
})
|
|
}
|
|
```
|
|
|
|
### 4.3 Security Headers Page
|
|
|
|
**File**: `frontend/src/pages/SecurityHeaders.tsx`
|
|
|
|
Main page with:
|
|
|
|
- List of custom profiles
|
|
- Preset cards (Basic, Strict, Paranoid)
|
|
- Create/Edit profile modal
|
|
- Security score display
|
|
- Delete confirmation dialog
|
|
|
|
### 4.4 Security Header Profile Form
|
|
|
|
**File**: `frontend/src/components/SecurityHeaderProfileForm.tsx`
|
|
|
|
Form component with:
|
|
|
|
- Name input
|
|
- HSTS section (enable, max-age, subdomains, preload)
|
|
- CSP section with builder
|
|
- X-Frame-Options selector
|
|
- X-Content-Type-Options toggle
|
|
- Referrer-Policy selector
|
|
- Permissions-Policy builder
|
|
- Cross-Origin headers section
|
|
- Live security score preview
|
|
- Save/Cancel buttons
|
|
|
|
### 4.5 CSP Builder Component
|
|
|
|
**File**: `frontend/src/components/CSPBuilder.tsx`
|
|
|
|
Interactive CSP builder:
|
|
|
|
- Directive selector dropdown
|
|
- Value input with suggestions
|
|
- Add/Remove directives
|
|
- Preview CSP string
|
|
- Validate button
|
|
- Common presets (default strict, allow scripts, etc.)
|
|
|
|
### 4.6 Security Score Display
|
|
|
|
**File**: `frontend/src/components/SecurityScoreDisplay.tsx`
|
|
|
|
Visual component showing:
|
|
|
|
- Circular progress indicator (0-100)
|
|
- Color coding (red < 50, yellow 50-75, green > 75)
|
|
- Score breakdown by category
|
|
- Suggestions for improvement
|
|
|
|
### 4.7 Permissions Policy Builder
|
|
|
|
**File**: `frontend/src/components/PermissionsPolicyBuilder.tsx`
|
|
|
|
Interactive builder for:
|
|
|
|
- Feature selector (camera, microphone, geolocation, etc.)
|
|
- Allowlist selector (none, self, all, specific origins)
|
|
- Preview policy string
|
|
|
|
### 4.8 Update ProxyHostForm
|
|
|
|
**File**: `frontend/src/components/ProxyHostForm.tsx`
|
|
|
|
Add to the form:
|
|
|
|
- "Security Headers" section
|
|
- Profile selector dropdown
|
|
- "Create New Profile" button
|
|
- Security score preview for selected profile
|
|
- Quick preset buttons
|
|
|
|
### 4.9 Unit Tests
|
|
|
|
**Files**:
|
|
|
|
- `frontend/src/hooks/__tests__/useSecurityHeaders.test.ts`
|
|
- `frontend/src/components/__tests__/SecurityHeaderProfileForm.test.tsx`
|
|
- `frontend/src/components/__tests__/CSPBuilder.test.tsx`
|
|
- `frontend/src/components/__tests__/SecurityScoreDisplay.test.tsx`
|
|
- `frontend/src/pages/__tests__/SecurityHeaders.test.tsx`
|
|
|
|
---
|
|
|
|
## Phase 5: Presets and Score Calculator
|
|
|
|
### 5.1 Built-in Presets
|
|
|
|
**File**: `backend/internal/services/security_headers_service.go`
|
|
|
|
```go
|
|
package services
|
|
|
|
// SecurityHeadersService manages security header profiles
|
|
type SecurityHeadersService struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// GetPresets returns the built-in presets
|
|
func (s *SecurityHeadersService) GetPresets() []models.SecurityHeaderProfile {
|
|
return []models.SecurityHeaderProfile{
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
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
|
|
```
|
|
|
|
### 5.2 Security Score Calculator
|
|
|
|
**File**: `backend/internal/services/security_score.go`
|
|
|
|
```go
|
|
package services
|
|
|
|
import "strings"
|
|
|
|
// ScoreBreakdown represents the detailed score calculation
|
|
type ScoreBreakdown struct {
|
|
TotalScore int `json:"score"`
|
|
MaxScore int `json:"max_score"`
|
|
Breakdown map[string]int `json:"breakdown"`
|
|
Suggestions []string `json:"suggestions"`
|
|
}
|
|
|
|
// CalculateSecurityScore calculates the security score for a profile
|
|
func CalculateSecurityScore(profile *models.SecurityHeaderProfile) ScoreBreakdown {
|
|
breakdown := make(map[string]int)
|
|
suggestions := []string{}
|
|
maxScore := 100
|
|
|
|
// HSTS (25 points max)
|
|
hstsScore := 0
|
|
if profile.HSTSEnabled {
|
|
hstsScore += 10
|
|
if profile.HSTSMaxAge >= 31536000 {
|
|
hstsScore += 5
|
|
} else {
|
|
suggestions = append(suggestions, "Increase HSTS max-age to at least 1 year")
|
|
}
|
|
if profile.HSTSIncludeSubdomains {
|
|
hstsScore += 5
|
|
} else {
|
|
suggestions = append(suggestions, "Enable HSTS for subdomains")
|
|
}
|
|
if profile.HSTSPreload {
|
|
hstsScore += 5
|
|
} else {
|
|
suggestions = append(suggestions, "Consider HSTS preload for browser preload lists")
|
|
}
|
|
} else {
|
|
suggestions = append(suggestions, "Enable HSTS to enforce HTTPS")
|
|
}
|
|
breakdown["hsts"] = hstsScore
|
|
|
|
// CSP (25 points max)
|
|
cspScore := 0
|
|
if profile.CSPEnabled {
|
|
cspScore += 15
|
|
// Additional points for strict CSP
|
|
if !strings.Contains(profile.CSPDirectives, "'unsafe-inline'") {
|
|
cspScore += 5
|
|
} else {
|
|
suggestions = append(suggestions, "Avoid 'unsafe-inline' in CSP for better security")
|
|
}
|
|
if !strings.Contains(profile.CSPDirectives, "'unsafe-eval'") {
|
|
cspScore += 5
|
|
} else {
|
|
suggestions = append(suggestions, "Avoid 'unsafe-eval' in CSP for better security")
|
|
}
|
|
} else {
|
|
suggestions = append(suggestions, "Enable Content-Security-Policy")
|
|
}
|
|
breakdown["csp"] = cspScore
|
|
|
|
// X-Frame-Options (10 points)
|
|
xfoScore := 0
|
|
if profile.XFrameOptions == "DENY" {
|
|
xfoScore = 10
|
|
} else if profile.XFrameOptions == "SAMEORIGIN" {
|
|
xfoScore = 7
|
|
} else {
|
|
suggestions = append(suggestions, "Set X-Frame-Options to DENY or SAMEORIGIN")
|
|
}
|
|
breakdown["x_frame_options"] = xfoScore
|
|
|
|
// X-Content-Type-Options (10 points)
|
|
xctoScore := 0
|
|
if profile.XContentTypeOptions {
|
|
xctoScore = 10
|
|
} else {
|
|
suggestions = append(suggestions, "Enable X-Content-Type-Options: nosniff")
|
|
}
|
|
breakdown["x_content_type_options"] = xctoScore
|
|
|
|
// Referrer-Policy (10 points)
|
|
rpScore := 0
|
|
strictPolicies := []string{"no-referrer", "strict-origin", "strict-origin-when-cross-origin"}
|
|
for _, p := range strictPolicies {
|
|
if profile.ReferrerPolicy == p {
|
|
rpScore = 10
|
|
break
|
|
}
|
|
}
|
|
if profile.ReferrerPolicy == "origin-when-cross-origin" {
|
|
rpScore = 7
|
|
}
|
|
if rpScore == 0 && profile.ReferrerPolicy != "" {
|
|
rpScore = 3
|
|
}
|
|
if rpScore < 10 {
|
|
suggestions = append(suggestions, "Use a stricter Referrer-Policy")
|
|
}
|
|
breakdown["referrer_policy"] = rpScore
|
|
|
|
// Permissions-Policy (10 points)
|
|
ppScore := 0
|
|
if profile.PermissionsPolicy != "" {
|
|
ppScore = 10
|
|
} else {
|
|
suggestions = append(suggestions, "Add Permissions-Policy to restrict browser features")
|
|
}
|
|
breakdown["permissions_policy"] = ppScore
|
|
|
|
// Cross-Origin headers (10 points)
|
|
coScore := 0
|
|
if profile.CrossOriginOpenerPolicy != "" {
|
|
coScore += 4
|
|
}
|
|
if profile.CrossOriginResourcePolicy != "" {
|
|
coScore += 3
|
|
}
|
|
if profile.CrossOriginEmbedderPolicy != "" {
|
|
coScore += 3
|
|
}
|
|
if coScore < 10 {
|
|
suggestions = append(suggestions, "Add Cross-Origin isolation headers")
|
|
}
|
|
breakdown["cross_origin"] = coScore
|
|
|
|
// Calculate total
|
|
total := hstsScore + cspScore + xfoScore + xctoScore + rpScore + ppScore + coScore
|
|
|
|
return ScoreBreakdown{
|
|
TotalScore: total,
|
|
MaxScore: maxScore,
|
|
Breakdown: breakdown,
|
|
Suggestions: suggestions,
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.3 Initialize Presets on Startup
|
|
|
|
**File**: `backend/internal/api/routes/routes.go`
|
|
|
|
Add to the Register function:
|
|
|
|
```go
|
|
// Ensure security header presets exist
|
|
secHeadersSvc := services.NewSecurityHeadersService(db)
|
|
if err := secHeadersSvc.EnsurePresetsExist(); err != nil {
|
|
logger.Log().WithError(err).Warn("Failed to initialize security header presets")
|
|
}
|
|
```
|
|
|
|
### 5.4 Unit Tests
|
|
|
|
**File**: `backend/internal/services/security_score_test.go`
|
|
|
|
Test cases:
|
|
|
|
- `TestCalculateScore_AllEnabled` - Full score
|
|
- `TestCalculateScore_HSTSOnly` - Partial score
|
|
- `TestCalculateScore_NoHeaders` - Zero score
|
|
- `TestCalculateScore_UnsafeCSP` - CSP with unsafe directives
|
|
- `TestCalculateScore_Suggestions` - Verify suggestions generated
|
|
|
|
**File**: `backend/internal/services/security_headers_service_test.go`
|
|
|
|
Test cases:
|
|
|
|
- `TestGetPresets` - Returns all presets
|
|
- `TestEnsurePresetsExist_Creates` - Creates presets when missing
|
|
- `TestEnsurePresetsExist_NoOp` - Doesn't duplicate presets
|
|
|
|
---
|
|
|
|
## Phase 6: Route Registration and Final Integration
|
|
|
|
### 6.1 Frontend Routes
|
|
|
|
**File**: `frontend/src/App.tsx`
|
|
|
|
Add routes:
|
|
|
|
```tsx
|
|
<Route path="/security/headers" element={<SecurityHeaders />} />
|
|
<Route path="/security/headers/:id" element={<SecurityHeadersEdit />} />
|
|
<Route path="/security/headers/new" element={<SecurityHeadersCreate />} />
|
|
```
|
|
|
|
### 6.2 Navigation Update
|
|
|
|
Add "Security Headers" to the Security section in the navigation.
|
|
|
|
### 6.3 Integration Test
|
|
|
|
**File**: `backend/integration/security_headers_test.go`
|
|
|
|
End-to-end test:
|
|
|
|
1. Create a security header profile
|
|
2. Assign to a proxy host
|
|
3. Generate Caddy config
|
|
4. Verify headers in generated config
|
|
|
|
---
|
|
|
|
## Implementation Summary
|
|
|
|
### Files to Create
|
|
|
|
| Phase | File Path | Description |
|
|
|-------|-----------|-------------|
|
|
| 1 | `backend/internal/models/security_header_profile.go` | New model |
|
|
| 1 | `backend/internal/models/security_header_profile_test.go` | Model tests |
|
|
| 2 | `backend/internal/api/handlers/security_headers_handler.go` | API handlers |
|
|
| 2 | `backend/internal/api/handlers/security_headers_handler_test.go` | Handler tests |
|
|
| 3 | `backend/internal/caddy/config_security_headers_test.go` | Caddy integration tests |
|
|
| 4 | `frontend/src/api/securityHeaders.ts` | API client |
|
|
| 4 | `frontend/src/hooks/useSecurityHeaders.ts` | React Query hooks |
|
|
| 4 | `frontend/src/pages/SecurityHeaders.tsx` | Main page |
|
|
| 4 | `frontend/src/components/SecurityHeaderProfileForm.tsx` | Profile form |
|
|
| 4 | `frontend/src/components/CSPBuilder.tsx` | CSP builder |
|
|
| 4 | `frontend/src/components/SecurityScoreDisplay.tsx` | Score display |
|
|
| 4 | `frontend/src/components/PermissionsPolicyBuilder.tsx` | Permissions builder |
|
|
| 5 | `backend/internal/services/security_headers_service.go` | Service layer |
|
|
| 5 | `backend/internal/services/security_score.go` | Score calculator |
|
|
| 5 | `backend/internal/services/security_score_test.go` | Score tests |
|
|
| 5 | `backend/internal/services/security_headers_service_test.go` | Service tests |
|
|
|
|
### Files to Modify
|
|
|
|
| Phase | File Path | Changes |
|
|
|-------|-----------|---------|
|
|
| 1 | `backend/internal/models/proxy_host.go` | Add security header fields |
|
|
| 1 | `backend/internal/api/routes/routes.go` | Add AutoMigrate |
|
|
| 2 | `backend/internal/api/routes/routes.go` | Register handlers |
|
|
| 3 | `backend/internal/caddy/config.go` | Add header handler builder |
|
|
| 3 | `backend/internal/caddy/manager.go` | Preload profiles |
|
|
| 4 | `frontend/src/components/ProxyHostForm.tsx` | Add profile selector |
|
|
| 4 | `frontend/src/App.tsx` | Add routes |
|
|
| 6 | Navigation component | Add menu item |
|
|
|
|
---
|
|
|
|
## Configuration File Updates
|
|
|
|
### .gitignore
|
|
|
|
No changes required - existing patterns cover new files.
|
|
|
|
### .codecov.yml
|
|
|
|
No changes required - existing patterns cover new test files.
|
|
|
|
### .dockerignore
|
|
|
|
No changes required - existing patterns cover new files.
|
|
|
|
### Dockerfile
|
|
|
|
No changes required - no new build dependencies needed.
|
|
|
|
---
|
|
|
|
## Acceptance Criteria Checklist
|
|
|
|
| Criteria | Phase | Implementation |
|
|
|----------|-------|----------------|
|
|
| ✅ Security headers automatically added | Phase 3 | `buildSecurityHeadersHandler()` |
|
|
| ✅ CSP configurable without breaking sites | Phase 4 | `CSPBuilder.tsx` with validation |
|
|
| ✅ Presets available for easy setup | Phase 5 | Basic, Strict, Paranoid presets |
|
|
| ✅ Security score shown in UI | Phase 4 | `SecurityScoreDisplay.tsx` |
|
|
| ✅ HSTS with preload support | Phase 1 | Model fields for all HSTS options |
|
|
| ✅ Content-Security-Policy builder | Phase 4 | `CSPBuilder.tsx` |
|
|
| ✅ X-Frame-Options (DENY/SAMEORIGIN) | Phase 1 | Model field with validation |
|
|
| ✅ X-Content-Type-Options (nosniff) | Phase 1 | Model boolean field |
|
|
| ✅ Referrer-Policy configuration | Phase 1 | Model field with enum values |
|
|
| ✅ Permissions-Policy headers | Phase 1/4 | Model + `PermissionsPolicyBuilder.tsx` |
|
|
| ✅ Security header presets | Phase 5 | basic, strict, paranoid |
|
|
| ✅ Security score calculator | Phase 5 | `CalculateSecurityScore()` |
|
|
|
|
---
|
|
|
|
## Testing Requirements
|
|
|
|
### Backend Unit Tests (Target: 85%+ coverage)
|
|
|
|
- Model CRUD operations
|
|
- Handler request/response validation
|
|
- Score calculation accuracy
|
|
- CSP string building
|
|
- Permissions-Policy string building
|
|
- Caddy config generation with headers
|
|
|
|
### Frontend Unit Tests (Target: 85%+ coverage)
|
|
|
|
- Component rendering
|
|
- Form validation
|
|
- API hook behavior
|
|
- Score display logic
|
|
- CSP builder interactions
|
|
|
|
### Integration Tests
|
|
|
|
- End-to-end profile creation and application
|
|
- Caddy config generation verification
|
|
- Preset application flow
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
| Risk | Mitigation |
|
|
|------|------------|
|
|
| CSP breaks existing sites | CSP disabled by default, builder has validation |
|
|
| HSTS preload locked forever | Warning in UI, preload disabled by default |
|
|
| Performance impact | Headers cached per-host, no per-request calculation |
|
|
| Migration issues | New table, no schema changes to existing tables |
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. **Phase 1**: Create model and migration (1 day)
|
|
2. **Phase 2**: Implement API handlers (1-2 days)
|
|
3. **Phase 3**: Caddy integration (1 day)
|
|
4. **Phase 4**: Frontend components (2-3 days)
|
|
5. **Phase 5**: Presets and score calculator (1 day)
|
|
6. **Phase 6**: Final integration and testing (1 day)
|
|
|
|
**Total Estimated Time**: 7-9 days
|
|
|
|
---
|
|
|
|
*Plan created by Planning Agent for Issue #20*
|