2026-03-31 22:13:34 +10:00
// Copyright 2026 Syntrex Lab. All rights reserved.
// Use of this source code is governed by an Apache-2.0 license
// that can be found in the LICENSE file.
2026-03-27 12:45:11 +10:00
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" `
2026-03-31 22:13:34 +10:00
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
2026-03-27 12:45:11 +10:00
SourceIP string ` json:"source_ip,omitempty" `
UserAgent string ` json:"user_agent,omitempty" `
2026-03-31 22:13:34 +10:00
DecoyTemplate string ` json:"decoy_template" ` // Which template was used
2026-03-27 12:45:11 +10:00
}
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 {
2026-03-31 22:13:34 +10:00
"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 ) ,
2026-03-27 12:45:11 +10:00
}
}
func ( gs * GhostSinkhole ) randomID ( ) string {
b := make ( [ ] byte , 8 )
rand . Read ( b )
return hex . EncodeToString ( b )
}