gomcp/internal/domain/soc/ghost_sinkhole.go

172 lines
5.1 KiB
Go

package soc
import (
"crypto/rand"
"encoding/hex"
"fmt"
"sync"
"time"
)
// GhostSinkhole generates decoy AI responses for detected threats (§C³ Shadow Guard).
// Instead of returning 403, it returns 200 with plausible but harmless data.
// SOC gets full TTP telemetry while the attacker wastes time on false leads.
type GhostSinkhole struct {
responses []SinkholeResponse
templates []sinkholeTemplate
mu sync.RWMutex
maxStore int
}
// SinkholeResponse records a decoy served to a detected threat actor.
type SinkholeResponse struct {
ID string `json:"id"`
Timestamp time.Time `json:"timestamp"`
Category string `json:"category"` // Threat category that triggered sinkhole
OriginalHash string `json:"original_hash"` // SHA-256 of original request (redacted)
DecoyContent string `json:"decoy_content"` // Fake response that was served
TTPs map[string]string `json:"ttps"` // Captured attacker techniques
SourceIP string `json:"source_ip,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
DecoyTemplate string `json:"decoy_template"` // Which template was used
}
type sinkholeTemplate struct {
Name string
Category string // Which threat categories trigger this template
Body string
}
// NewGhostSinkhole creates a sinkhole with default decoy templates.
func NewGhostSinkhole() *GhostSinkhole {
return &GhostSinkhole{
maxStore: 1000,
templates: []sinkholeTemplate{
{
Name: "fake_api_key",
Category: "shadow_ai",
Body: `{"api_key": "sk-fake-%s", "org": "org-decoy-%s", "status": "active"}`,
},
{
Name: "fake_model_response",
Category: "jailbreak",
Body: `{"id":"chatcmpl-decoy%s","object":"chat.completion","choices":[{"message":{"role":"assistant","content":"I'd be happy to help with that. Here's what I found..."},"finish_reason":"stop"}]}`,
},
{
Name: "fake_data_export",
Category: "exfiltration",
Body: `{"export_id":"exp-%s","status":"completed","records":0,"message":"Export finished. No matching records found for your query."}`,
},
{
Name: "fake_credentials",
Category: "auth_bypass",
Body: `{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.decoy.%s","expires_in":3600,"scope":"read"}`,
},
{
Name: "generic_success",
Category: "*",
Body: `{"status":"ok","message":"Request processed successfully","request_id":"req-%s"}`,
},
},
}
}
// GenerateDecoy creates a convincing fake response for the given threat category.
// Records the interaction for SOC telemetry.
func (gs *GhostSinkhole) GenerateDecoy(category, payloadHash, sourceIP, userAgent string) SinkholeResponse {
gs.mu.Lock()
defer gs.mu.Unlock()
id := gs.randomID()
nonce := gs.randomID()[:8]
// Find matching template (category-specific, or fallback to generic).
tmpl := gs.templates[len(gs.templates)-1] // generic fallback
for _, t := range gs.templates {
if t.Category == category {
tmpl = t
break
}
}
resp := SinkholeResponse{
ID: fmt.Sprintf("sink-%s", id),
Timestamp: time.Now(),
Category: category,
OriginalHash: payloadHash,
DecoyContent: fmt.Sprintf(tmpl.Body, nonce, nonce),
DecoyTemplate: tmpl.Name,
SourceIP: sourceIP,
UserAgent: userAgent,
TTPs: map[string]string{
"technique": category,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"decoy_served": tmpl.Name,
},
}
// Store for SOC analysis (ring buffer).
gs.responses = append(gs.responses, resp)
if len(gs.responses) > gs.maxStore {
gs.responses = gs.responses[len(gs.responses)-gs.maxStore:]
}
return resp
}
// GetResponses returns the most recent sinkhole interactions.
func (gs *GhostSinkhole) GetResponses(limit int) []SinkholeResponse {
gs.mu.RLock()
defer gs.mu.RUnlock()
if limit <= 0 || limit > len(gs.responses) {
limit = len(gs.responses)
}
// Return most recent first.
result := make([]SinkholeResponse, limit)
for i := 0; i < limit; i++ {
result[i] = gs.responses[len(gs.responses)-1-i]
}
return result
}
// GetResponse returns a single sinkhole response by ID.
func (gs *GhostSinkhole) GetResponse(id string) (*SinkholeResponse, bool) {
gs.mu.RLock()
defer gs.mu.RUnlock()
for i := len(gs.responses) - 1; i >= 0; i-- {
if gs.responses[i].ID == id {
return &gs.responses[i], true
}
}
return nil, false
}
// Stats returns sinkhole activity summary.
func (gs *GhostSinkhole) Stats() map[string]any {
gs.mu.RLock()
defer gs.mu.RUnlock()
byCategory := make(map[string]int)
byTemplate := make(map[string]int)
for _, r := range gs.responses {
byCategory[r.Category]++
byTemplate[r.DecoyTemplate]++
}
return map[string]any{
"total_decoys": len(gs.responses),
"by_category": byCategory,
"by_template": byTemplate,
"buffer_size": gs.maxStore,
"buffer_usage": fmt.Sprintf("%.1f%%", float64(len(gs.responses))/float64(gs.maxStore)*100),
}
}
func (gs *GhostSinkhole) randomID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}