Files
livedash-node/tests/integration/security-monitoring-api.test.ts
Kaj Kowalski 1eea2cc3e4 refactor: fix biome linting issues and update project documentation
- Fix 36+ biome linting issues reducing errors/warnings from 227 to 191
- Replace explicit 'any' types with proper TypeScript interfaces
- Fix React hooks dependencies and useCallback patterns
- Resolve unused variables and parameter assignment issues
- Improve accessibility with proper label associations
- Add comprehensive API documentation for admin and security features
- Update README.md with accurate PostgreSQL setup and current tech stack
- Create complete documentation for audit logging, CSP monitoring, and batch processing
- Fix outdated project information and missing developer workflows
2025-07-12 00:28:09 +02:00

546 lines
16 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import { NextRequest } from "next/server";
import { getServerSession } from "next-auth";
import { GET, POST } from "@/app/api/admin/security-monitoring/route";
import {
GET as AlertsGET,
POST as AlertsPOST,
} from "@/app/api/admin/security-monitoring/alerts/route";
import { GET as ExportGET } from "@/app/api/admin/security-monitoring/export/route";
import { POST as ThreatAnalysisPOST } from "@/app/api/admin/security-monitoring/threat-analysis/route";
// Mock next-auth
vi.mock("next-auth", () => ({
getServerSession: vi.fn(),
}));
// Mock security monitoring
vi.mock("@/lib/securityMonitoring", () => ({
securityMonitoring: {
getSecurityMetrics: vi.fn(),
getActiveAlerts: vi.fn(),
getConfig: vi.fn(),
updateConfig: vi.fn(),
acknowledgeAlert: vi.fn(),
exportSecurityData: vi.fn(),
calculateIPThreatLevel: vi.fn(),
},
}));
// Mock security audit logger
vi.mock("@/lib/securityAuditLogger", () => ({
createAuditContext: vi.fn(),
securityAuditLogger: {
logPlatformAdmin: vi.fn(),
},
}));
const { securityMonitoring } = await import("@/lib/securityMonitoring");
const { createAuditContext, securityAuditLogger } = await import(
"@/lib/securityAuditLogger"
);
const mockPlatformUserSession = {
user: {
id: "platform-user-1",
email: "admin@platform.com",
isPlatformUser: true,
platformRole: "ADMIN",
},
};
const mockRegularUserSession = {
user: {
id: "user-1",
email: "user@company.com",
isPlatformUser: false,
companyId: "company-1",
},
};
describe("Security Monitoring API", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(createAuditContext).mockResolvedValue({
userId: "platform-user-1",
requestId: "test-request-123",
});
});
describe("GET /api/admin/security-monitoring", () => {
it("should return security metrics for platform admin", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const mockMetrics = {
totalEvents: 100,
criticalEvents: 5,
activeAlerts: 3,
resolvedAlerts: 10,
securityScore: 85,
threatLevel: "MODERATE",
eventsByType: { AUTHENTICATION: 50, RATE_LIMITING: 30 },
alertsByType: { BRUTE_FORCE_ATTACK: 2, RATE_LIMIT_BREACH: 1 },
topThreats: [{ type: "BRUTE_FORCE_ATTACK", count: 2 }],
geoDistribution: { USA: 60, GBR: 40 },
timeDistribution: Array.from({ length: 24 }, (_, i) => ({
hour: i,
count: Math.floor(Math.random() * 10),
})),
userRiskScores: [
{ userId: "user-1", email: "test@test.com", riskScore: 75 },
],
};
const mockConfig = {
thresholds: {
failedLoginsPerMinute: 5,
failedLoginsPerHour: 20,
rateLimitViolationsPerMinute: 10,
cspViolationsPerMinute: 15,
adminActionsPerHour: 25,
massDataAccessThreshold: 100,
suspiciousIPThreshold: 10,
},
alerting: {
enabled: true,
channels: ["EMAIL"],
suppressDuplicateMinutes: 10,
escalationTimeoutMinutes: 60,
},
retention: {
alertRetentionDays: 90,
metricsRetentionDays: 365,
},
};
const mockAlerts = [
{
id: "alert-1",
timestamp: new Date(),
severity: "HIGH",
type: "BRUTE_FORCE_ATTACK",
title: "Brute Force Attack Detected",
description: "Multiple failed login attempts",
eventType: "AUTHENTICATION",
context: { ipAddress: "192.168.1.100" },
metadata: {},
acknowledged: false,
},
];
vi.mocked(securityMonitoring.getSecurityMetrics).mockResolvedValue(
mockMetrics
);
vi.mocked(securityMonitoring.getConfig).mockReturnValue(mockConfig);
vi.mocked(securityMonitoring.getActiveAlerts).mockReturnValue(mockAlerts);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring"
);
const response = await GET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data).toMatchObject({
metrics: mockMetrics,
alerts: mockAlerts,
config: mockConfig,
timeRange: expect.any(Object),
});
expect(securityAuditLogger.logPlatformAdmin).toHaveBeenCalledWith(
"security_monitoring_access",
"SUCCESS",
expect.any(Object)
);
});
it("should reject non-platform users", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockRegularUserSession);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring"
);
const response = await GET(request);
expect(response.status).toBe(403);
const data = await response.json();
expect(data.error).toBe("Forbidden");
});
it("should reject unauthenticated requests", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring"
);
const response = await GET(request);
expect(response.status).toBe(401);
const data = await response.json();
expect(data.error).toBe("Unauthorized");
});
it("should handle query parameters correctly", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
vi.mocked(securityMonitoring.getSecurityMetrics).mockResolvedValue(
{} as any
);
vi.mocked(securityMonitoring.getConfig).mockReturnValue({} as any);
vi.mocked(securityMonitoring.getActiveAlerts).mockReturnValue([]);
const url =
"http://localhost:3000/api/admin/security-monitoring?startDate=2024-01-01T00:00:00Z&endDate=2024-01-02T00:00:00Z&companyId=company-1&severity=HIGH";
const request = new NextRequest(url);
const response = await GET(request);
expect(response.status).toBe(200);
expect(securityMonitoring.getSecurityMetrics).toHaveBeenCalledWith(
{
start: new Date("2024-01-01T00:00:00Z"),
end: new Date("2024-01-02T00:00:00Z"),
},
"company-1"
);
expect(securityMonitoring.getActiveAlerts).toHaveBeenCalledWith("HIGH");
});
});
describe("POST /api/admin/security-monitoring", () => {
it("should update security configuration", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const newConfig = {
thresholds: {
failedLoginsPerMinute: 3,
failedLoginsPerHour: 15,
},
alerting: {
enabled: false,
channels: ["EMAIL", "SLACK"],
},
};
const updatedConfig = {
thresholds: {
failedLoginsPerMinute: 3,
failedLoginsPerHour: 15,
rateLimitViolationsPerMinute: 10,
cspViolationsPerMinute: 15,
adminActionsPerHour: 25,
massDataAccessThreshold: 100,
suspiciousIPThreshold: 10,
},
alerting: {
enabled: false,
channels: ["EMAIL", "SLACK"],
suppressDuplicateMinutes: 10,
escalationTimeoutMinutes: 60,
},
retention: {
alertRetentionDays: 90,
metricsRetentionDays: 365,
},
};
vi.mocked(securityMonitoring.getConfig).mockReturnValue(updatedConfig);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring",
{
method: "POST",
body: JSON.stringify(newConfig),
headers: { "Content-Type": "application/json" },
}
);
const response = await POST(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
expect(data.config).toEqual(updatedConfig);
expect(securityMonitoring.updateConfig).toHaveBeenCalledWith(newConfig);
expect(securityAuditLogger.logPlatformAdmin).toHaveBeenCalledWith(
"security_monitoring_config_update",
"SUCCESS",
expect.any(Object),
undefined,
{ configChanges: newConfig }
);
});
it("should validate configuration input", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const invalidConfig = {
thresholds: {
failedLoginsPerMinute: -1, // Invalid: negative number
failedLoginsPerHour: 2000, // Invalid: too large
},
};
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring",
{
method: "POST",
body: JSON.stringify(invalidConfig),
headers: { "Content-Type": "application/json" },
}
);
const response = await POST(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("Invalid configuration");
expect(data.details).toBeDefined();
});
});
describe("GET /api/admin/security-monitoring/alerts", () => {
it("should return filtered alerts", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const mockAlerts = [
{
id: "alert-1",
timestamp: new Date().toISOString(),
severity: "HIGH",
type: "BRUTE_FORCE_ATTACK",
title: "Brute Force Attack",
description: "Multiple failed logins",
eventType: "AUTHENTICATION",
context: {},
metadata: {},
acknowledged: false,
},
{
id: "alert-2",
timestamp: new Date().toISOString(),
severity: "MEDIUM",
type: "RATE_LIMIT_BREACH",
title: "Rate Limit Exceeded",
description: "Too many requests",
eventType: "RATE_LIMITING",
context: {},
metadata: {},
acknowledged: false,
},
];
vi.mocked(securityMonitoring.getActiveAlerts).mockReturnValue(mockAlerts);
const url =
"http://localhost:3000/api/admin/security-monitoring/alerts?severity=HIGH&limit=10&offset=0";
const request = new NextRequest(url);
const response = await AlertsGET(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.alerts).toEqual(mockAlerts);
expect(data.total).toBe(2);
expect(data.limit).toBe(10);
expect(data.offset).toBe(0);
expect(securityMonitoring.getActiveAlerts).toHaveBeenCalledWith("HIGH");
});
});
describe("POST /api/admin/security-monitoring/alerts", () => {
it("should acknowledge alert", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
vi.mocked(securityMonitoring.acknowledgeAlert).mockResolvedValue(true);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring/alerts",
{
method: "POST",
body: JSON.stringify({
alertId: "alert-123",
action: "acknowledge",
}),
headers: { "Content-Type": "application/json" },
}
);
const response = await AlertsPOST(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.success).toBe(true);
expect(securityMonitoring.acknowledgeAlert).toHaveBeenCalledWith(
"alert-123",
"platform-user-1"
);
});
it("should handle non-existent alert", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
vi.mocked(securityMonitoring.acknowledgeAlert).mockResolvedValue(false);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring/alerts",
{
method: "POST",
body: JSON.stringify({
alertId: "non-existent",
action: "acknowledge",
}),
headers: { "Content-Type": "application/json" },
}
);
const response = await AlertsPOST(request);
expect(response.status).toBe(404);
const data = await response.json();
expect(data.error).toBe("Alert not found");
});
});
describe("GET /api/admin/security-monitoring/export", () => {
it("should export security data as JSON", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const mockExportData = JSON.stringify([
{
id: "alert-1",
timestamp: "2024-01-01T00:00:00.000Z",
severity: "HIGH",
type: "BRUTE_FORCE_ATTACK",
title: "Test Alert",
description: "Test Description",
},
]);
vi.mocked(securityMonitoring.exportSecurityData).mockReturnValue(
mockExportData
);
const url =
"http://localhost:3000/api/admin/security-monitoring/export?format=json&type=alerts&startDate=2024-01-01T00:00:00Z&endDate=2024-01-02T00:00:00Z";
const request = new NextRequest(url);
const response = await ExportGET(request);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("application/json");
expect(response.headers.get("Content-Disposition")).toContain(
"attachment"
);
const data = await response.text();
expect(data).toBe(mockExportData);
});
it("should export security data as CSV", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const mockCsvData =
"timestamp,severity,type,title\n2024-01-01T00:00:00.000Z,HIGH,BRUTE_FORCE_ATTACK,Test Alert";
vi.mocked(securityMonitoring.exportSecurityData).mockReturnValue(
mockCsvData
);
const url =
"http://localhost:3000/api/admin/security-monitoring/export?format=csv&type=alerts&startDate=2024-01-01T00:00:00Z&endDate=2024-01-02T00:00:00Z";
const request = new NextRequest(url);
const response = await ExportGET(request);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/csv");
const data = await response.text();
expect(data).toBe(mockCsvData);
});
});
describe("POST /api/admin/security-monitoring/threat-analysis", () => {
it("should perform IP threat analysis", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const mockThreatAnalysis = {
threatLevel: "HIGH",
riskFactors: ["Multiple failed logins", "Rate limit violations"],
recommendations: ["Block IP address", "Investigate source"],
};
const mockMetrics = {
securityScore: 65,
threatLevel: "HIGH",
activeAlerts: 5,
criticalEvents: 2,
topThreats: [],
geoDistribution: {},
userRiskScores: [],
};
vi.mocked(securityMonitoring.calculateIPThreatLevel).mockResolvedValue(
mockThreatAnalysis
);
vi.mocked(securityMonitoring.getSecurityMetrics).mockResolvedValue(
mockMetrics
);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring/threat-analysis",
{
method: "POST",
body: JSON.stringify({
ipAddress: "192.168.1.100",
}),
headers: { "Content-Type": "application/json" },
}
);
const response = await ThreatAnalysisPOST(request);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.ipThreatAnalysis).toMatchObject({
ipAddress: "192.168.1.100",
...mockThreatAnalysis,
});
expect(data.overallThreatLandscape).toBeDefined();
expect(securityMonitoring.calculateIPThreatLevel).toHaveBeenCalledWith(
"192.168.1.100"
);
});
it("should validate IP address format", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockPlatformUserSession);
const request = new NextRequest(
"http://localhost:3000/api/admin/security-monitoring/threat-analysis",
{
method: "POST",
body: JSON.stringify({
ipAddress: "invalid-ip",
}),
headers: { "Content-Type": "application/json" },
}
);
const response = await ThreatAnalysisPOST(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("Invalid request");
expect(data.details).toBeDefined();
});
});
});