mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-11 20:42:36 +02:00
feat: SOC ghost sinkhole, rate limiter, RBAC, demo seed
This commit is contained in:
parent
cc7956d835
commit
b8097d3f1b
19 changed files with 1169 additions and 63 deletions
172
internal/domain/soc/ghost_sinkhole.go
Normal file
172
internal/domain/soc/ghost_sinkhole.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue