initial: Syntrex extraction from sentinel-community (615 files)

This commit is contained in:
DmitrL-dev 2026-03-11 15:12:02 +10:00
commit 2c50c993b1
175 changed files with 32396 additions and 0 deletions

View file

@ -0,0 +1,299 @@
package httpserver
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
appsoc "github.com/sentinel-community/gomcp/internal/application/soc"
domsoc "github.com/sentinel-community/gomcp/internal/domain/soc"
"github.com/sentinel-community/gomcp/internal/infrastructure/sqlite"
)
// newTestServer creates an HTTP test server with a real SOC service backed by in-memory SQLite.
func newTestServer(t *testing.T) (*httptest.Server, *appsoc.Service) {
t.Helper()
// In-memory SQLite for SOC
db, err := sqlite.Open(":memory:")
if err != nil {
t.Fatalf("open test db: %v", err)
}
t.Cleanup(func() { db.Close() })
repo, err := sqlite.NewSOCRepo(db)
if err != nil {
t.Fatalf("create SOC repo: %v", err)
}
socSvc := appsoc.NewService(repo, nil) // no decision logger for tests
srv := New(socSvc, 0) // port 0, we use httptest
mux := http.NewServeMux()
mux.HandleFunc("GET /api/soc/dashboard", srv.handleDashboard)
mux.HandleFunc("GET /api/soc/events", srv.handleEvents)
mux.HandleFunc("GET /api/soc/incidents", srv.handleIncidents)
mux.HandleFunc("GET /api/soc/sensors", srv.handleSensors)
mux.HandleFunc("GET /api/soc/threat-intel", srv.handleThreatIntel)
mux.HandleFunc("GET /api/soc/webhook-stats", srv.handleWebhookStats)
mux.HandleFunc("GET /api/soc/analytics", srv.handleAnalytics)
mux.HandleFunc("GET /health", srv.handleHealth)
ts := httptest.NewServer(corsMiddleware(mux))
t.Cleanup(ts.Close)
return ts, socSvc
}
// TestHTTP_Dashboard_Returns200 verifies GET /api/soc/dashboard returns 200 with valid JSON.
func TestHTTP_Dashboard_Returns200(t *testing.T) {
ts, _ := newTestServer(t)
resp, err := http.Get(ts.URL + "/api/soc/dashboard")
if err != nil {
t.Fatalf("GET /api/soc/dashboard: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
// Verify JSON structure
var result map[string]any
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
// Must contain total_events key
if _, ok := result["total_events"]; !ok {
t.Error("response missing 'total_events' field")
}
// Verify CORS headers
if origin := resp.Header.Get("Access-Control-Allow-Origin"); origin != "*" {
t.Errorf("CORS: expected *, got %q", origin)
}
}
// TestHTTP_Events_WithLimit verifies GET /api/soc/events?limit=5 returns at most 5 events.
func TestHTTP_Events_WithLimit(t *testing.T) {
ts, socSvc := newTestServer(t)
// Ingest 10 events
for i := 0; i < 10; i++ {
socSvc.IngestEvent(domsoc.SOCEvent{
SensorID: "test-sensor",
Category: "test",
Severity: domsoc.SeverityLow,
Payload: "test event payload",
})
}
resp, err := http.Get(ts.URL + "/api/soc/events?limit=5")
if err != nil {
t.Fatalf("GET /api/soc/events: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var result struct {
Events []any `json:"events"`
Count int `json:"count"`
Limit int `json:"limit"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
if result.Limit != 5 {
t.Errorf("expected limit=5, got %d", result.Limit)
}
if result.Count > 5 {
t.Errorf("expected at most 5 events, got %d", result.Count)
}
}
// TestHTTP_Incidents_FilterByStatus verifies GET /api/soc/incidents?status=open returns only open incidents.
func TestHTTP_Incidents_FilterByStatus(t *testing.T) {
ts, socSvc := newTestServer(t)
// Ingest 3 correlated jailbreak events to trigger incident creation
for i := 0; i < 3; i++ {
socSvc.IngestEvent(domsoc.SOCEvent{
SensorID: "test-sensor",
Category: "jailbreak",
Severity: domsoc.SeverityCritical,
Payload: "jailbreak attempt payload",
})
}
resp, err := http.Get(ts.URL + "/api/soc/incidents?status=open")
if err != nil {
t.Fatalf("GET /api/soc/incidents: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var result struct {
Incidents []any `json:"incidents"`
Count int `json:"count"`
Status string `json:"status"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
if result.Status != "open" {
t.Errorf("expected status filter 'open', got %q", result.Status)
}
// After 3 jailbreak events, correlation should have created at least one open incident
t.Logf("incidents: count=%d, status=%s", result.Count, result.Status)
}
// TestHTTP_Health returns ok.
func TestHTTP_Health(t *testing.T) {
ts, _ := newTestServer(t)
resp, err := http.Get(ts.URL + "/health")
if err != nil {
t.Fatalf("GET /health: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var result map[string]string
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
if result["status"] != "ok" {
t.Errorf("expected status 'ok', got %q", result["status"])
}
}
// TestHTTP_Sensors_Returns200 verifies GET /api/soc/sensors returns 200.
func TestHTTP_Sensors_Returns200(t *testing.T) {
ts, socSvc := newTestServer(t)
// Ingest an event to auto-register a sensor
socSvc.IngestEvent(domsoc.SOCEvent{
SensorID: "test-sensor-001",
Source: domsoc.SourceSentinelCore,
Category: "test",
Severity: domsoc.SeverityLow,
})
resp, err := http.Get(ts.URL + "/api/soc/sensors")
if err != nil {
t.Fatalf("GET /api/soc/sensors: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var result struct {
Sensors []any `json:"sensors"`
Count int `json:"count"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
if result.Count < 1 {
t.Error("expected at least 1 sensor after event ingest")
}
t.Logf("sensors: count=%d", result.Count)
}
// TestHTTP_ThreatIntel_NotConfigured verifies threat-intel returns disabled when not configured.
func TestHTTP_ThreatIntel_NotConfigured(t *testing.T) {
ts, _ := newTestServer(t)
resp, err := http.Get(ts.URL + "/api/soc/threat-intel")
if err != nil {
t.Fatalf("GET /api/soc/threat-intel: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var result map[string]any
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
// Without SetThreatIntel, should return enabled=false
if enabled, ok := result["enabled"].(bool); !ok || enabled {
t.Error("expected enabled=false when threat intel not configured")
}
}
// TestHTTP_Analytics_Returns200 verifies GET /api/soc/analytics returns a valid report.
func TestHTTP_Analytics_Returns200(t *testing.T) {
ts, socSvc := newTestServer(t)
// Ingest some events for analytics
for i := 0; i < 5; i++ {
socSvc.IngestEvent(domsoc.SOCEvent{
SensorID: "analytics-sensor",
Source: domsoc.SourceShield,
Category: "injection",
Severity: domsoc.SeverityHigh,
})
}
resp, err := http.Get(ts.URL + "/api/soc/analytics?window=1")
if err != nil {
t.Fatalf("GET /api/soc/analytics: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
var result map[string]any
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
t.Fatalf("decode JSON: %v", err)
}
// Must have analytics fields
for _, field := range []string{"generated_at", "event_trend", "severity_distribution", "top_sources", "mttr_hours"} {
if _, ok := result[field]; !ok {
t.Errorf("response missing '%s' field", field)
}
}
t.Logf("analytics: events_per_hour=%.1f", result["events_per_hour"])
}
// TestHTTP_WebhookStats_Returns200 verifies webhook-stats endpoint works.
func TestHTTP_WebhookStats_Returns200(t *testing.T) {
ts, _ := newTestServer(t)
resp, err := http.Get(ts.URL + "/api/soc/webhook-stats")
if err != nil {
t.Fatalf("GET /api/soc/webhook-stats: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200, got %d", resp.StatusCode)
}
}