mirror of
https://github.com/syntrex-lab/gomcp.git
synced 2026-05-02 07:42:37 +02:00
Release prep: 54 engines, self-hosted signatures, i18n, dashboard updates
This commit is contained in:
parent
694e32be26
commit
41cbfd6e0a
178 changed files with 36008 additions and 399 deletions
312
internal/domain/soc/genai_rules_test.go
Normal file
312
internal/domain/soc/genai_rules_test.go
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
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 + 6 GenAI = 21
|
||||
if len(rules) != 21 {
|
||||
t.Errorf("AllSOCCorrelationRules() returned %d rules, want 21", 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue