Files
Charon/backend/internal/services/security_notification_service.go
T
akanealw eec8c28fb3
Go Benchmark / Performance Regression Check (push) Has been cancelled
Cerberus Integration / Cerberus Security Stack Integration (push) Has been cancelled
Upload Coverage to Codecov / Backend Codecov Upload (push) Has been cancelled
Upload Coverage to Codecov / Frontend Codecov Upload (push) Has been cancelled
CodeQL - Analyze / CodeQL analysis (go) (push) Has been cancelled
CodeQL - Analyze / CodeQL analysis (javascript-typescript) (push) Has been cancelled
CrowdSec Integration / CrowdSec Bouncer Integration (push) Has been cancelled
Docker Build, Publish & Test / build-and-push (push) Has been cancelled
Quality Checks / Auth Route Protection Contract (push) Has been cancelled
Quality Checks / Codecov Trigger/Comment Parity Guard (push) Has been cancelled
Quality Checks / Backend (Go) (push) Has been cancelled
Quality Checks / Frontend (React) (push) Has been cancelled
Rate Limit integration / Rate Limiting Integration (push) Has been cancelled
Security Scan (PR) / Trivy Binary Scan (push) Has been cancelled
Supply Chain Verification (PR) / Verify Supply Chain (push) Has been cancelled
WAF integration / Coraza WAF Integration (push) Has been cancelled
Docker Build, Publish & Test / Security Scan PR Image (push) Has been cancelled
Repo Health Check / Repo health (push) Has been cancelled
History Rewrite Dry-Run / Dry-run preview for history rewrite (push) Has been cancelled
Prune Renovate Branches / prune (push) Has been cancelled
Renovate / renovate (push) Has been cancelled
Nightly Build & Package / sync-development-to-nightly (push) Has been cancelled
Nightly Build & Package / Trigger Nightly Validation Workflows (push) Has been cancelled
Nightly Build & Package / build-and-push-nightly (push) Has been cancelled
Nightly Build & Package / test-nightly-image (push) Has been cancelled
Nightly Build & Package / verify-nightly-supply-chain (push) Has been cancelled
Update GeoLite2 Checksum / update-checksum (push) Has been cancelled
Container Registry Prune / prune-ghcr (push) Has been cancelled
Container Registry Prune / prune-dockerhub (push) Has been cancelled
Container Registry Prune / summarize (push) Has been cancelled
Supply Chain Verification / Verify SBOM (push) Has been cancelled
Supply Chain Verification / Verify Release Artifacts (push) Has been cancelled
Supply Chain Verification / Verify Docker Image Supply Chain (push) Has been cancelled
Monitor Caddy Major Release / check-caddy-major (push) Has been cancelled
Weekly Nightly to Main Promotion / Verify Nightly Branch Health (push) Has been cancelled
Weekly Nightly to Main Promotion / Create Promotion PR (push) Has been cancelled
Weekly Nightly to Main Promotion / Trigger Missing Required Checks (push) Has been cancelled
Weekly Nightly to Main Promotion / Notify on Failure (push) Has been cancelled
Weekly Nightly to Main Promotion / Workflow Summary (push) Has been cancelled
Weekly Security Rebuild / Security Rebuild & Scan (push) Has been cancelled
changed perms
2026-04-22 18:19:14 +00:00

168 lines
4.6 KiB
Go
Executable File

package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/Wikid82/charon/backend/internal/logger"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/Wikid82/charon/backend/internal/network"
"github.com/Wikid82/charon/backend/internal/security"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
// SecurityNotificationService handles dispatching security event notifications.
type SecurityNotificationService struct {
db *gorm.DB
}
// NewSecurityNotificationService creates a new SecurityNotificationService instance.
func NewSecurityNotificationService(db *gorm.DB) *SecurityNotificationService {
return &SecurityNotificationService{db: db}
}
// GetSettings retrieves the notification configuration.
func (s *SecurityNotificationService) GetSettings() (*models.NotificationConfig, error) {
var config models.NotificationConfig
err := s.db.First(&config).Error
if err == gorm.ErrRecordNotFound {
// Return default config if none exists
return &models.NotificationConfig{
Enabled: false,
MinLogLevel: "error",
NotifyWAFBlocks: true,
NotifyACLDenies: true,
NotifyRateLimitHits: true,
}, nil
}
return &config, err
}
// UpdateSettings updates the notification configuration.
func (s *SecurityNotificationService) UpdateSettings(config *models.NotificationConfig) error {
var existing models.NotificationConfig
err := s.db.First(&existing).Error
if err == gorm.ErrRecordNotFound {
// Create new config
return s.db.Create(config).Error
}
if err != nil {
return fmt.Errorf("fetch existing config: %w", err)
}
// Update existing config
config.ID = existing.ID
return s.db.Save(config).Error
}
// Send dispatches a security event to configured channels.
func (s *SecurityNotificationService) Send(ctx context.Context, event models.SecurityEvent) error {
config, err := s.GetSettings()
if err != nil {
return fmt.Errorf("get settings: %w", err)
}
if !config.Enabled {
return nil
}
// Check if event type should be notified
if event.EventType == "waf_block" && !config.NotifyWAFBlocks {
return nil
}
if event.EventType == "acl_deny" && !config.NotifyACLDenies {
return nil
}
// Check severity against minimum log level
if !shouldNotify(event.Severity, config.MinLogLevel) {
return nil
}
// Dispatch to webhook if configured
if config.WebhookURL != "" {
if err := s.sendWebhook(ctx, config.WebhookURL, event); err != nil {
logger.Log().WithError(err).Error("Failed to send webhook notification")
return fmt.Errorf("send webhook: %w", err)
}
}
return nil
}
// sendWebhook sends the event to a webhook URL.
func (s *SecurityNotificationService) sendWebhook(ctx context.Context, webhookURL string, event models.SecurityEvent) error {
// CRITICAL FIX: Validate webhook URL before making request (SSRF protection)
validatedURL, err := security.ValidateExternalURL(webhookURL,
security.WithAllowLocalhost(), // Allow localhost for testing
security.WithAllowHTTP(), // Some webhooks use HTTP
)
if err != nil {
// Log SSRF attempt with high severity
logger.Log().WithFields(logrus.Fields{
"url": webhookURL,
"error": err.Error(),
"event_type": "ssrf_blocked",
"severity": "HIGH",
}).Warn("Blocked SSRF attempt in security notification webhook")
return fmt.Errorf("invalid webhook URL: %w", err)
}
payload, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshal event: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", validatedURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "Charon-Cerberus/1.0")
// Use SSRF-safe HTTP client for defense-in-depth
client := network.NewSafeHTTPClient(
network.WithTimeout(10*time.Second),
network.WithAllowLocalhost(), // Allow localhost for testing
)
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("execute request: %w", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
logger.Log().WithError(err).Warn("Failed to close response body")
}
}()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
}
return nil
}
// shouldNotify determines if an event should trigger a notification based on severity.
func shouldNotify(eventSeverity, minLevel string) bool {
levels := map[string]int{
"debug": 0,
"info": 1,
"warn": 2,
"error": 3,
}
eventLevel := levels[eventSeverity]
minLevelValue := levels[minLevel]
return eventLevel >= minLevelValue
}