mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-04-27 21:36:21 +02:00
312 lines
8.5 KiB
Go
312 lines
8.5 KiB
Go
package soc
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// === GenAI Monitor Tests ===
|
|
|
|
func TestIsGenAIProcess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
process string
|
|
expected bool
|
|
}{
|
|
{"claude detected", "claude", true},
|
|
{"cursor detected", "cursor", true},
|
|
{"Cursor Helper detected", "Cursor Helper", true},
|
|
{"copilot detected", "copilot", true},
|
|
{"windsurf detected", "windsurf", true},
|
|
{"gemini detected", "gemini", true},
|
|
{"aider detected", "aider", true},
|
|
{"codex detected", "codex", true},
|
|
{"normal process ignored", "python3", false},
|
|
{"vim ignored", "vim", false},
|
|
{"empty string ignored", "", false},
|
|
{"partial match rejected", "claud", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsGenAIProcess(tt.process)
|
|
if got != tt.expected {
|
|
t.Errorf("IsGenAIProcess(%q) = %v, want %v", tt.process, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsCredentialFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
path string
|
|
expected bool
|
|
}{
|
|
{"credentials.db", "/home/user/.config/google-chrome/Default/credentials.db", true},
|
|
{"Cookies", "/home/user/.config/chromium/Default/Cookies", true},
|
|
{"Login Data", "/home/user/.config/google-chrome/Default/Login Data", true},
|
|
{"logins.json", "/home/user/.mozilla/firefox/profile/logins.json", true},
|
|
{"ssh key", "/home/user/.ssh/id_rsa", true},
|
|
{"aws credentials", "/home/user/.aws/credentials", true},
|
|
{"env file", "/app/.env", true},
|
|
{"normal file ignored", "/home/user/document.txt", false},
|
|
{"code file ignored", "/home/user/project/main.go", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsCredentialFile(tt.path)
|
|
if got != tt.expected {
|
|
t.Errorf("IsCredentialFile(%q) = %v, want %v", tt.path, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsLLMEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
domain string
|
|
expected bool
|
|
}{
|
|
{"anthropic", "api.anthropic.com", true},
|
|
{"openai", "api.openai.com", true},
|
|
{"gemini", "gemini.googleapis.com", true},
|
|
{"deepseek", "api.deepseek.com", true},
|
|
{"normal domain", "google.com", false},
|
|
{"github", "api.github.com", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := IsLLMEndpoint(tt.domain)
|
|
if got != tt.expected {
|
|
t.Errorf("IsLLMEndpoint(%q) = %v, want %v", tt.domain, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProcessAncestryHasGenAIAncestor(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ancestry ProcessAncestry
|
|
expected bool
|
|
}{
|
|
{
|
|
"claude parent",
|
|
ProcessAncestry{ParentName: "claude", Ancestry: []string{"zsh", "login"}},
|
|
true,
|
|
},
|
|
{
|
|
"claude in ancestry chain",
|
|
ProcessAncestry{ParentName: "python3", Ancestry: []string{"claude", "zsh", "login"}},
|
|
true,
|
|
},
|
|
{
|
|
"no genai ancestor",
|
|
ProcessAncestry{ParentName: "bash", Ancestry: []string{"sshd", "login"}},
|
|
false,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := tt.ancestry.HasGenAIAncestor()
|
|
if got != tt.expected {
|
|
t.Errorf("HasGenAIAncestor() = %v, want %v", got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenAIAncestorName(t *testing.T) {
|
|
p := ProcessAncestry{ParentName: "python3", Ancestry: []string{"cursor", "zsh"}}
|
|
if name := p.GenAIAncestorName(); name != "cursor" {
|
|
t.Errorf("GenAIAncestorName() = %q, want %q", name, "cursor")
|
|
}
|
|
}
|
|
|
|
// === GenAI Rules Tests ===
|
|
|
|
func TestGenAICorrelationRulesCount(t *testing.T) {
|
|
rules := GenAICorrelationRules()
|
|
if len(rules) != 6 {
|
|
t.Errorf("GenAICorrelationRules() returned %d rules, want 6", len(rules))
|
|
}
|
|
}
|
|
|
|
func TestAllSOCCorrelationRulesCount(t *testing.T) {
|
|
rules := AllSOCCorrelationRules()
|
|
// 15 default + 2 Shadow AI + 6 GenAI = 23
|
|
if len(rules) != 23 {
|
|
t.Errorf("AllSOCCorrelationRules() returned %d rules, want 23", len(rules))
|
|
}
|
|
}
|
|
|
|
func TestGenAIChildProcessRule(t *testing.T) {
|
|
now := time.Now()
|
|
events := []SOCEvent{
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAIChildProcess,
|
|
Severity: SeverityInfo,
|
|
Timestamp: now.Add(-30 * time.Second),
|
|
Metadata: map[string]string{
|
|
"parent_process": "claude",
|
|
"child_process": "python3",
|
|
},
|
|
},
|
|
}
|
|
rules := GenAICorrelationRules()
|
|
matches := CorrelateSOCEvents(events, rules[:1]) // R1 only
|
|
if len(matches) != 1 {
|
|
t.Fatalf("expected 1 match for GenAI child process, got %d", len(matches))
|
|
}
|
|
if matches[0].Rule.ID != "SOC-CR-016" {
|
|
t.Errorf("expected SOC-CR-016, got %s", matches[0].Rule.ID)
|
|
}
|
|
}
|
|
|
|
func TestGenAISuspiciousDescendantRule(t *testing.T) {
|
|
now := time.Now()
|
|
events := []SOCEvent{
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAIChildProcess,
|
|
Severity: SeverityInfo,
|
|
Timestamp: now.Add(-3 * time.Minute),
|
|
},
|
|
{
|
|
Source: SourceImmune,
|
|
Category: "tool_abuse",
|
|
Severity: SeverityMedium,
|
|
Timestamp: now.Add(-1 * time.Minute),
|
|
},
|
|
}
|
|
rules := GenAICorrelationRules()
|
|
matches := CorrelateSOCEvents(events, rules[1:2]) // R2 only
|
|
if len(matches) != 1 {
|
|
t.Fatalf("expected 1 match for GenAI suspicious descendant, got %d", len(matches))
|
|
}
|
|
if matches[0].Rule.ID != "SOC-CR-017" {
|
|
t.Errorf("expected SOC-CR-017, got %s", matches[0].Rule.ID)
|
|
}
|
|
}
|
|
|
|
func TestGenAICredentialAccessRule(t *testing.T) {
|
|
now := time.Now()
|
|
events := []SOCEvent{
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAIChildProcess,
|
|
Severity: SeverityInfo,
|
|
Timestamp: now.Add(-1 * time.Minute),
|
|
},
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAICredentialAccess,
|
|
Severity: SeverityCritical,
|
|
Timestamp: now.Add(-30 * time.Second),
|
|
Metadata: map[string]string{
|
|
"file_path": "/home/user/.config/google-chrome/Default/Login Data",
|
|
},
|
|
},
|
|
}
|
|
rules := GenAICorrelationRules()
|
|
matches := CorrelateSOCEvents(events, rules[3:4]) // R4 only
|
|
if len(matches) != 1 {
|
|
t.Fatalf("expected 1 match for GenAI credential access, got %d", len(matches))
|
|
}
|
|
if matches[0].Rule.Severity != SeverityCritical {
|
|
t.Errorf("expected CRITICAL severity, got %s", matches[0].Rule.Severity)
|
|
}
|
|
}
|
|
|
|
func TestGenAICredentialAccessAutoKill(t *testing.T) {
|
|
match := CorrelationMatch{
|
|
Rule: SOCCorrelationRule{ID: "SOC-CR-019"},
|
|
}
|
|
action := EvaluateGenAIAutoResponse(match)
|
|
if action == nil {
|
|
t.Fatal("expected auto-response for SOC-CR-019, got nil")
|
|
}
|
|
if action.Type != "kill_process" {
|
|
t.Errorf("expected kill_process, got %s", action.Type)
|
|
}
|
|
}
|
|
|
|
func TestGenAIPersistenceRule(t *testing.T) {
|
|
now := time.Now()
|
|
events := []SOCEvent{
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAIChildProcess,
|
|
Severity: SeverityInfo,
|
|
Timestamp: now.Add(-8 * time.Minute),
|
|
},
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAIPersistence,
|
|
Severity: SeverityHigh,
|
|
Timestamp: now.Add(-2 * time.Minute),
|
|
},
|
|
}
|
|
rules := GenAICorrelationRules()
|
|
matches := CorrelateSOCEvents(events, rules[4:5]) // R5 only
|
|
if len(matches) != 1 {
|
|
t.Fatalf("expected 1 match for GenAI persistence, got %d", len(matches))
|
|
}
|
|
if matches[0].Rule.ID != "SOC-CR-020" {
|
|
t.Errorf("expected SOC-CR-020, got %s", matches[0].Rule.ID)
|
|
}
|
|
}
|
|
|
|
func TestGenAIConfigModificationRule(t *testing.T) {
|
|
now := time.Now()
|
|
events := []SOCEvent{
|
|
{
|
|
Source: SourceImmune,
|
|
Category: CategoryGenAIConfigModification,
|
|
Severity: SeverityMedium,
|
|
Timestamp: now.Add(-2 * time.Minute),
|
|
},
|
|
}
|
|
rules := GenAICorrelationRules()
|
|
matches := CorrelateSOCEvents(events, rules[5:6]) // R6 only
|
|
if len(matches) != 1 {
|
|
t.Fatalf("expected 1 match for GenAI config modification, got %d", len(matches))
|
|
}
|
|
}
|
|
|
|
func TestGenAINonGenAIProcessIgnored(t *testing.T) {
|
|
now := time.Now()
|
|
// Normal process events should not trigger GenAI rules
|
|
events := []SOCEvent{
|
|
{
|
|
Source: SourceSentinelCore,
|
|
Category: "prompt_injection",
|
|
Severity: SeverityHigh,
|
|
Timestamp: now.Add(-1 * time.Minute),
|
|
},
|
|
}
|
|
rules := GenAICorrelationRules()
|
|
matches := CorrelateSOCEvents(events, rules)
|
|
// None of the 6 GenAI rules should fire on a regular prompt_injection event
|
|
for _, m := range matches {
|
|
if m.Rule.ID >= "SOC-CR-016" && m.Rule.ID <= "SOC-CR-021" {
|
|
t.Errorf("GenAI rule %s should not fire on non-GenAI event", m.Rule.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenAINoAutoResponseForNonCredentialRules(t *testing.T) {
|
|
// Rules other than SOC-CR-019 should NOT have auto-response
|
|
nonAutoRuleIDs := []string{"SOC-CR-016", "SOC-CR-017", "SOC-CR-018", "SOC-CR-020", "SOC-CR-021"}
|
|
for _, ruleID := range nonAutoRuleIDs {
|
|
match := CorrelationMatch{
|
|
Rule: SOCCorrelationRule{ID: ruleID},
|
|
}
|
|
action := EvaluateGenAIAutoResponse(match)
|
|
if action != nil {
|
|
t.Errorf("rule %s should NOT have auto-response, got %+v", ruleID, action)
|
|
}
|
|
}
|
|
}
|