Files
breakpilot-compliance/pca-platform/heuristic-service/internal/api/handlers.go
Benjamin Boenisch 4435e7ea0a Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:28 +01:00

286 lines
7.9 KiB
Go

package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/breakpilot/pca-platform/heuristic-service/internal/config"
"github.com/breakpilot/pca-platform/heuristic-service/internal/heuristics"
"github.com/breakpilot/pca-platform/heuristic-service/internal/stepup"
)
// Handler holds all API handlers
type Handler struct {
config *config.Config
scorer *heuristics.Scorer
webauthn *stepup.WebAuthnService
pow *stepup.PoWService
}
// NewHandler creates a new API handler
func NewHandler(cfg *config.Config) *Handler {
return &Handler{
config: cfg,
scorer: heuristics.NewScorer(cfg),
webauthn: stepup.NewWebAuthnService(&cfg.StepUp.WebAuthn),
pow: stepup.NewPoWService(&cfg.StepUp.PoW),
}
}
// TickRequest represents metrics sent from client SDK
type TickRequest struct {
SessionID string `json:"session_id"`
Score float64 `json:"score,omitempty"`
DwellRatio float64 `json:"dwell_ratio"`
ScrollDepth float64 `json:"scroll_depth"`
Clicks int `json:"clicks"`
MouseMoves int `json:"mouse_moves"`
KeyStrokes int `json:"key_strokes,omitempty"`
TouchEvents int `json:"touch_events,omitempty"`
MouseVelocities []float64 `json:"mouse_velocities,omitempty"`
ScrollVelocities []float64 `json:"scroll_velocities,omitempty"`
ClickIntervals []float64 `json:"click_intervals,omitempty"`
Timestamp int64 `json:"ts"`
}
// TickResponse returns the computed score and action
type TickResponse struct {
SessionID string `json:"session_id"`
Score float64 `json:"score"`
Action string `json:"action"`
StepUpMethod string `json:"step_up_method,omitempty"`
Message string `json:"message,omitempty"`
}
// HandleTick receives tick data from client SDK
func (h *Handler) HandleTick(c *gin.Context) {
var req TickRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
// Generate session ID if not provided
if req.SessionID == "" {
req.SessionID = uuid.New().String()
}
// Get or create session
session := h.scorer.GetOrCreateSession(req.SessionID)
// Update metrics
totalTime := time.Since(session.StartTime).Seconds()
session.VisibleTime = req.DwellRatio * totalTime
session.MaxScrollPercent = req.ScrollDepth / 100.0 // Convert from percentage
session.ClickCount = req.Clicks
session.MouseMoves = req.MouseMoves
session.KeyStrokes = req.KeyStrokes
session.TouchEvents = req.TouchEvents
if len(req.MouseVelocities) > 0 {
session.MouseVelocities = append(session.MouseVelocities, req.MouseVelocities...)
}
if len(req.ScrollVelocities) > 0 {
session.ScrollVelocities = append(session.ScrollVelocities, req.ScrollVelocities...)
}
if len(req.ClickIntervals) > 0 {
session.ClickIntervals = append(session.ClickIntervals, req.ClickIntervals...)
}
// Calculate score
score := h.scorer.CalculateScore(req.SessionID)
// Determine action
var action, stepUpMethod, message string
if score >= h.config.Thresholds.ScorePass {
action = "allow"
message = "Human behavior detected"
} else if score >= h.config.Thresholds.ScoreChallenge {
action = "allow"
message = "Acceptable behavior"
} else {
action = "challenge"
stepUpMethod = h.config.StepUp.Primary
message = "Additional verification required"
}
c.JSON(http.StatusOK, TickResponse{
SessionID: req.SessionID,
Score: score,
Action: action,
StepUpMethod: stepUpMethod,
Message: message,
})
}
// HandleEvaluate evaluates a session for a specific path
func (h *Handler) HandleEvaluate(c *gin.Context) {
sessionID := c.Query("session_id")
path := c.Query("path")
if sessionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "session_id required"})
return
}
if path == "" {
path = "default"
}
// TODO: Load path configs from ai-access.json
pathConfigs := map[string]config.PathConfig{}
result := h.scorer.EvaluateRequest(sessionID, path, pathConfigs)
c.JSON(http.StatusOK, result)
}
// HandleWebAuthnChallenge creates a WebAuthn challenge
func (h *Handler) HandleWebAuthnChallenge(c *gin.Context) {
if !h.webauthn.IsEnabled() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "WebAuthn not enabled"})
return
}
sessionID := c.Query("session_id")
if sessionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "session_id required"})
return
}
challenge, err := h.webauthn.CreateChallenge(sessionID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create challenge"})
return
}
c.JSON(http.StatusOK, challenge)
}
// HandleWebAuthnVerify verifies a WebAuthn assertion
func (h *Handler) HandleWebAuthnVerify(c *gin.Context) {
if !h.webauthn.IsEnabled() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "WebAuthn not enabled"})
return
}
var req stepup.VerifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
verified, err := h.webauthn.VerifyChallenge(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Verification failed"})
return
}
c.JSON(http.StatusOK, gin.H{
"verified": verified,
"session_id": req.SessionID,
})
}
// HandlePoWChallenge creates a Proof-of-Work challenge
func (h *Handler) HandlePoWChallenge(c *gin.Context) {
if !h.pow.IsEnabled() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "PoW not enabled"})
return
}
sessionID := c.Query("session_id")
if sessionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "session_id required"})
return
}
challenge, err := h.pow.CreateChallenge(sessionID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create challenge"})
return
}
c.JSON(http.StatusOK, challenge)
}
// HandlePoWVerify verifies a Proof-of-Work solution
func (h *Handler) HandlePoWVerify(c *gin.Context) {
if !h.pow.IsEnabled() {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "PoW not enabled"})
return
}
var req stepup.PoWVerifyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
verified, err := h.pow.VerifyChallenge(&req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Verification failed"})
return
}
c.JSON(http.StatusOK, gin.H{
"verified": verified,
"session_id": req.SessionID,
})
}
// HandleGetConfig returns client-safe configuration
func (h *Handler) HandleGetConfig(c *gin.Context) {
// Return only non-sensitive config for client SDK
clientConfig := gin.H{
"thresholds": h.config.Thresholds,
"weights": h.config.Weights,
"tick": gin.H{
"endpoint": h.config.Tick.Endpoint,
"interval_ms": h.config.Tick.IntervalMs,
},
"step_up": gin.H{
"methods": h.config.StepUp.Methods,
"primary": h.config.StepUp.Primary,
"webauthn": gin.H{
"enabled": h.config.StepUp.WebAuthn.Enabled,
"userVerification": h.config.StepUp.WebAuthn.UserVerification,
"timeout_ms": h.config.StepUp.WebAuthn.TimeoutMs,
"challenge_endpoint": h.config.StepUp.WebAuthn.ChallengeEndpoint,
},
"pow": gin.H{
"enabled": h.config.StepUp.PoW.Enabled,
"difficulty": h.config.StepUp.PoW.Difficulty,
"max_duration_ms": h.config.StepUp.PoW.MaxDurationMs,
},
},
"compliance": h.config.Compliance,
}
c.JSON(http.StatusOK, clientConfig)
}
// HandleHealth returns service health
func (h *Handler) HandleHealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "pca-heuristic-service",
"version": "0.1.0",
})
}
// StartCleanupRoutine starts background cleanup
func (h *Handler) StartCleanupRoutine() {
go func() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
h.scorer.CleanupOldSessions(30 * time.Minute)
h.webauthn.CleanupExpiredChallenges()
h.pow.CleanupExpiredChallenges()
}
}()
}