Files
Charon/docs/plans/archive/security_headers_investigation.md
2026-02-19 16:34:10 +00:00

10 KiB

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)

// Frontend expects this type:
export interface SecurityHeaderPreset {
  type: 'basic' | 'strict' | 'paranoid';
  name: string;
  description: string;
  score: number;           // <-- FRONTEND EXPECTS "score"
  config: Partial<SecurityHeaderProfile>;
}

// Renders:
{presets.map((preset) => (
  <div className="text-2xl font-bold">{preset.score}</div>  // <-- Uses preset.score
  <Button onClick={() => handleApplyPreset(preset.type)}>Apply Preset</Button>
))}

3. Backend GetPresets Handler (Lines 220-224)

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)

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:

SecurityScore int `json:"security_score"`

The JSON serialization sends security_score, but the frontend reads preset.score.

Evidence

In SecurityHeaders.tsx line 130:

<div className="text-2xl font-bold">{preset.score}</div>  // undefined!

But backend returns:

{
  "presets": [
    {
      "security_score": 65,  // NOT "score"
      "preset_type": "basic" // NOT "type"
    }
  ]
}

Why It Still "Works" Partially

The frontend API layer (securityHeaders.ts) defines:

async getPresets(): Promise<SecurityHeaderPreset[]> {
    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

Fix #1: Field Name Alignment (Critical)

Option A - Backend Change (Recommended): Transform the backend response to match frontend expectations:

// 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:

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