# Security Headers Investigation Report **Date**: December 18, 2025 **Issue**: Quick Start Presets Not Applying / Score Not Showing + Potential Redundancy --- ## Executive Summary After tracing the complete data flow from frontend → API → backend → database, I identified **two root causes** and a **design clarity issue**: 1. **Root Cause #1 - Type Mismatch**: The frontend API expects `SecurityHeaderPreset` type with a `score` field, but the backend returns `SecurityHeaderProfile[]` with a `security_score` field 2. **Root Cause #2 - Presets ARE Working**: The presets actually DO work, but the UI displays `preset.score` which doesn't exist on the backend response 3. **Redundancy Question**: Quick Start and System Presets serve DIFFERENT purposes (one creates new profiles, one shows existing system presets), but the UX doesn't make this clear --- ## Complete Data Flow Trace ### 1. Frontend Component Flow (`SecurityHeaders.tsx`) ``` Component Mounts ↓ useSecurityHeaderProfiles() → GET /api/v1/security/headers/profiles → Returns ALL profiles (custom + system presets) → Splits into: presetProfiles (is_preset=true) + customProfiles (is_preset=false) ↓ useSecurityHeaderPresets() → GET /api/v1/security/headers/presets → Returns preset TEMPLATES (not saved profiles) → Used for "Quick Start Presets" section ``` **Key Insight**: There are TWO separate data sources: 1. **Profiles** (`/profiles`) - Saved profiles in the database (includes system presets with `is_preset=true`) 2. **Presets** (`/presets`) - Template definitions for creating new profiles (not stored in DB) ### 2. Quick Start Presets Section (Lines 120-145) ```tsx // Frontend expects this type: export interface SecurityHeaderPreset { type: 'basic' | 'strict' | 'paranoid'; name: string; description: string; score: number; // <-- FRONTEND EXPECTS "score" config: Partial; } // Renders: {presets.map((preset) => (
{preset.score}
// <-- Uses preset.score ))} ``` ### 3. Backend GetPresets Handler (Lines 220-224) ```go func (h *SecurityHeadersHandler) GetPresets(c *gin.Context) { presets := h.service.GetPresets() // Returns []models.SecurityHeaderProfile c.JSON(http.StatusOK, gin.H{"presets": presets}) } ``` ### 4. Backend Service GetPresets (security_headers_service.go) ```go func (s *SecurityHeadersService) GetPresets() []models.SecurityHeaderProfile { return []models.SecurityHeaderProfile{ { Name: "Basic Security", PresetType: "basic", SecurityScore: 65, // <-- BACKEND RETURNS "security_score" // ... }, // ... } } ``` ### 5. Type Field Mismatch | Frontend Expects | Backend Returns | Result | |------------------|-----------------|--------| | `preset.score` | `preset.security_score` | Score shows as `undefined` | | `preset.type` | `preset.preset_type` | Works (mapped correctly) | | `preset.config` | Not returned | `undefined` (not critical) | --- ## Issue #1: Score Not Showing ### Root Cause The frontend TypeScript interface defines `score: number`, but the backend Go struct has: ```go SecurityScore int `json:"security_score"` ``` The JSON serialization sends `security_score`, but the frontend reads `preset.score`. ### Evidence In `SecurityHeaders.tsx` line 130: ```tsx
{preset.score}
// undefined! ``` But backend returns: ```json { "presets": [ { "security_score": 65, // NOT "score" "preset_type": "basic" // NOT "type" } ] } ``` ### Why It Still "Works" Partially The frontend API layer (`securityHeaders.ts`) defines: ```typescript async getPresets(): Promise { const response = await client.get<{presets: SecurityHeaderPreset[]}>('/security/headers/presets'); return response.data.presets; } ``` TypeScript doesn't validate runtime data - it just trusts the type. The actual JSON has `security_score` but TypeScript thinks it has `score`. --- ## Issue #2: Preset Not Applying (Or Is It?) ### Flow When "Apply Preset" is Clicked ``` 1. User clicks "Apply Preset" on "Basic" card 2. handleApplyPreset("basic") is called 3. applyPresetMutation.mutate({ preset_type: "basic", name: "Basic Security Profile" }) 4. POST /api/v1/security/headers/presets/apply { preset_type: "basic", name: "..." } 5. Backend ApplyPreset() creates a NEW profile from the preset template 6. Returns the new profile 7. React Query invalidates 'securityHeaderProfiles' query 8. New profile appears in "Custom Profiles" section (NOT "System Presets") ``` ### The Preset IS Working The preset application actually **does work** - it creates a new custom profile. The confusion is: - User clicks "Apply" on Basic preset - A new **custom** profile named "Basic Security Profile" appears - It's NOT a system preset, it's a copy with `is_preset=false` **Users might not realize** the new profile was created because: 1. No navigation to the new profile 2. No highlighting of the newly created profile 3. The score on the Quick Start card shows `undefined` --- ## Issue #3: Redundancy Analysis - Quick Start vs System Presets ### What They Are | Section | Data Source | Purpose | Read-Only? | |---------|-------------|---------|------------| | **Quick Start Presets** | `/presets` (templates) | Templates to CREATE new profiles | N/A (action buttons) | | **System Presets** | `/profiles` with `is_preset=true` | Pre-saved profiles in DB | Yes (can only View/Clone) | ### Are They Redundant? **Answer: Partially YES - They show the SAME presets twice, but serve different UX purposes.** 1. **Quick Start**: Action-oriented - "Click to create a new profile from this template" 2. **System Presets**: Reference-oriented - "These are the built-in profiles you can clone or view" **The Problem**: - If `EnsurePresetsExist()` runs on startup, it creates the presets in the database - So System Presets section shows the same Basic/Strict/Paranoid presets - Quick Start also shows Basic/Strict/Paranoid templates - User sees the same 3 presets TWICE ### Why Both Exist Looking at the code: - `EnsurePresetsExist()` saves presets to DB so they can be assigned to hosts - Quick Start exists to let users create custom copies with different names - But the UX doesn't differentiate them clearly --- ## Recommended Solutions ### Fix #1: Field Name Alignment (Critical) **Option A - Backend Change** (Recommended): Transform the backend response to match frontend expectations: ```go // In GetPresets handler, transform before returning: type PresetResponse struct { Type string `json:"type"` Name string `json:"name"` Description string `json:"description"` Score int `json:"score"` } ``` **Option B - Frontend Change**: Update TypeScript interface and component to use `security_score` and `preset_type`: ```typescript export interface SecurityHeaderPreset { preset_type: string; // Changed from "type" name: string; description: string; security_score: number; // Changed from "score" // ... other fields match SecurityHeaderProfile } ``` ### Fix #2: UX Clarity for Redundancy **Option A - Remove Quick Start Section**: - Users can clone from System Presets instead - Fewer UI elements, less confusion **Option B - Differentiate Purpose**: - Rename "Quick Start Presets" → "Create from Template" - Add descriptive text: "Create a new custom profile based on these templates" - Rename "System Presets" → "Built-in Profiles (Read-Only)" **Option C - Don't Save Presets to DB**: - Remove `EnsurePresetsExist()` call - Keep Quick Start only - Users always create custom profiles from templates - System Presets section would be empty (could remove) ### Fix #3: Success Feedback on Apply After applying a preset: 1. Show toast: "Created 'Basic Security Profile' - Scroll down to see it" 2. Or navigate to edit page for the new profile 3. Or highlight/scroll to the new profile card --- ## Code Locations for Fixes | Fix | File | Lines | |-----|------|-------| | Backend type transform | `handlers/security_headers_handler.go` | 220-224 | | Frontend type update | `api/securityHeaders.ts` | 33-40 | | Frontend score display | `pages/SecurityHeaders.tsx` | 130 | | Frontend preset_type | `pages/SecurityHeaders.tsx` | 138 | | Success feedback | `hooks/useSecurityHeaders.ts` | 75-87 | | Remove quick start | `pages/SecurityHeaders.tsx` | 118-148 | | Preset DB creation | `routes/routes.go` | (where EnsurePresetsExist called) | --- ## Severity Assessment | Issue | Severity | User Impact | |-------|----------|-------------| | Score not showing | **High** | UI shows "undefined", looks broken | | Preset applies but unclear | **Medium** | Works but confusing | | Redundancy confusion | **Low** | UX issue, not functional bug | --- ## Verification Steps To confirm these findings: 1. **Check browser Network tab** when loading Security Headers page: - GET `/api/v1/security/headers/presets` response should show `security_score` not `score` 2. **Check console for TypeScript errors**: - Should NOT show errors (TypeScript doesn't validate runtime JSON) 3. **Click "Apply Preset"**: - Watch Network tab for POST to `/presets/apply` - Check if new profile appears in Custom Profiles section 4. **Compare Quick Start cards vs System Presets cards**: - Should show same 3 presets (Basic, Strict, Paranoid) --- ## Conclusion The issues stem from: 1. **Type mismatch** between frontend interface and backend JSON serialization 2. **Unclear UX** about what "Apply Preset" does vs. viewing System Presets 3. **Redundant display** of the same preset data in two sections **Recommended Priority**: 1. First: Fix the type mismatch (score → security_score, type → preset_type) 2. Second: Improve success feedback on preset application 3. Third: Consolidate or differentiate the two preset sections --- *Investigation completed by Copilot - December 18, 2025*