mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-30 23:06:21 +02:00
172 lines
5.1 KiB
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)
|
|
}
|