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