chore: clean .gitignore cache
This commit is contained in:
@@ -1,361 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Wikid82/charon/backend/internal/caddy"
|
||||
"github.com/Wikid82/charon/backend/internal/models"
|
||||
"github.com/Wikid82/charon/backend/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SecurityHeadersHandler manages security header profiles
|
||||
type SecurityHeadersHandler struct {
|
||||
db *gorm.DB
|
||||
caddyManager *caddy.Manager
|
||||
service *services.SecurityHeadersService
|
||||
}
|
||||
|
||||
// NewSecurityHeadersHandler creates a new handler
|
||||
func NewSecurityHeadersHandler(db *gorm.DB, caddyManager *caddy.Manager) *SecurityHeadersHandler {
|
||||
return &SecurityHeadersHandler{
|
||||
db: db,
|
||||
caddyManager: caddyManager,
|
||||
service: services.NewSecurityHeadersService(db),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all security headers routes
|
||||
func (h *SecurityHeadersHandler) RegisterRoutes(router *gin.RouterGroup) {
|
||||
group := router.Group("/security/headers")
|
||||
group.GET("/profiles", h.ListProfiles)
|
||||
group.GET("/profiles/:id", h.GetProfile)
|
||||
group.POST("/profiles", h.CreateProfile)
|
||||
group.PUT("/profiles/:id", h.UpdateProfile)
|
||||
group.DELETE("/profiles/:id", h.DeleteProfile)
|
||||
|
||||
group.GET("/presets", h.GetPresets)
|
||||
group.POST("/presets/apply", h.ApplyPreset)
|
||||
|
||||
group.POST("/score", h.CalculateScore)
|
||||
|
||||
group.POST("/csp/validate", h.ValidateCSP)
|
||||
group.POST("/csp/build", h.BuildCSP)
|
||||
}
|
||||
|
||||
// ListProfiles returns all security header profiles
|
||||
// GET /api/v1/security/headers/profiles
|
||||
func (h *SecurityHeadersHandler) ListProfiles(c *gin.Context) {
|
||||
var profiles []models.SecurityHeaderProfile
|
||||
if err := h.db.Order("is_preset DESC, name ASC").Find(&profiles).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"profiles": profiles})
|
||||
}
|
||||
|
||||
// GetProfile returns a single profile by ID or UUID
|
||||
// GET /api/v1/security/headers/profiles/:id
|
||||
func (h *SecurityHeadersHandler) GetProfile(c *gin.Context) {
|
||||
idParam := c.Param("id")
|
||||
|
||||
var profile models.SecurityHeaderProfile
|
||||
|
||||
// Try to parse as uint ID first
|
||||
if id, err := strconv.ParseUint(idParam, 10, 32); err == nil {
|
||||
if err := h.db.First(&profile, uint(id)).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Try UUID
|
||||
if err := h.db.Where("uuid = ?", idParam).First(&profile).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"profile": profile})
|
||||
}
|
||||
|
||||
// CreateProfile creates a new security header profile
|
||||
// POST /api/v1/security/headers/profiles
|
||||
func (h *SecurityHeadersHandler) CreateProfile(c *gin.Context) {
|
||||
var req models.SecurityHeaderProfile
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate name is provided
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate UUID
|
||||
req.UUID = uuid.New().String()
|
||||
|
||||
// Calculate security score
|
||||
scoreResult := services.CalculateSecurityScore(&req)
|
||||
req.SecurityScore = scoreResult.TotalScore
|
||||
|
||||
// Create profile
|
||||
if err := h.db.Create(&req).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"profile": req})
|
||||
}
|
||||
|
||||
// UpdateProfile updates an existing profile
|
||||
// PUT /api/v1/security/headers/profiles/:id
|
||||
func (h *SecurityHeadersHandler) UpdateProfile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var existing models.SecurityHeaderProfile
|
||||
if err := h.db.First(&existing, uint(id)).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Cannot modify presets
|
||||
if existing.IsPreset {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "cannot modify system presets"})
|
||||
return
|
||||
}
|
||||
|
||||
var updates models.SecurityHeaderProfile
|
||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve ID and UUID
|
||||
updates.ID = existing.ID
|
||||
updates.UUID = existing.UUID
|
||||
|
||||
// Recalculate security score
|
||||
scoreResult := services.CalculateSecurityScore(&updates)
|
||||
updates.SecurityScore = scoreResult.TotalScore
|
||||
|
||||
if err := h.db.Save(&updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"profile": updates})
|
||||
}
|
||||
|
||||
// DeleteProfile deletes a profile (not presets)
|
||||
// DELETE /api/v1/security/headers/profiles/:id
|
||||
func (h *SecurityHeadersHandler) DeleteProfile(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var profile models.SecurityHeaderProfile
|
||||
if err := h.db.First(&profile, uint(id)).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Cannot delete presets
|
||||
if profile.IsPreset {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete system presets"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if profile is in use by any proxy hosts
|
||||
var count int64
|
||||
if err := h.db.Model(&models.ProxyHost{}).Where("security_header_profile_id = ?", id).Count(&count).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf("profile is in use by %d proxy host(s)", count)})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Delete(&profile).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
// GetPresets returns the list of built-in presets
|
||||
// GET /api/v1/security/headers/presets
|
||||
func (h *SecurityHeadersHandler) GetPresets(c *gin.Context) {
|
||||
presets := h.service.GetPresets()
|
||||
c.JSON(http.StatusOK, gin.H{"presets": presets})
|
||||
}
|
||||
|
||||
// ApplyPreset applies a preset to create/update a profile
|
||||
// POST /api/v1/security/headers/presets/apply
|
||||
func (h *SecurityHeadersHandler) ApplyPreset(c *gin.Context) {
|
||||
var req struct {
|
||||
PresetType string `json:"preset_type" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := h.service.ApplyPreset(req.PresetType, req.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"profile": profile})
|
||||
}
|
||||
|
||||
// CalculateScore calculates security score for given settings
|
||||
// POST /api/v1/security/headers/score
|
||||
func (h *SecurityHeadersHandler) CalculateScore(c *gin.Context) {
|
||||
var profile models.SecurityHeaderProfile
|
||||
if err := c.ShouldBindJSON(&profile); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
scoreResult := services.CalculateSecurityScore(&profile)
|
||||
c.JSON(http.StatusOK, scoreResult)
|
||||
}
|
||||
|
||||
// ValidateCSP validates a CSP string
|
||||
// POST /api/v1/security/headers/csp/validate
|
||||
func (h *SecurityHeadersHandler) ValidateCSP(c *gin.Context) {
|
||||
var req struct {
|
||||
CSP string `json:"csp" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
errors := validateCSPString(req.CSP)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
})
|
||||
}
|
||||
|
||||
// BuildCSP builds a CSP string from directives
|
||||
// POST /api/v1/security/headers/csp/build
|
||||
func (h *SecurityHeadersHandler) BuildCSP(c *gin.Context) {
|
||||
var req struct {
|
||||
Directives []models.CSPDirective `json:"directives" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert directives to map for JSON storage
|
||||
directivesMap := make(map[string][]string)
|
||||
for _, dir := range req.Directives {
|
||||
directivesMap[dir.Directive] = dir.Values
|
||||
}
|
||||
|
||||
cspJSON, err := json.Marshal(directivesMap)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to build CSP"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"csp": string(cspJSON)})
|
||||
}
|
||||
|
||||
// validateCSPString performs basic validation on a CSP string
|
||||
func validateCSPString(csp string) []string {
|
||||
var errors []string
|
||||
|
||||
if csp == "" {
|
||||
errors = append(errors, "CSP cannot be empty")
|
||||
return errors
|
||||
}
|
||||
|
||||
// Try to parse as JSON
|
||||
var directivesMap map[string][]string
|
||||
if err := json.Unmarshal([]byte(csp), &directivesMap); err != nil {
|
||||
errors = append(errors, "CSP must be valid JSON")
|
||||
return errors
|
||||
}
|
||||
|
||||
// Validate known directives
|
||||
validDirectives := map[string]bool{
|
||||
"default-src": true,
|
||||
"script-src": true,
|
||||
"style-src": true,
|
||||
"img-src": true,
|
||||
"font-src": true,
|
||||
"connect-src": true,
|
||||
"frame-src": true,
|
||||
"object-src": true,
|
||||
"media-src": true,
|
||||
"worker-src": true,
|
||||
"manifest-src": true,
|
||||
"base-uri": true,
|
||||
"form-action": true,
|
||||
"frame-ancestors": true,
|
||||
"report-uri": true,
|
||||
"report-to": true,
|
||||
"upgrade-insecure-requests": true,
|
||||
"block-all-mixed-content": true,
|
||||
}
|
||||
|
||||
for directive := range directivesMap {
|
||||
if !validDirectives[directive] {
|
||||
errors = append(errors, fmt.Sprintf("unknown CSP directive: %s", directive))
|
||||
}
|
||||
}
|
||||
|
||||
// Warn about unsafe directives
|
||||
for directive, values := range directivesMap {
|
||||
for _, value := range values {
|
||||
if strings.Contains(value, "unsafe-inline") || strings.Contains(value, "unsafe-eval") {
|
||||
errors = append(errors, fmt.Sprintf("'%s' contains unsafe directive in %s", value, directive))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
Reference in New Issue
Block a user