gomcp/internal/application/shadow_ai/soc_integration.go

377 lines
10 KiB
Go

// 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.
package shadow_ai
import (
"context"
"fmt"
"log/slog"
"sync"
"time"
)
// ShadowAIController is the main orchestrator that ties together
// detection, enforcement, SOC event emission, and statistics.
type ShadowAIController struct {
mu sync.RWMutex
registry *PluginRegistry
fallback *FallbackManager
healthChecker *HealthChecker
netDetector *NetworkDetector
behavioral *BehavioralDetector
docBridge *DocBridge
approval *ApprovalEngine
events []ShadowAIEvent // In-memory event store (bounded)
maxEvents int
socEventFn func(source, severity, category, description string, meta map[string]string) // Bridge to SOC event bus
logger *slog.Logger
}
// NewShadowAIController creates the main Shadow AI Control orchestrator.
func NewShadowAIController() *ShadowAIController {
registry := NewPluginRegistry()
RegisterDefaultPlugins(registry)
return &ShadowAIController{
registry: registry,
fallback: NewFallbackManager(registry, "detect_only"),
netDetector: NewNetworkDetector(),
behavioral: NewBehavioralDetector(100),
docBridge: NewDocBridge(),
approval: NewApprovalEngine(),
events: make([]ShadowAIEvent, 0, 1000),
maxEvents: 10000,
logger: slog.Default().With("component", "shadow-ai-controller"),
}
}
// SetSOCEventEmitter sets the function used to emit events into the SOC pipeline.
func (c *ShadowAIController) SetSOCEventEmitter(fn func(source, severity, category, description string, meta map[string]string)) {
c.mu.Lock()
defer c.mu.Unlock()
c.socEventFn = fn
}
// Configure loads plugin configuration and initializes the integration layer.
func (c *ShadowAIController) Configure(config *IntegrationConfig) error {
c.mu.Lock()
defer c.mu.Unlock()
if err := c.registry.LoadPlugins(config); err != nil {
return fmt.Errorf("failed to load plugins: %w", err)
}
c.fallback = NewFallbackManager(c.registry, config.FallbackStrategy)
c.fallback.SetEventLogger(func(event ShadowAIEvent) {
c.recordEvent(event)
})
interval := config.HealthCheckInterval
if interval <= 0 {
interval = 30 * time.Second
}
c.healthChecker = NewHealthChecker(c.registry, interval, func(vendor string, status PluginStatus, msg string) {
c.emitSOCEvent("HIGH", "integration_health", msg, map[string]string{
"vendor": vendor,
"status": string(status),
})
})
return nil
}
// StartHealthChecker starts continuous plugin health monitoring.
func (c *ShadowAIController) StartHealthChecker(ctx context.Context) {
if c.healthChecker != nil {
go c.healthChecker.Start(ctx)
}
}
// ProcessNetworkEvent analyzes a network event and enforces policy.
func (c *ShadowAIController) ProcessNetworkEvent(ctx context.Context, event NetworkEvent) *ShadowAIEvent {
detected := c.netDetector.Analyze(event)
if detected == nil {
return nil
}
// Record behavioral data.
c.behavioral.RecordAccess(event.User, event.Destination, event.DataSize)
// Attempt to block.
enforcedBy, err := c.fallback.BlockDomain(ctx, event.Destination, fmt.Sprintf("Shadow AI: %s", detected.AIService))
if err != nil {
c.logger.Error("enforcement failed", "destination", event.Destination, "error", err)
}
if enforcedBy != "" {
detected.Action = "blocked"
detected.EnforcedBy = enforcedBy
} else {
detected.Action = "detected"
}
detected.ID = genEventID()
c.recordEvent(*detected)
// Emit to SOC event bus.
c.emitSOCEvent("HIGH", "shadow_ai_usage",
fmt.Sprintf("Shadow AI access detected: %s → %s", event.User, detected.AIService),
map[string]string{
"user": event.User,
"hostname": event.Hostname,
"destination": event.Destination,
"ai_service": detected.AIService,
"action": detected.Action,
"enforced_by": detected.EnforcedBy,
},
)
return detected
}
// ScanContent scans text content for AI API keys.
func (c *ShadowAIController) ScanContent(content string) string {
return c.netDetector.SignatureDB().ScanForAPIKeys(content)
}
// ManualBlock manually blocks a domain or IP.
func (c *ShadowAIController) ManualBlock(ctx context.Context, req BlockRequest) error {
switch req.TargetType {
case "domain":
_, err := c.fallback.BlockDomain(ctx, req.Target, req.Reason)
return err
case "ip":
_, err := c.fallback.BlockIP(ctx, req.Target, req.Duration, req.Reason)
return err
case "host":
_, err := c.fallback.IsolateHost(ctx, req.Target)
return err
default:
return fmt.Errorf("unsupported target type: %s", req.TargetType)
}
}
// GetStats returns aggregate shadow AI statistics.
func (c *ShadowAIController) GetStats(timeRange string) ShadowAIStats {
c.mu.RLock()
defer c.mu.RUnlock()
cutoff := parseCutoff(timeRange)
stats := ShadowAIStats{
TimeRange: timeRange,
ByService: make(map[string]int),
ByDepartment: make(map[string]int),
}
violatorMap := make(map[string]int)
for _, e := range c.events {
if e.Timestamp.Before(cutoff) {
continue
}
stats.Total++
switch e.Action {
case "blocked":
stats.Blocked++
case "allowed", "approved":
stats.Approved++
case "pending":
stats.Pending++
}
if e.AIService != "" {
stats.ByService[e.AIService]++
}
if dept, ok := e.Metadata["department"]; ok {
stats.ByDepartment[dept]++
}
if e.UserID != "" {
violatorMap[e.UserID]++
}
}
// Build top violators list (sorted desc).
for uid, count := range violatorMap {
stats.TopViolators = append(stats.TopViolators, Violator{UserID: uid, Attempts: count})
}
// Sort by attempts descending, limit to 10.
for i := 0; i < len(stats.TopViolators); i++ {
for j := i + 1; j < len(stats.TopViolators); j++ {
if stats.TopViolators[j].Attempts > stats.TopViolators[i].Attempts {
stats.TopViolators[i], stats.TopViolators[j] = stats.TopViolators[j], stats.TopViolators[i]
}
}
}
if len(stats.TopViolators) > 10 {
stats.TopViolators = stats.TopViolators[:10]
}
return stats
}
// GetEvents returns recent shadow AI events (newest first).
func (c *ShadowAIController) GetEvents(limit int) []ShadowAIEvent {
c.mu.RLock()
defer c.mu.RUnlock()
total := len(c.events)
if total == 0 {
return nil
}
start := total - limit
if start < 0 {
start = 0
}
// Return newest first.
result := make([]ShadowAIEvent, 0, limit)
for i := total - 1; i >= start; i-- {
result = append(result, c.events[i])
}
return result
}
// GetEvent returns a single event by ID.
func (c *ShadowAIController) GetEvent(id string) (*ShadowAIEvent, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
for i := len(c.events) - 1; i >= 0; i-- {
if c.events[i].ID == id {
cp := c.events[i]
return &cp, true
}
}
return nil, false
}
// IntegrationHealth returns health status of all plugins.
func (c *ShadowAIController) IntegrationHealth() []PluginHealth {
return c.registry.AllHealth()
}
// VendorHealth returns health for a specific vendor.
func (c *ShadowAIController) VendorHealth(vendor string) (*PluginHealth, bool) {
return c.registry.GetHealth(vendor)
}
// Registry returns the plugin registry for direct access.
func (c *ShadowAIController) Registry() *PluginRegistry {
return c.registry
}
// NetworkDetector returns the network detector for configuration.
func (c *ShadowAIController) NetworkDetector() *NetworkDetector {
return c.netDetector
}
// BehavioralDetector returns the behavioral detector.
func (c *ShadowAIController) BehavioralDetector() *BehavioralDetector {
return c.behavioral
}
// DocBridge returns the document review bridge.
func (c *ShadowAIController) DocBridge() *DocBridge {
return c.docBridge
}
// ApprovalEngine returns the approval workflow engine.
func (c *ShadowAIController) ApprovalEngine() *ApprovalEngine {
return c.approval
}
// ReviewDocument scans a document and creates an approval request if needed.
func (c *ShadowAIController) ReviewDocument(docID, content, userID string) (*ScanResult, *ApprovalRequest) {
result := c.docBridge.ScanDocument(docID, content, userID)
// Create approval request based on data classification.
var req *ApprovalRequest
if result.Status != DocReviewBlocked {
req = c.approval.SubmitRequest(userID, docID, result.DataClass)
}
// Emit SOC event for tracking.
c.emitSOCEvent("MEDIUM", "shadow_ai_usage",
fmt.Sprintf("Document review: %s by %s — %s (%s)",
docID, userID, result.Status, result.DataClass),
map[string]string{
"user": userID,
"doc_id": docID,
"status": string(result.Status),
"data_class": string(result.DataClass),
"pii_count": fmt.Sprintf("%d", len(result.PIIFound)),
},
)
return result, req
}
// GenerateComplianceReport generates a compliance report for the given period.
func (c *ShadowAIController) GenerateComplianceReport(period string) ComplianceReport {
stats := c.GetStats(period)
docStats := c.docBridge.Stats()
return ComplianceReport{
GeneratedAt: time.Now(),
Period: period,
TotalInteractions: stats.Total,
BlockedAttempts: stats.Blocked,
ApprovedReviews: stats.Approved,
PIIDetected: docStats["redacted"] + docStats["blocked"],
SecretsDetected: docStats["blocked"],
AuditComplete: true,
Regulations: []string{"GDPR", "SOC2", "EU AI Act Article 15"},
}
}
// --- Internal helpers ---
func (c *ShadowAIController) recordEvent(event ShadowAIEvent) {
c.mu.Lock()
defer c.mu.Unlock()
c.events = append(c.events, event)
// Evict oldest events if over capacity.
if len(c.events) > c.maxEvents {
excess := len(c.events) - c.maxEvents
c.events = c.events[excess:]
}
}
func (c *ShadowAIController) emitSOCEvent(severity, category, description string, meta map[string]string) {
c.mu.RLock()
fn := c.socEventFn
c.mu.RUnlock()
if fn != nil {
fn("shadow-ai", severity, category, description, meta)
}
}
func parseCutoff(timeRange string) time.Time {
switch timeRange {
case "1h":
return time.Now().Add(-1 * time.Hour)
case "24h":
return time.Now().Add(-24 * time.Hour)
case "7d":
return time.Now().Add(-7 * 24 * time.Hour)
case "30d":
return time.Now().Add(-30 * 24 * time.Hour)
case "90d":
return time.Now().Add(-90 * 24 * time.Hour)
default:
return time.Now().Add(-24 * time.Hour)
}
}
var eventCounter uint64
var eventCounterMu sync.Mutex
func genEventID() string {
eventCounterMu.Lock()
eventCounter++
id := eventCounter
eventCounterMu.Unlock()
return fmt.Sprintf("sai-%d-%d", time.Now().UnixMilli(), id)
}