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
This commit is contained in:
2025-07-11 21:50:53 +02:00
committed by Kaj Kowalski
parent 3e9e75e854
commit 1eea2cc3e4
121 changed files with 28687 additions and 4895 deletions

View File

@ -0,0 +1,487 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { NextRequest } from "next/server";
import { getServerSession } from "next-auth/next";
import { GET } from "../../app/api/admin/audit-logs/route";
import {
GET as RetentionGET,
POST as RetentionPOST,
} from "../../app/api/admin/audit-logs/retention/route";
import { prisma } from "../../lib/prisma";
// Mock dependencies
vi.mock("next-auth/next");
vi.mock("../../lib/prisma", () => ({
prisma: {
securityAuditLog: {
findMany: vi.fn(),
count: vi.fn(),
create: vi.fn(),
groupBy: vi.fn(),
findFirst: vi.fn(),
deleteMany: vi.fn(),
},
},
}));
vi.mock("../../lib/rateLimiter", () => ({
extractClientIP: vi.fn().mockReturnValue("192.168.1.1"),
}));
vi.mock("../../lib/securityAuditLogger", () => ({
securityAuditLogger: {
logAuthorization: vi.fn(),
logDataPrivacy: vi.fn(),
log: vi.fn(),
},
AuditOutcome: {
SUCCESS: "SUCCESS",
FAILURE: "FAILURE",
BLOCKED: "BLOCKED",
},
createAuditMetadata: vi.fn((data) => data),
}));
vi.mock("../../lib/auditLogRetention", () => ({
AuditLogRetentionManager: vi.fn().mockImplementation(() => ({
getRetentionStatistics: vi.fn().mockResolvedValue({
totalLogs: 1000,
logsByEventType: { AUTHENTICATION: 600, AUTHORIZATION: 400 },
logsBySeverity: { INFO: 700, MEDIUM: 250, HIGH: 50 },
logsByAge: [
{ age: "Last 24 hours", count: 50 },
{ age: "Last 7 days", count: 200 },
],
oldestLog: new Date("2023-01-01"),
newestLog: new Date("2024-01-15"),
}),
validateRetentionPolicies: vi.fn().mockResolvedValue({
valid: true,
errors: [],
warnings: [],
}),
})),
DEFAULT_RETENTION_POLICIES: [
{ name: "Test Policy", maxAgeDays: 365, archiveBeforeDelete: true },
],
executeScheduledRetention: vi.fn().mockResolvedValue({
totalProcessed: 100,
totalDeleted: 50,
totalArchived: 50,
policyResults: [],
}),
}));
vi.mock("../../lib/auditLogScheduler", () => ({
auditLogScheduler: {
getStatus: vi.fn().mockReturnValue({
isRunning: true,
nextExecution: new Date("2024-01-16T02:00:00Z"),
schedule: "0 2 * * 0",
}),
},
}));
const createMockRequest = (url: string, options: RequestInit = {}) => {
return new NextRequest(url, {
headers: {
"user-agent": "Test Agent",
"x-forwarded-for": "192.168.1.1",
...options.headers,
},
...options,
});
};
const mockSession = {
user: {
id: "user-123",
email: "admin@company.com",
role: "ADMIN",
companyId: "company-456",
},
};
const mockUserSession = {
user: {
id: "user-456",
email: "user@company.com",
role: "USER",
companyId: "company-456",
},
};
describe("Audit Logs API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("GET /api/admin/audit-logs", () => {
it("should return audit logs for admin users", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockResolvedValue([
{
id: "log-1",
eventType: "AUTHENTICATION",
action: "login_success",
outcome: "SUCCESS",
severity: "INFO",
timestamp: new Date("2024-01-15T10:00:00Z"),
userId: "user-123",
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
metadata: { browser: "Chrome" },
user: {
id: "user-123",
email: "user@company.com",
name: "Test User",
role: "USER",
},
platformUser: null,
},
]);
vi.mocked(prisma.securityAuditLog.count).mockResolvedValue(1);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs?page=1&limit=50"
);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data.auditLogs).toHaveLength(1);
expect(data.data.pagination).toEqual({
page: 1,
limit: 50,
totalCount: 1,
totalPages: 1,
hasNext: false,
hasPrev: false,
});
});
it("should filter audit logs by event type", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockResolvedValue([]);
vi.mocked(prisma.securityAuditLog.count).mockResolvedValue(0);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs?eventType=AUTHENTICATION"
);
await GET(request);
expect(prisma.securityAuditLog.findMany).toHaveBeenCalledWith({
where: expect.objectContaining({
eventType: "AUTHENTICATION",
}),
skip: 0,
take: 50,
orderBy: { timestamp: "desc" },
include: expect.any(Object),
});
});
it("should filter audit logs by date range", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockResolvedValue([]);
vi.mocked(prisma.securityAuditLog.count).mockResolvedValue(0);
const startDate = "2024-01-01T00:00:00Z";
const endDate = "2024-01-31T23:59:59Z";
const request = createMockRequest(
`http://localhost:3000/api/admin/audit-logs?startDate=${startDate}&endDate=${endDate}`
);
await GET(request);
expect(prisma.securityAuditLog.findMany).toHaveBeenCalledWith({
where: expect.objectContaining({
timestamp: {
gte: new Date(startDate),
lte: new Date(endDate),
},
}),
skip: 0,
take: 50,
orderBy: { timestamp: "desc" },
include: expect.any(Object),
});
});
it("should reject unauthorized users", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs"
);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.success).toBe(false);
expect(data.error).toBe("Unauthorized");
});
it("should reject non-admin users", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockUserSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs"
);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.success).toBe(false);
expect(data.error).toBe("Insufficient permissions");
});
it("should handle server errors gracefully", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockRejectedValue(
new Error("Database error")
);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs"
);
const response = await GET(request);
const data = await response.json();
expect(response.status).toBe(500);
expect(data.success).toBe(false);
expect(data.error).toBe("Internal server error");
});
it("should enforce pagination limits", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockResolvedValue([]);
vi.mocked(prisma.securityAuditLog.count).mockResolvedValue(0);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs?limit=200"
);
await GET(request);
expect(prisma.securityAuditLog.findMany).toHaveBeenCalledWith(
expect.objectContaining({
take: 100, // Should be capped at 100
})
);
});
it("should scope logs to user's company", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockResolvedValue([]);
vi.mocked(prisma.securityAuditLog.count).mockResolvedValue(0);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs"
);
await GET(request);
expect(prisma.securityAuditLog.findMany).toHaveBeenCalledWith({
where: expect.objectContaining({
companyId: "company-456",
}),
skip: 0,
take: 50,
orderBy: { timestamp: "desc" },
include: expect.any(Object),
});
});
});
describe("GET /api/admin/audit-logs/retention", () => {
it("should return retention statistics for admin users", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention"
);
const response = await RetentionGET(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data.statistics).toBeDefined();
expect(data.data.policies).toBeDefined();
expect(data.data.policyValidation).toBeDefined();
expect(data.data.scheduler).toBeDefined();
});
it("should reject non-admin users", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockUserSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention"
);
const response = await RetentionGET(request);
const data = await response.json();
expect(response.status).toBe(403);
expect(data.success).toBe(false);
});
});
describe("POST /api/admin/audit-logs/retention", () => {
it("should execute retention policies in dry run mode", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "execute", isDryRun: true }),
}
);
const response = await RetentionPOST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data.isDryRun).toBe(true);
expect(data.data.message).toContain("Dry run completed");
});
it("should execute retention policies in production mode", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "execute", isDryRun: false }),
}
);
const response = await RetentionPOST(request);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.success).toBe(true);
expect(data.data.isDryRun).toBe(false);
expect(data.data.message).toContain("executed successfully");
});
it("should reject invalid actions", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "invalid" }),
}
);
const response = await RetentionPOST(request);
const data = await response.json();
expect(response.status).toBe(400);
expect(data.success).toBe(false);
expect(data.error).toContain("Invalid action");
});
it("should reject unauthorized users", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockUserSession);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "execute" }),
}
);
const response = await RetentionPOST(request);
const data = await response.json();
expect(response.status).toBe(401);
expect(data.success).toBe(false);
});
});
describe("Audit Log Integration", () => {
it("should log access to audit logs", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
vi.mocked(prisma.securityAuditLog.findMany).mockResolvedValue([]);
vi.mocked(prisma.securityAuditLog.count).mockResolvedValue(0);
const { securityAuditLogger } = await import(
"../../lib/securityAuditLogger"
);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs"
);
await GET(request);
expect(securityAuditLogger.logDataPrivacy).toHaveBeenCalledWith(
"audit_logs_accessed",
"SUCCESS",
expect.objectContaining({
userId: "user-123",
companyId: "company-456",
ipAddress: "192.168.1.1",
}),
"Audit logs accessed by admin user"
);
});
it("should log retention policy execution", async () => {
vi.mocked(getServerSession).mockResolvedValue(mockSession);
const { securityAuditLogger } = await import(
"../../lib/securityAuditLogger"
);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs/retention",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "execute", isDryRun: true }),
}
);
await RetentionPOST(request);
expect(securityAuditLogger.logDataPrivacy).toHaveBeenCalledWith(
"audit_retention_manual_execution",
"SUCCESS",
expect.objectContaining({
userId: "user-123",
companyId: "company-456",
}),
expect.stringContaining("Admin manually triggered audit retention")
);
});
it("should log unauthorized access attempts", async () => {
vi.mocked(getServerSession).mockResolvedValue(null);
const { securityAuditLogger } = await import(
"../../lib/securityAuditLogger"
);
const request = createMockRequest(
"http://localhost:3000/api/admin/audit-logs"
);
await GET(request);
expect(securityAuditLogger.logAuthorization).toHaveBeenCalledWith(
"audit_logs_unauthorized_access",
"BLOCKED",
expect.objectContaining({
ipAddress: "192.168.1.1",
}),
"Unauthorized attempt to access audit logs"
);
});
});
});

View File

@ -0,0 +1,283 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { NextRequest } from "next/server";
import { middleware } from "@/middleware";
// Mock the CSP utilities
vi.mock("@/lib/csp", () => ({
buildCSP: vi.fn(({ nonce, isDevelopment, reportUri }) => {
const base = "default-src 'self'; object-src 'none'";
const script = isDevelopment
? "script-src 'self' 'unsafe-eval' 'unsafe-inline'"
: nonce
? `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`
: "script-src 'self'";
const style = nonce
? `style-src 'self' 'nonce-${nonce}'`
: "style-src 'self' 'unsafe-inline'";
const report = reportUri ? `report-uri ${reportUri}` : "";
return [base, script, style, report].filter(Boolean).join("; ");
}),
generateNonce: vi.fn(() => "test-nonce-12345"),
}));
describe("CSP Middleware Integration", () => {
let originalEnv: string | undefined;
beforeEach(() => {
originalEnv = process.env.NODE_ENV;
vi.clearAllMocks();
});
afterEach(() => {
process.env.NODE_ENV = originalEnv;
});
function createMockRequest(url: string, options: RequestInit = {}) {
return new NextRequest(url, options);
}
describe("Route Filtering", () => {
it("should skip CSP for API routes (except csp-report)", async () => {
const request = createMockRequest("https://example.com/api/auth/signin");
const response = await middleware(request);
expect(response.headers.get("Content-Security-Policy")).toBeNull();
});
it("should apply CSP to csp-report endpoint", async () => {
const request = createMockRequest("https://example.com/api/csp-report");
const response = await middleware(request);
expect(response.headers.get("Content-Security-Policy")).toBeTruthy();
});
it("should skip CSP for static assets", async () => {
const staticAssets = [
"https://example.com/_next/static/chunks/main.js",
"https://example.com/_next/image/favicon.ico",
"https://example.com/favicon.ico",
"https://example.com/logo.png",
];
for (const url of staticAssets) {
const request = createMockRequest(url);
const response = await middleware(request);
expect(response.headers.get("Content-Security-Policy")).toBeNull();
}
});
it("should apply CSP to page routes", async () => {
const pageRoutes = [
"https://example.com/",
"https://example.com/dashboard",
"https://example.com/platform/settings",
];
for (const url of pageRoutes) {
const request = createMockRequest(url);
const response = await middleware(request);
expect(response.headers.get("Content-Security-Policy")).toBeTruthy();
}
});
});
describe("Development vs Production CSP", () => {
it("should use permissive CSP in development", async () => {
process.env.NODE_ENV = "development";
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const csp = response.headers.get("Content-Security-Policy");
expect(csp).toContain("'unsafe-eval'");
expect(csp).toContain("'unsafe-inline'");
});
it("should use strict CSP in production", async () => {
process.env.NODE_ENV = "production";
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const csp = response.headers.get("Content-Security-Policy");
expect(csp).toContain("'nonce-test-nonce-12345'");
expect(csp).toContain("'strict-dynamic'");
expect(csp).not.toContain("'unsafe-eval'");
});
});
describe("Security Headers", () => {
it("should set all required security headers", async () => {
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const expectedHeaders = [
"Content-Security-Policy",
"X-Nonce",
"X-Content-Type-Options",
"X-Frame-Options",
"X-XSS-Protection",
"Referrer-Policy",
"X-DNS-Prefetch-Control",
"Permissions-Policy",
"X-Permitted-Cross-Domain-Policies",
"Cross-Origin-Embedder-Policy",
"Cross-Origin-Opener-Policy",
"Cross-Origin-Resource-Policy",
];
for (const header of expectedHeaders) {
expect(response.headers.get(header)).toBeTruthy();
}
});
it("should set HSTS only in production", async () => {
// Test development
process.env.NODE_ENV = "development";
let request = createMockRequest("https://example.com/");
let response = await middleware(request);
expect(response.headers.get("Strict-Transport-Security")).toBeNull();
// Test production
process.env.NODE_ENV = "production";
request = createMockRequest("https://example.com/");
response = await middleware(request);
expect(response.headers.get("Strict-Transport-Security")).toBeTruthy();
});
it("should set correct header values", async () => {
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
expect(response.headers.get("X-Frame-Options")).toBe("DENY");
expect(response.headers.get("X-XSS-Protection")).toBe("1; mode=block");
expect(response.headers.get("Referrer-Policy")).toBe(
"strict-origin-when-cross-origin"
);
expect(response.headers.get("X-DNS-Prefetch-Control")).toBe("off");
});
it("should set enhanced Permissions Policy", async () => {
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const permissionsPolicy = response.headers.get("Permissions-Policy");
// Check for restrictive permissions
expect(permissionsPolicy).toContain("camera=()");
expect(permissionsPolicy).toContain("microphone=()");
expect(permissionsPolicy).toContain("geolocation=()");
expect(permissionsPolicy).toContain("payment=()");
expect(permissionsPolicy).toContain("usb=()");
expect(permissionsPolicy).toContain("bluetooth=()");
// Check for allowed permissions
expect(permissionsPolicy).toContain("fullscreen=(self)");
expect(permissionsPolicy).toContain("web-share=(self)");
expect(permissionsPolicy).toContain("autoplay=(self)");
});
it("should set CORP headers correctly", async () => {
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
expect(response.headers.get("Cross-Origin-Embedder-Policy")).toBe(
"require-corp"
);
expect(response.headers.get("Cross-Origin-Opener-Policy")).toBe(
"same-origin"
);
expect(response.headers.get("Cross-Origin-Resource-Policy")).toBe(
"same-origin"
);
});
});
describe("Nonce Generation", () => {
it("should generate and set nonce header", async () => {
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const nonce = response.headers.get("X-Nonce");
expect(nonce).toBe("test-nonce-12345");
});
it("should include nonce in CSP", async () => {
process.env.NODE_ENV = "production";
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const csp = response.headers.get("Content-Security-Policy");
expect(csp).toContain("'nonce-test-nonce-12345'");
});
});
describe("CSP Report URI", () => {
it("should include report URI in CSP", async () => {
const request = createMockRequest("https://example.com/");
const response = await middleware(request);
const csp = response.headers.get("Content-Security-Policy");
expect(csp).toContain("report-uri /api/csp-report");
});
});
describe("Edge Cases", () => {
it("should handle requests without proper URL", async () => {
const request = createMockRequest("https://example.com");
const response = await middleware(request);
// Should not throw and should return a response
expect(response).toBeTruthy();
expect(response.headers.get("Content-Security-Policy")).toBeTruthy();
});
it("should handle multiple middleware calls", async () => {
const request = createMockRequest("https://example.com/");
const response1 = await middleware(request);
const response2 = await middleware(request);
// Both should have CSP headers
expect(response1.headers.get("Content-Security-Policy")).toBeTruthy();
expect(response2.headers.get("Content-Security-Policy")).toBeTruthy();
// Nonces should be different (new nonce per request)
expect(response1.headers.get("X-Nonce")).toBe("test-nonce-12345");
expect(response2.headers.get("X-Nonce")).toBe("test-nonce-12345");
});
});
describe("Performance", () => {
it("should process requests quickly", async () => {
const start = Date.now();
const request = createMockRequest("https://example.com/");
await middleware(request);
const duration = Date.now() - start;
expect(duration).toBeLessThan(100); // Should complete in under 100ms
});
it("should handle concurrent requests", async () => {
const requests = Array.from({ length: 10 }, (_, i) =>
createMockRequest(`https://example.com/page-${i}`)
);
const responses = await Promise.all(
requests.map((req) => middleware(req))
);
// All should have CSP headers
responses.forEach((response) => {
expect(response.headers.get("Content-Security-Policy")).toBeTruthy();
expect(response.headers.get("X-Nonce")).toBeTruthy();
});
});
});
});

View File

@ -0,0 +1,445 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { POST, OPTIONS } from "@/app/api/csp-report/route";
import { NextRequest } from "next/server";
// Mock rate limiter
vi.mock("@/lib/rateLimiter", () => ({
rateLimiter: {
check: vi.fn(() => Promise.resolve({ success: true, remaining: 9 })),
},
}));
// Mock CSP utilities
vi.mock("@/lib/csp", () => ({
parseCSPViolation: vi.fn((report) => ({
directive: report["csp-report"]["violated-directive"],
blockedUri: report["csp-report"]["blocked-uri"],
sourceFile: report["csp-report"]["source-file"],
lineNumber: report["csp-report"]["line-number"],
isInlineViolation: report["csp-report"]["blocked-uri"] === "inline",
isCritical:
report["csp-report"]["violated-directive"].startsWith("script-src"),
})),
detectCSPBypass: vi.fn((content) => ({
isDetected: content.includes("javascript:"),
patterns: content.includes("javascript:") ? ["javascript:"] : [],
riskLevel: content.includes("javascript:") ? "high" : "low",
})),
}));
import { rateLimiter } from "@/lib/rateLimiter";
import { parseCSPViolation, detectCSPBypass } from "@/lib/csp";
describe("CSP Report Endpoint", () => {
let originalEnv: string | undefined;
let consoleSpy: any;
beforeEach(() => {
originalEnv = process.env.NODE_ENV;
consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
vi.clearAllMocks();
});
afterEach(() => {
process.env.NODE_ENV = originalEnv;
consoleSpy.mockRestore();
});
function createCSPRequest(body: any, options: Partial<RequestInit> = {}) {
return new NextRequest("https://example.com/api/csp-report", {
method: "POST",
headers: {
"content-type": "application/csp-report",
"x-forwarded-for": "192.168.1.1",
"user-agent": "Mozilla/5.0 Test Browser",
...options.headers,
},
body: JSON.stringify(body),
...options,
});
}
describe("POST /api/csp-report", () => {
it("should accept valid CSP reports", async () => {
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "https://example.com/",
"violated-directive": "script-src 'self'",
"original-policy": "default-src 'self'; script-src 'self'",
"blocked-uri": "https://evil.com/script.js",
"source-file": "https://example.com/page",
"line-number": 42,
"column-number": 15,
},
};
const request = createCSPRequest(report);
const response = await POST(request);
expect(response.status).toBe(204);
expect(parseCSPViolation).toHaveBeenCalledWith(report);
expect(detectCSPBypass).toHaveBeenCalled();
});
it("should handle rate limiting", async () => {
vi.mocked(rateLimiter.check).mockResolvedValueOnce({
success: false,
remaining: 0,
});
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "script-src 'self'",
"original-policy": "",
"blocked-uri": "inline",
},
};
const request = createCSPRequest(report);
const response = await POST(request);
expect(response.status).toBe(429);
const data = await response.json();
expect(data.error).toBe("Too many CSP reports");
});
it("should validate content type", async () => {
const report = { "csp-report": {} };
const request = createCSPRequest(report, {
headers: { "content-type": "text/plain" },
});
const response = await POST(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("Invalid content type");
});
it("should accept application/json content type", async () => {
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "img-src 'self'",
"original-policy": "",
"blocked-uri": "https://evil.com/image.jpg",
},
};
const request = createCSPRequest(report, {
headers: { "content-type": "application/json" },
});
const response = await POST(request);
expect(response.status).toBe(204);
});
it("should validate report format", async () => {
const invalidReport = { invalid: "report" };
const request = createCSPRequest(invalidReport);
const response = await POST(request);
expect(response.status).toBe(400);
const data = await response.json();
expect(data.error).toBe("Invalid CSP report format");
});
it("should log violations in development", async () => {
process.env.NODE_ENV = "development";
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "script-src 'self'",
"original-policy": "",
"blocked-uri": "inline",
},
};
const request = createCSPRequest(report);
await POST(request);
expect(consoleSpy).toHaveBeenCalledWith(
"🚨 CSP Violation Detected:",
expect.any(Object)
);
});
it("should detect and alert critical violations", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.mocked(parseCSPViolation).mockReturnValueOnce({
directive: "script-src 'self'",
blockedUri: "https://evil.com/script.js",
sourceFile: "https://example.com/page",
lineNumber: 42,
isInlineViolation: false,
isCritical: true,
});
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "script-src 'self'",
"original-policy": "",
"blocked-uri": "https://evil.com/script.js",
},
};
const request = createCSPRequest(report);
await POST(request);
expect(errorSpy).toHaveBeenCalledWith(
"🔴 CRITICAL CSP VIOLATION:",
expect.objectContaining({
directive: "script-src 'self'",
blockedUri: "https://evil.com/script.js",
isBypassAttempt: false,
riskLevel: "low",
})
);
errorSpy.mockRestore();
});
it("should detect bypass attempts and alert", async () => {
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.mocked(detectCSPBypass).mockReturnValueOnce({
isDetected: true,
patterns: ["javascript:"],
riskLevel: "high",
});
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "script-src 'self'",
"original-policy": "",
"blocked-uri": "javascript:alert(1)",
"script-sample": "javascript:alert(1)",
},
};
const request = createCSPRequest(report);
await POST(request);
expect(errorSpy).toHaveBeenCalledWith(
"🔴 CRITICAL CSP VIOLATION:",
expect.objectContaining({
isBypassAttempt: true,
riskLevel: "high",
})
);
errorSpy.mockRestore();
});
it("should handle malformed JSON", async () => {
const request = new NextRequest("https://example.com/api/csp-report", {
method: "POST",
headers: {
"content-type": "application/csp-report",
"x-forwarded-for": "192.168.1.1",
},
body: "invalid json{",
});
const response = await POST(request);
expect(response.status).toBe(500);
const data = await response.json();
expect(data.error).toBe("Failed to process report");
});
it("should extract IP from different headers", async () => {
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "img-src 'self'",
"original-policy": "",
"blocked-uri": "https://evil.com/image.jpg",
},
};
// Test with request.ip
const requestWithIp = new NextRequest(
"https://example.com/api/csp-report",
{
method: "POST",
headers: { "content-type": "application/csp-report" },
body: JSON.stringify(report),
}
);
Object.defineProperty(requestWithIp, "ip", { value: "10.0.0.1" });
let response = await POST(requestWithIp);
expect(response.status).toBe(204);
// Test with x-forwarded-for header
const requestWithHeader = createCSPRequest(report, {
headers: {
"content-type": "application/csp-report",
"x-forwarded-for": "203.0.113.1",
},
});
response = await POST(requestWithHeader);
expect(response.status).toBe(204);
// Verify rate limiting was called with correct IPs
expect(rateLimiter.check).toHaveBeenCalledWith(
"csp-report:10.0.0.1",
10,
60000
);
expect(rateLimiter.check).toHaveBeenCalledWith(
"csp-report:203.0.113.1",
10,
60000
);
});
it("should handle missing IP gracefully", async () => {
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "img-src 'self'",
"original-policy": "",
"blocked-uri": "https://evil.com/image.jpg",
},
};
const request = new NextRequest("https://example.com/api/csp-report", {
method: "POST",
headers: { "content-type": "application/csp-report" },
body: JSON.stringify(report),
});
const response = await POST(request);
expect(response.status).toBe(204);
// Should use "unknown" as fallback IP
expect(rateLimiter.check).toHaveBeenCalledWith(
"csp-report:unknown",
10,
60000
);
});
});
describe("OPTIONS /api/csp-report", () => {
it("should handle preflight requests", async () => {
const response = await OPTIONS();
expect(response.status).toBe(200);
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
expect(response.headers.get("Access-Control-Allow-Methods")).toBe(
"POST, OPTIONS"
);
expect(response.headers.get("Access-Control-Allow-Headers")).toBe(
"Content-Type"
);
});
});
describe("Error Handling", () => {
it("should handle rate limiter errors gracefully", async () => {
vi.mocked(rateLimiter.check).mockRejectedValueOnce(
new Error("Redis error")
);
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "script-src 'self'",
"original-policy": "",
"blocked-uri": "inline",
},
};
const request = createCSPRequest(report);
const response = await POST(request);
expect(response.status).toBe(500);
const data = await response.json();
expect(data.error).toBe("Failed to process report");
});
it("should handle CSP parsing errors gracefully", async () => {
vi.mocked(parseCSPViolation).mockImplementationOnce(() => {
throw new Error("Parsing error");
});
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "script-src 'self'",
"original-policy": "",
"blocked-uri": "inline",
},
};
const request = createCSPRequest(report);
const response = await POST(request);
expect(response.status).toBe(500);
const data = await response.json();
expect(data.error).toBe("Failed to process report");
});
});
describe("Security", () => {
it("should rate limit per IP", async () => {
const report = {
"csp-report": {
"document-uri": "https://example.com/page",
referrer: "",
"violated-directive": "img-src 'self'",
"original-policy": "",
"blocked-uri": "https://evil.com/image.jpg",
},
};
const request = createCSPRequest(report);
await POST(request);
expect(rateLimiter.check).toHaveBeenCalledWith(
"csp-report:192.168.1.1",
10,
60000
);
});
it("should validate report structure to prevent injection", async () => {
const maliciousReport = {
"csp-report": {
"document-uri": "<script>alert('xss')</script>",
referrer: "javascript:alert('xss')",
"violated-directive": "eval('malicious')",
"original-policy": "",
"blocked-uri": "data:text/html,<script>alert(1)</script>",
},
};
const request = createCSPRequest(maliciousReport);
const response = await POST(request);
// Should still process but detect as bypass attempt
expect(response.status).toBe(204);
expect(detectCSPBypass).toHaveBeenCalled();
});
});
});

View File

@ -7,7 +7,10 @@
import { describe, it, expect, beforeEach } from "vitest";
import { createMocks } from "node-mocks-http";
import type { NextRequest } from "next/server";
import { csrfProtectionMiddleware, csrfTokenMiddleware } from "../../middleware/csrfProtection";
import {
csrfProtectionMiddleware,
csrfTokenMiddleware,
} from "../../middleware/csrfProtection";
import { generateCSRFToken } from "../../lib/csrf";
describe("CSRF Protection Integration", () => {
@ -250,4 +253,4 @@ describe("CSRF Protection Integration", () => {
expect(response.status).toBe(403);
});
});
});
});

View File

@ -346,7 +346,9 @@ session4,user4,en,US,192.168.1.4,positive,5,2024-01-15T10:00:00Z,2024-01-15T10:3
describe("Batch Import Performance", () => {
it("should handle large CSV files efficiently", async () => {
// Generate large CSV with 1000 rows
const largeCSVRows = ["sessionId,userId,language,country,ipAddress,sentiment,messagesSent,startTime,endTime,escalated,forwardedHr,summary"];
const largeCSVRows = [
"sessionId,userId,language,country,ipAddress,sentiment,messagesSent,startTime,endTime,escalated,forwardedHr,summary",
];
for (let i = 0; i < 1000; i++) {
largeCSVRows.push(
@ -379,4 +381,4 @@ session4,user4,en,US,192.168.1.4,positive,5,2024-01-15T10:00:00Z,2024-01-15T10:3
expect(importCount).toBe(1000);
});
});
});
});

View File

@ -108,9 +108,15 @@ describe("Password Reset Flow Integration", () => {
};
// Generate multiple tokens
await authRouter.createCaller(ctx).forgotPassword({ email: "test@example.com" });
await authRouter.createCaller(ctx).forgotPassword({ email: "test@example.com" });
await authRouter.createCaller(ctx).forgotPassword({ email: "test@example.com" });
await authRouter
.createCaller(ctx)
.forgotPassword({ email: "test@example.com" });
await authRouter
.createCaller(ctx)
.forgotPassword({ email: "test@example.com" });
await authRouter
.createCaller(ctx)
.forgotPassword({ email: "test@example.com" });
expect(capturedTokens).toHaveLength(3);
expect(capturedTokens[0]).not.toBe(capturedTokens[1]);
@ -118,7 +124,7 @@ describe("Password Reset Flow Integration", () => {
expect(capturedTokens[0]).not.toBe(capturedTokens[2]);
// All tokens should be properly formatted
capturedTokens.forEach(token => {
capturedTokens.forEach((token) => {
expect(token).toHaveLength(64);
expect(token).toMatch(/^[0-9a-f]{64}$/);
});
@ -131,7 +137,7 @@ describe("Password Reset Flow Integration", () => {
const { authRouter } = await import("../../server/routers/auth");
const { prisma } = await import("../../lib/prisma");
const secureToken = crypto.randomBytes(32).toString('hex');
const secureToken = crypto.randomBytes(32).toString("hex");
const futureDate = new Date(Date.now() + 3600000);
const userWithResetToken = {
@ -146,7 +152,9 @@ describe("Password Reset Flow Integration", () => {
updatedAt: new Date(),
};
vi.mocked(prisma.user.findFirst).mockResolvedValueOnce(userWithResetToken);
vi.mocked(prisma.user.findFirst).mockResolvedValueOnce(
userWithResetToken
);
vi.mocked(prisma.user.update).mockResolvedValueOnce({
...userWithResetToken,
password: "new-hashed-password",
@ -159,12 +167,10 @@ describe("Password Reset Flow Integration", () => {
session: null,
};
const result = await authRouter
.createCaller(ctx)
.resetPassword({
token: secureToken,
password: "NewSecurePassword123!",
});
const result = await authRouter.createCaller(ctx).resetPassword({
token: secureToken,
password: "NewSecurePassword123!",
});
expect(result.message).toBe("Password reset successfully");
expect(prisma.user.findFirst).toHaveBeenCalledWith({
@ -212,7 +218,7 @@ describe("Password Reset Flow Integration", () => {
it("should demonstrate improvement over weak Math.random() tokens", () => {
// Generate tokens using both methods
const secureTokens = Array.from({ length: 100 }, () =>
crypto.randomBytes(32).toString('hex')
crypto.randomBytes(32).toString("hex")
);
const weakTokens = Array.from({ length: 100 }, () =>
@ -220,8 +226,11 @@ describe("Password Reset Flow Integration", () => {
);
// Secure tokens should be longer
const avgSecureLength = secureTokens.reduce((sum, t) => sum + t.length, 0) / secureTokens.length;
const avgWeakLength = weakTokens.reduce((sum, t) => sum + t.length, 0) / weakTokens.length;
const avgSecureLength =
secureTokens.reduce((sum, t) => sum + t.length, 0) /
secureTokens.length;
const avgWeakLength =
weakTokens.reduce((sum, t) => sum + t.length, 0) / weakTokens.length;
expect(avgSecureLength).toBeGreaterThan(avgWeakLength * 4);
@ -230,9 +239,9 @@ describe("Password Reset Flow Integration", () => {
// Weak tokens might have collisions with enough samples
// but more importantly, they're predictable
secureTokens.forEach(token => {
secureTokens.forEach((token) => {
expect(token).toMatch(/^[0-9a-f]{64}$/);
});
});
});
});
});

View File

@ -19,11 +19,13 @@ describe("Security Headers Configuration", () => {
expect(headers.length).toBeGreaterThan(0);
// Find the main security headers configuration
const securityConfig = headers.find(h => h.source === "/(.*)" && h.headers.length > 1);
const securityConfig = headers.find(
(h) => h.source === "/(.*)" && h.headers.length > 1
);
expect(securityConfig).toBeDefined();
if (securityConfig) {
const headerNames = securityConfig.headers.map(h => h.key);
const headerNames = securityConfig.headers.map((h) => h.key);
// Check required security headers are present
expect(headerNames).toContain("X-Content-Type-Options");
@ -40,15 +42,21 @@ describe("Security Headers Configuration", () => {
const nextConfig = await import("../../next.config.js");
const headers = await nextConfig.default.headers();
const securityConfig = headers.find(h => h.source === "/(.*)" && h.headers.length > 1);
const securityConfig = headers.find(
(h) => h.source === "/(.*)" && h.headers.length > 1
);
if (securityConfig) {
const headerMap = new Map(securityConfig.headers.map(h => [h.key, h.value]));
const headerMap = new Map(
securityConfig.headers.map((h) => [h.key, h.value])
);
expect(headerMap.get("X-Content-Type-Options")).toBe("nosniff");
expect(headerMap.get("X-Frame-Options")).toBe("DENY");
expect(headerMap.get("X-XSS-Protection")).toBe("1; mode=block");
expect(headerMap.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
expect(headerMap.get("Referrer-Policy")).toBe(
"strict-origin-when-cross-origin"
);
expect(headerMap.get("X-DNS-Prefetch-Control")).toBe("off");
// CSP should contain essential directives
@ -73,21 +81,25 @@ describe("Security Headers Configuration", () => {
process.env.NODE_ENV = "production";
const prodHeaders = await nextConfig.default.headers();
const hstsConfig = prodHeaders.find(h =>
h.headers.some(header => header.key === "Strict-Transport-Security")
const hstsConfig = prodHeaders.find((h) =>
h.headers.some((header) => header.key === "Strict-Transport-Security")
);
if (hstsConfig) {
const hstsHeader = hstsConfig.headers.find(h => h.key === "Strict-Transport-Security");
expect(hstsHeader?.value).toBe("max-age=31536000; includeSubDomains; preload");
const hstsHeader = hstsConfig.headers.find(
(h) => h.key === "Strict-Transport-Security"
);
expect(hstsHeader?.value).toBe(
"max-age=31536000; includeSubDomains; preload"
);
}
// Test development environment
process.env.NODE_ENV = "development";
const devHeaders = await nextConfig.default.headers();
const devHstsConfig = devHeaders.find(h =>
h.headers.some(header => header.key === "Strict-Transport-Security")
const devHstsConfig = devHeaders.find((h) =>
h.headers.some((header) => header.key === "Strict-Transport-Security")
);
// In development, HSTS header array should be empty
@ -105,8 +117,12 @@ describe("Security Headers Configuration", () => {
const nextConfig = await import("../../next.config.js");
const headers = await nextConfig.default.headers();
const securityConfig = headers.find(h => h.source === "/(.*)" && h.headers.length > 1);
const cspHeader = securityConfig?.headers.find(h => h.key === "Content-Security-Policy");
const securityConfig = headers.find(
(h) => h.source === "/(.*)" && h.headers.length > 1
);
const cspHeader = securityConfig?.headers.find(
(h) => h.key === "Content-Security-Policy"
);
expect(cspHeader).toBeDefined();
@ -122,7 +138,9 @@ describe("Security Headers Configuration", () => {
expect(csp).toContain("upgrade-insecure-requests");
// Next.js compatibility directives
expect(csp).toContain("script-src 'self' 'unsafe-eval' 'unsafe-inline'");
expect(csp).toContain(
"script-src 'self' 'unsafe-eval' 'unsafe-inline'"
);
expect(csp).toContain("style-src 'self' 'unsafe-inline'");
expect(csp).toContain("img-src 'self' data: https:");
expect(csp).toContain("font-src 'self' data:");
@ -136,8 +154,12 @@ describe("Security Headers Configuration", () => {
const nextConfig = await import("../../next.config.js");
const headers = await nextConfig.default.headers();
const securityConfig = headers.find(h => h.source === "/(.*)" && h.headers.length > 1);
const permissionsHeader = securityConfig?.headers.find(h => h.key === "Permissions-Policy");
const securityConfig = headers.find(
(h) => h.source === "/(.*)" && h.headers.length > 1
);
const permissionsHeader = securityConfig?.headers.find(
(h) => h.key === "Permissions-Policy"
);
expect(permissionsHeader).toBeDefined();
@ -153,4 +175,4 @@ describe("Security Headers Configuration", () => {
}
});
});
});
});

View File

@ -0,0 +1,545 @@
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();
});
});
});

View File

@ -14,10 +14,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { prisma } from "@/lib/prisma";
import { processSessionImports } from "@/lib/importProcessor";
import { processUnprocessedSessions } from "@/lib/processingScheduler";
import { createBatchJob, checkBatchStatuses, processCompletedBatches } from "@/lib/batchProcessor";
import {
createBatchJob,
checkBatchStatuses,
processCompletedBatches,
} from "@/lib/batchProcessor";
import { transcriptFetcher } from "@/lib/transcriptFetcher";
import { parseTranscriptMessages } from "@/lib/transcriptParser";
import type { Company, SessionImport, Session, User, AIBatchRequest } from "@prisma/client";
import type {
Company,
SessionImport,
Session,
User,
AIBatchRequest,
} from "@prisma/client";
// Mock external dependencies
vi.mock("@/lib/transcriptFetcher");
@ -127,7 +137,8 @@ Chat ended at 10:15 AM
});
expect(session?.messages[1]).toMatchObject({
role: "Assistant",
content: "Hi! I'd be happy to help you with your vacation request. How many days are you planning to take off?",
content:
"Hi! I'd be happy to help you with your vacation request. How many days are you planning to take off?",
order: 1,
});
@ -174,7 +185,9 @@ Chat ended at 10:15 AM
});
// Mock transcript fetching to throw error
vi.mocked(transcriptFetcher).mockRejectedValueOnce(new Error("Network error"));
vi.mocked(transcriptFetcher).mockRejectedValueOnce(
new Error("Network error")
);
await processSessionImports(testCompany.id);
@ -207,7 +220,11 @@ Chat ended at 10:15 AM
messages: {
create: [
{ role: "User", content: "I need 10 days of vacation", order: 0 },
{ role: "Assistant", content: "I'll help you with that", order: 1 },
{
role: "Assistant",
content: "I'll help you with that",
order: 1,
},
],
},
},
@ -277,7 +294,9 @@ Chat ended at 10:15 AM
// Mock OpenAI batch status check
const openai = await import("openai");
vi.mocked(openai.default.prototype.batches.retrieve).mockResolvedValueOnce({
vi.mocked(
openai.default.prototype.batches.retrieve
).mockResolvedValueOnce({
id: "batch_xyz789",
status: "completed",
output_file_id: "file-output",
@ -323,19 +342,21 @@ Chat ended at 10:15 AM
response: {
status_code: 200,
body: {
choices: [{
message: {
content: JSON.stringify({
sentiment: "POSITIVE",
category: "LEAVE_VACATION",
summary: "User requesting 10 days vacation",
questions: [
"How do I access the HR portal?",
"When should my vacation end?"
],
}),
choices: [
{
message: {
content: JSON.stringify({
sentiment: "POSITIVE",
category: "LEAVE_VACATION",
summary: "User requesting 10 days vacation",
questions: [
"How do I access the HR portal?",
"When should my vacation end?",
],
}),
},
},
}],
],
usage: {
prompt_tokens: 100,
completion_tokens: 50,
@ -344,7 +365,9 @@ Chat ended at 10:15 AM
},
},
},
].map(r => JSON.stringify(r)).join("\n");
]
.map((r) => JSON.stringify(r))
.join("\n");
// Mock OpenAI file content retrieval
const openai = await import("openai");
@ -372,7 +395,9 @@ Chat ended at 10:15 AM
expect(updatedSession?.sessionQuestions).toHaveLength(2);
// Verify questions were extracted
const questions = updatedSession?.sessionQuestions.map(sq => sq.question.content);
const questions = updatedSession?.sessionQuestions.map(
(sq) => sq.question.content
);
expect(questions).toContain("How do I access the HR portal?");
expect(questions).toContain("When should my vacation end?");
@ -503,28 +528,36 @@ Chat ended at 10:15 AM
});
// Step 5: Process batch results
const mockResults = [{
custom_id: session!.id,
response: {
status_code: 200,
body: {
choices: [{
message: {
content: JSON.stringify({
sentiment: "POSITIVE",
category: "LEAVE_VACATION",
summary: "User successfully requested vacation time",
questions: ["How do I access the HR portal?"],
}),
const mockResults = [
{
custom_id: session!.id,
response: {
status_code: 200,
body: {
choices: [
{
message: {
content: JSON.stringify({
sentiment: "POSITIVE",
category: "LEAVE_VACATION",
summary: "User successfully requested vacation time",
questions: ["How do I access the HR portal?"],
}),
},
},
],
usage: {
prompt_tokens: 200,
completion_tokens: 100,
total_tokens: 300,
},
}],
usage: { prompt_tokens: 200, completion_tokens: 100, total_tokens: 300 },
},
},
},
}];
];
vi.mocked(openai.default.prototype.files.content).mockResolvedValueOnce({
text: async () => mockResults.map(r => JSON.stringify(r)).join("\n"),
text: async () => mockResults.map((r) => JSON.stringify(r)).join("\n"),
} as any);
await processCompletedBatches(testCompany.id);
@ -541,10 +574,12 @@ Chat ended at 10:15 AM
expect(finalSession?.sentiment).toBe("POSITIVE");
expect(finalSession?.category).toBe("LEAVE_VACATION");
expect(finalSession?.summary).toBe("User successfully requested vacation time");
expect(finalSession?.summary).toBe(
"User successfully requested vacation time"
);
expect(finalSession?.sessionQuestions).toHaveLength(1);
expect(finalSession?.aiProcessingRequests).toHaveLength(1);
expect(finalSession?.aiProcessingRequests[0].success).toBe(true);
});
});
});
});

View File

@ -321,4 +321,4 @@ describe("CSRF Hooks", () => {
);
});
});
});
});

View File

@ -5,7 +5,12 @@
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { generateCSRFToken, verifyCSRFToken, CSRFProtection, CSRF_CONFIG } from "../../lib/csrf";
import {
generateCSRFToken,
verifyCSRFToken,
CSRFProtection,
CSRF_CONFIG,
} from "../../lib/csrf";
// Mock Next.js modules
vi.mock("next/headers", () => ({
@ -237,4 +242,4 @@ describe("CSRF Protection", () => {
expect(typeof CSRF_CONFIG.cookie.secure).toBe("boolean");
});
});
});
});

View File

@ -0,0 +1,155 @@
import { describe, it, expect, beforeEach } from "vitest";
import {
buildCSP,
validateCSP,
testCSPImplementation,
generateNonce,
detectCSPBypass,
type CSPConfig,
} from "../../lib/csp";
import { cspMonitoring } from "../../lib/csp-monitoring";
describe("Enhanced CSP Implementation", () => {
describe("CSP Building", () => {
it("should build development CSP with unsafe directives", () => {
const csp = buildCSP({ isDevelopment: true });
expect(csp).toContain("'unsafe-eval'");
expect(csp).toContain("'unsafe-inline'");
expect(csp).toContain("wss:");
expect(csp).toContain("ws:");
});
it("should build production CSP with nonce-based execution", () => {
const nonce = generateNonce();
const csp = buildCSP({
isDevelopment: false,
nonce,
strictMode: true,
});
expect(csp).toContain(`'nonce-${nonce}'`);
expect(csp).toContain("'strict-dynamic'");
expect(csp).not.toContain("'unsafe-inline'");
expect(csp).not.toContain("'unsafe-eval'");
});
it("should handle external domains in strict mode", () => {
const config: CSPConfig = {
isDevelopment: false,
strictMode: true,
allowedExternalDomains: [
"https://api.openai.com",
"https://example.com",
],
};
const csp = buildCSP(config);
expect(csp).toContain("https://api.openai.com");
expect(csp).toContain("https://example.com");
// Check that connect-src doesn't have broad https: allowlist (only specific domains)
const connectSrcMatch = csp.match(/connect-src[^;]+/);
// Should not contain "https:" as a standalone directive (which would allow all HTTPS)
expect(connectSrcMatch?.[0]).not.toMatch(/\bhttps:\s/);
expect(connectSrcMatch?.[0]).not.toMatch(/\shttps:$/);
// But should contain specific domains
expect(connectSrcMatch?.[0]).toContain("https://api.openai.com");
});
it("should include proper map tile sources", () => {
const csp = buildCSP({ isDevelopment: false });
expect(csp).toContain("https://*.basemaps.cartocdn.com");
expect(csp).toContain("https://*.openstreetmap.org");
});
});
describe("CSP Validation", () => {
it("should validate development CSP with appropriate warnings", () => {
const csp = buildCSP({ isDevelopment: true });
const validation = validateCSP(csp);
expect(validation.isValid).toBe(true);
expect(validation.warnings.length).toBeGreaterThan(0);
expect(validation.warnings.some((w) => w.includes("unsafe-eval"))).toBe(
true
);
expect(validation.securityScore).toBeLessThan(100);
});
it("should validate production CSP with higher security score", () => {
const nonce = generateNonce();
const csp = buildCSP({
isDevelopment: false,
nonce,
strictMode: true,
reportUri: "/api/csp-report",
});
const validation = validateCSP(csp, { strictMode: true });
expect(validation.isValid).toBe(true);
expect(validation.securityScore).toBeGreaterThan(80);
expect(validation.recommendations).toBeDefined();
});
});
describe("CSP Bypass Detection", () => {
it("should detect JavaScript protocol attempts", () => {
const content = "javascript:alert(1)";
const detection = detectCSPBypass(content);
expect(detection.isDetected).toBe(true);
expect(detection.riskLevel).toBe("high");
expect(detection.patterns.length).toBeGreaterThan(0);
});
it("should detect data URI script injection", () => {
const content = "data:text/javascript,alert(1)";
const detection = detectCSPBypass(content);
expect(detection.isDetected).toBe(true);
expect(detection.riskLevel).toBe("high");
});
it("should detect eval injection attempts", () => {
const content = "eval('malicious code')";
const detection = detectCSPBypass(content);
expect(detection.isDetected).toBe(true);
expect(detection.riskLevel).toBe("high");
});
it("should not flag legitimate JavaScript", () => {
const content = "const x = document.getElementById('safe');";
const detection = detectCSPBypass(content);
expect(detection.isDetected).toBe(false);
expect(detection.riskLevel).toBe("low");
});
});
describe("Nonce Generation", () => {
it("should generate cryptographically secure nonces", () => {
const nonce1 = generateNonce();
const nonce2 = generateNonce();
expect(nonce1).not.toBe(nonce2);
expect(nonce1.length).toBeGreaterThan(10);
expect(typeof nonce1).toBe("string");
// Should be base64 encoded
expect(() => atob(nonce1)).not.toThrow();
});
it("should generate unique nonces", () => {
const nonces = new Set();
for (let i = 0; i < 1000; i++) {
nonces.add(generateNonce());
}
expect(nonces.size).toBe(1000);
});
});
});

View File

@ -370,4 +370,4 @@ describe("Security Header Integration", () => {
expect(cspValue).toContain("style-src 'self' 'unsafe-inline'");
});
});
});
});

View File

@ -12,14 +12,14 @@ describe("Password Reset Token Security", () => {
describe("Token Generation Security Properties", () => {
it("should generate tokens with 64 characters (32 bytes as hex)", () => {
const token = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString("hex");
expect(token).toHaveLength(64);
});
it("should generate unique tokens on each call", () => {
const token1 = crypto.randomBytes(32).toString('hex');
const token2 = crypto.randomBytes(32).toString('hex');
const token3 = crypto.randomBytes(32).toString('hex');
const token1 = crypto.randomBytes(32).toString("hex");
const token2 = crypto.randomBytes(32).toString("hex");
const token3 = crypto.randomBytes(32).toString("hex");
expect(token1).not.toBe(token2);
expect(token2).not.toBe(token3);
@ -32,7 +32,7 @@ describe("Password Reset Token Security", () => {
// Generate multiple tokens to check for patterns
for (let i = 0; i < numTokens; i++) {
const token = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString("hex");
tokens.add(token);
}
@ -41,7 +41,7 @@ describe("Password Reset Token Security", () => {
});
it("should generate tokens with hex characters only", () => {
const token = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString("hex");
const hexPattern = /^[0-9a-f]+$/;
expect(token).toMatch(hexPattern);
});
@ -49,7 +49,7 @@ describe("Password Reset Token Security", () => {
it("should have sufficient entropy to prevent brute force attacks", () => {
// 32 bytes = 256 bits of entropy
// This provides 2^256 possible combinations
const token = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString("hex");
// Verify we have the expected length for 256-bit security
expect(token).toHaveLength(64);
@ -66,7 +66,7 @@ describe("Password Reset Token Security", () => {
it("should be significantly more secure than Math.random() approach", () => {
// Generate tokens using both methods for comparison
const secureToken = crypto.randomBytes(32).toString('hex');
const secureToken = crypto.randomBytes(32).toString("hex");
const weakToken = Math.random().toString(36).substring(2, 15);
// Secure token should be much longer
@ -88,7 +88,7 @@ describe("Password Reset Token Security", () => {
// Generate many tokens to test collision resistance
for (let i = 0; i < iterations; i++) {
const token = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString("hex");
expect(tokens.has(token)).toBe(false); // No collisions
tokens.add(token);
}
@ -103,7 +103,7 @@ describe("Password Reset Token Security", () => {
// Generate 1000 tokens
for (let i = 0; i < 1000; i++) {
crypto.randomBytes(32).toString('hex');
crypto.randomBytes(32).toString("hex");
}
const endTime = Date.now();
@ -117,7 +117,7 @@ describe("Password Reset Token Security", () => {
describe("Token Format Validation", () => {
it("should always produce lowercase hex", () => {
for (let i = 0; i < 10; i++) {
const token = crypto.randomBytes(32).toString('hex');
const token = crypto.randomBytes(32).toString("hex");
expect(token).toBe(token.toLowerCase());
expect(token).toMatch(/^[0-9a-f]{64}$/);
}
@ -127,16 +127,16 @@ describe("Password Reset Token Security", () => {
const tokens = [];
for (let i = 0; i < 100; i++) {
tokens.push(crypto.randomBytes(32).toString('hex'));
tokens.push(crypto.randomBytes(32).toString("hex"));
}
// Check that tokens don't all start with same character
const firstChars = new Set(tokens.map(t => t[0]));
const firstChars = new Set(tokens.map((t) => t[0]));
expect(firstChars.size).toBeGreaterThan(1);
// Check that we don't have obvious patterns like all starting with '0'
const zeroStart = tokens.filter(t => t.startsWith('0')).length;
const zeroStart = tokens.filter((t) => t.startsWith("0")).length;
expect(zeroStart).toBeLessThan(tokens.length * 0.8); // Should be roughly 1/16
});
});
});
});

View File

@ -0,0 +1,553 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { prisma } from "../../lib/prisma";
import {
securityAuditLogger,
SecurityEventType,
AuditOutcome,
AuditSeverity,
createAuditMetadata,
createAuditContext,
} from "../../lib/securityAuditLogger";
import {
AuditLogRetentionManager,
DEFAULT_RETENTION_POLICIES,
} from "../../lib/auditLogRetention";
import { NextRequest } from "next/server";
// Mock Prisma
vi.mock("../../lib/prisma", () => ({
prisma: {
securityAuditLog: {
create: vi.fn(),
findMany: vi.fn(),
count: vi.fn(),
deleteMany: vi.fn(),
findFirst: vi.fn(),
groupBy: vi.fn(),
},
},
}));
describe("Security Audit Logging", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-15T10:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
describe("SecurityAuditLogger", () => {
it("should log authentication events with correct structure", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValueOnce({} as any);
const context = {
userId: "user-123",
companyId: "company-456",
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
metadata: { action: "login" },
};
await securityAuditLogger.logAuthentication(
"user_login",
AuditOutcome.SUCCESS,
context,
undefined
);
expect(mockCreate).toHaveBeenCalledWith({
data: {
eventType: SecurityEventType.AUTHENTICATION,
action: "user_login",
outcome: AuditOutcome.SUCCESS,
severity: AuditSeverity.INFO,
userId: "user-123",
companyId: "company-456",
platformUserId: null,
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
country: null,
sessionId: null,
requestId: null,
metadata: { action: "login" },
errorMessage: null,
},
});
});
it("should assign correct severity for failed authentication", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValueOnce({} as any);
await securityAuditLogger.logAuthentication(
"user_login_failed",
AuditOutcome.FAILURE,
{ ipAddress: "192.168.1.1" },
"Invalid credentials"
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
severity: AuditSeverity.MEDIUM,
errorMessage: "Invalid credentials",
}),
});
});
it("should assign high severity for blocked authentication", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValueOnce({} as any);
await securityAuditLogger.logAuthentication(
"user_login_blocked",
AuditOutcome.BLOCKED,
{ ipAddress: "192.168.1.1" },
"Account suspended"
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
severity: AuditSeverity.HIGH,
}),
});
});
it("should log platform admin events with critical severity", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValueOnce({} as any);
await securityAuditLogger.logPlatformAdmin(
"company_suspended",
AuditOutcome.SUCCESS,
{
platformUserId: "admin-123",
companyId: "company-456",
ipAddress: "10.0.0.1",
}
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
eventType: SecurityEventType.PLATFORM_ADMIN,
severity: AuditSeverity.HIGH,
platformUserId: "admin-123",
}),
});
});
it("should handle logging errors gracefully", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockRejectedValueOnce(new Error("Database error"));
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
// Should not throw
await expect(
securityAuditLogger.logAuthentication(
"test_action",
AuditOutcome.SUCCESS,
{ ipAddress: "127.0.0.1" }
)
).resolves.not.toThrow();
expect(consoleSpy).toHaveBeenCalledWith(
"Failed to write audit log:",
expect.any(Error)
);
consoleSpy.mockRestore();
});
it("should respect audit logging disabled flag", async () => {
const originalEnv = process.env.AUDIT_LOGGING_ENABLED;
process.env.AUDIT_LOGGING_ENABLED = "false";
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
// Create new instance to pick up environment change
const disabledLogger = new (
await import("../../lib/securityAuditLogger")
).SecurityAuditLogger();
await (disabledLogger as any).log({
eventType: SecurityEventType.AUTHENTICATION,
action: "test",
outcome: AuditOutcome.SUCCESS,
context: {},
});
expect(mockCreate).not.toHaveBeenCalled();
process.env.AUDIT_LOGGING_ENABLED = originalEnv;
});
});
describe("createAuditMetadata", () => {
it("should sanitize sensitive data", () => {
const input = {
email: "user@example.com",
password: "secret123",
token: "jwt-token",
count: 5,
isValid: true,
nestedObject: { key: "value" },
arrayOfObjects: [{ id: 1 }, { id: 2 }],
arrayOfStrings: ["a", "b", "c"],
};
const result = createAuditMetadata(input);
expect(result).toEqual({
email: "user@example.com",
password: "secret123",
token: "jwt-token",
count: 5,
isValid: true,
nestedObject: "[Object]",
arrayOfObjects: ["[Object]", "[Object]"],
arrayOfStrings: ["a", "b", "c"],
});
});
it("should handle empty and null values", () => {
const input = {
emptyString: "",
nullValue: null,
undefinedValue: undefined,
zeroNumber: 0,
falseBool: false,
};
const result = createAuditMetadata(input);
expect(result).toEqual({
emptyString: "",
zeroNumber: 0,
falseBool: false,
});
});
});
describe("createAuditContext", () => {
it("should extract context from NextRequest", async () => {
const mockRequest = new NextRequest("http://localhost:3000/api/test", {
headers: {
"user-agent": "Test Agent",
"x-forwarded-for": "203.0.113.1",
"x-request-id": "req-123",
},
});
const context = await createAuditContext(mockRequest);
expect(context).toEqual({
requestId: expect.any(String),
ipAddress: "203.0.113.1",
userAgent: "Test Agent",
});
});
it("should include session information when provided", async () => {
const mockSession = {
user: {
id: "user-123",
email: "user@example.com",
companyId: "company-456",
role: "USER",
},
};
const context = await createAuditContext(undefined, mockSession);
expect(context).toEqual({
requestId: expect.any(String),
userId: "user-123",
companyId: "company-456",
});
});
it("should detect platform users", async () => {
const mockSession = {
user: {
id: "admin-123",
email: "admin@platform.com",
isPlatformUser: true,
},
};
const context = await createAuditContext(undefined, mockSession);
expect(context).toEqual({
requestId: expect.any(String),
userId: "admin-123",
platformUserId: "admin-123",
});
});
});
describe("AuditLogRetentionManager", () => {
it("should validate retention policies correctly", async () => {
const manager = new AuditLogRetentionManager();
const validation = await manager.validateRetentionPolicies();
expect(validation.valid).toBe(true);
expect(validation.errors).toHaveLength(0);
expect(validation.warnings.length).toBeGreaterThanOrEqual(0);
});
it("should detect invalid retention policies", async () => {
const invalidPolicies = [
{
name: "",
maxAgeDays: 30,
},
{
name: "Invalid Age",
maxAgeDays: -5,
},
];
const manager = new AuditLogRetentionManager(invalidPolicies);
const validation = await manager.validateRetentionPolicies();
expect(validation.valid).toBe(false);
expect(validation.errors.length).toBeGreaterThan(0);
});
it("should calculate retention statistics", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
const mockGroupBy = vi.mocked(prisma.securityAuditLog.groupBy);
const mockFindFirst = vi.mocked(prisma.securityAuditLog.findFirst);
mockCount
.mockResolvedValueOnce(1000) // total logs
.mockResolvedValueOnce(50) // last 24 hours
.mockResolvedValueOnce(200) // last 7 days
.mockResolvedValueOnce(500) // last 30 days
.mockResolvedValueOnce(800) // last 90 days
.mockResolvedValueOnce(950) // last 365 days
.mockResolvedValueOnce(50); // older than 1 year
mockGroupBy
.mockResolvedValueOnce([
{ eventType: "AUTHENTICATION", _count: { id: 600 } },
{ eventType: "AUTHORIZATION", _count: { id: 400 } },
])
.mockResolvedValueOnce([
{ severity: "INFO", _count: { id: 700 } },
{ severity: "MEDIUM", _count: { id: 250 } },
{ severity: "HIGH", _count: { id: 50 } },
]);
mockFindFirst
.mockResolvedValueOnce({ timestamp: new Date("2023-01-01") })
.mockResolvedValueOnce({ timestamp: new Date("2024-01-15") });
const manager = new AuditLogRetentionManager();
const stats = await manager.getRetentionStatistics();
expect(stats.totalLogs).toBe(1000);
expect(stats.logsByEventType).toEqual({
AUTHENTICATION: 600,
AUTHORIZATION: 400,
});
expect(stats.logsBySeverity).toEqual({
INFO: 700,
MEDIUM: 250,
HIGH: 50,
});
expect(stats.logsByAge).toHaveLength(6);
expect(stats.oldestLog).toEqual(new Date("2023-01-01"));
expect(stats.newestLog).toEqual(new Date("2024-01-15"));
});
it("should execute retention policies in dry run mode", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
const mockDeleteMany = vi.mocked(prisma.securityAuditLog.deleteMany);
mockCount.mockResolvedValue(100);
const manager = new AuditLogRetentionManager(
DEFAULT_RETENTION_POLICIES,
true
);
const results = await manager.executeRetentionPolicies();
expect(results.totalProcessed).toBeGreaterThan(0);
expect(mockDeleteMany).not.toHaveBeenCalled(); // Dry run shouldn't delete
});
it("should execute retention policies with actual deletion", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
const mockDeleteMany = vi.mocked(prisma.securityAuditLog.deleteMany);
mockCount.mockResolvedValue(50);
mockDeleteMany.mockResolvedValue({ count: 50 });
const testPolicies = [
{
name: "Test Policy",
maxAgeDays: 30,
severityFilter: ["INFO"],
archiveBeforeDelete: false,
},
];
const manager = new AuditLogRetentionManager(testPolicies, false);
const results = await manager.executeRetentionPolicies();
expect(results.totalDeleted).toBe(50);
expect(mockDeleteMany).toHaveBeenCalled();
});
it("should handle retention policy errors gracefully", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
mockCount.mockRejectedValue(new Error("Database connection failed"));
const manager = new AuditLogRetentionManager();
const results = await manager.executeRetentionPolicies();
expect(
results.policyResults.every((result) => result.errors.length > 0)
).toBe(true);
});
it("should detect policy overlaps", async () => {
const overlappingPolicies = [
{
name: "Policy 1",
maxAgeDays: 30,
severityFilter: ["INFO", "LOW"],
},
{
name: "Policy 2",
maxAgeDays: 60,
severityFilter: ["LOW", "MEDIUM"],
},
];
const manager = new AuditLogRetentionManager(overlappingPolicies);
const validation = await manager.validateRetentionPolicies();
expect(validation.warnings.some((w) => w.includes("overlap"))).toBe(true);
});
});
describe("Severity Assignment", () => {
it("should assign correct severity for user management actions", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValue({} as any);
// Test privileged action
await securityAuditLogger.logUserManagement(
"user_deleted",
AuditOutcome.SUCCESS,
{ userId: "admin-123" }
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
severity: AuditSeverity.HIGH,
}),
});
mockCreate.mockClear();
// Test regular action
await securityAuditLogger.logUserManagement(
"user_profile_updated",
AuditOutcome.SUCCESS,
{ userId: "user-123" }
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
severity: AuditSeverity.MEDIUM,
}),
});
});
it("should assign correct severity for company management actions", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValue({} as any);
// Test critical action
await securityAuditLogger.logCompanyManagement(
"company_suspended",
AuditOutcome.SUCCESS,
{ platformUserId: "admin-123" }
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
severity: AuditSeverity.CRITICAL,
}),
});
});
it("should assign high severity for data privacy events", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValue({} as any);
await securityAuditLogger.logDataPrivacy(
"data_exported",
AuditOutcome.SUCCESS,
{ userId: "user-123" }
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
severity: AuditSeverity.HIGH,
}),
});
});
});
describe("Error Handling", () => {
it("should continue operation when audit logging fails", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockRejectedValue(new Error("Database error"));
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
// This should not throw an error
await expect(
securityAuditLogger.logAuthentication(
"test_action",
AuditOutcome.SUCCESS,
{}
)
).resolves.not.toThrow();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it("should handle missing context gracefully", async () => {
const mockCreate = vi.mocked(prisma.securityAuditLog.create);
mockCreate.mockResolvedValue({} as any);
await securityAuditLogger.logAuthentication(
"test_action",
AuditOutcome.SUCCESS,
{} // Empty context
);
expect(mockCreate).toHaveBeenCalledWith({
data: expect.objectContaining({
userId: null,
companyId: null,
ipAddress: null,
}),
});
});
});
});

View File

@ -0,0 +1,527 @@
import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
import {
securityMonitoring,
enhancedSecurityLog,
AlertSeverity,
AlertType,
ThreatLevel,
} from "@/lib/securityMonitoring";
import {
SecurityEventType,
AuditOutcome,
AuditSeverity,
} from "@/lib/securityAuditLogger";
import { prisma } from "@/lib/prisma";
// Mock prisma
vi.mock("@/lib/prisma", () => ({
prisma: {
securityAuditLog: {
findMany: vi.fn(),
count: vi.fn(),
create: vi.fn(),
},
},
}));
// Mock securityAuditLogger
vi.mock("@/lib/securityAuditLogger", async () => {
const actual = await vi.importActual("@/lib/securityAuditLogger");
return {
...actual,
securityAuditLogger: {
log: vi.fn(),
},
};
});
describe("Security Monitoring System", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
// Reset the monitoring service state
securityMonitoring.updateConfig({
thresholds: {
failedLoginsPerMinute: 5,
failedLoginsPerHour: 20,
rateLimitViolationsPerMinute: 10,
cspViolationsPerMinute: 15,
adminActionsPerHour: 25,
massDataAccessThreshold: 100,
suspiciousIPThreshold: 10,
},
});
});
describe("Alert Generation", () => {
it("should generate brute force alert for multiple failed logins", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockCount.mockResolvedValue(6); // Above threshold of 5
mockFindMany.mockResolvedValue([]); // Empty historical events for anomaly detection
const context = {
ipAddress: "192.168.1.100",
userAgent: "Mozilla/5.0",
requestId: "test-123",
};
await enhancedSecurityLog(
SecurityEventType.AUTHENTICATION,
"login_attempt",
AuditOutcome.FAILURE,
context,
AuditSeverity.HIGH,
"Failed login attempt"
);
expect(mockCount).toHaveBeenCalledWith({
where: {
eventType: SecurityEventType.AUTHENTICATION,
outcome: AuditOutcome.FAILURE,
ipAddress: "192.168.1.100",
timestamp: expect.any(Object),
},
});
});
it("should generate rate limit breach alert", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockCount.mockResolvedValue(11); // Above threshold of 10
mockFindMany.mockResolvedValue([]); // Empty historical events for anomaly detection
const context = {
ipAddress: "192.168.1.100",
userAgent: "Mozilla/5.0",
requestId: "test-123",
};
await enhancedSecurityLog(
SecurityEventType.RATE_LIMITING,
"rate_limit_exceeded",
AuditOutcome.RATE_LIMITED,
context,
AuditSeverity.MEDIUM,
"Rate limit exceeded"
);
expect(mockCount).toHaveBeenCalled();
});
it("should generate admin activity alert for excessive actions", async () => {
const mockCount = vi.mocked(prisma.securityAuditLog.count);
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockCount.mockResolvedValue(26); // Above threshold of 25
mockFindMany.mockResolvedValue([]); // Empty historical events for anomaly detection
const context = {
userId: "user-123",
requestId: "test-123",
};
await enhancedSecurityLog(
SecurityEventType.PLATFORM_ADMIN,
"admin_action",
AuditOutcome.SUCCESS,
context,
AuditSeverity.INFO,
"Admin action performed"
);
expect(mockCount).toHaveBeenCalled();
});
});
describe("Anomaly Detection", () => {
it("should detect geographical anomalies", async () => {
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockFindMany.mockResolvedValue([
{
id: "1",
eventType: SecurityEventType.AUTHENTICATION,
action: "login_success",
outcome: AuditOutcome.SUCCESS,
userId: "user-123",
companyId: "company-1",
platformUserId: null,
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
country: "USA",
metadata: null,
errorMessage: null,
severity: AuditSeverity.INFO,
sessionId: null,
requestId: "req-1",
timestamp: new Date(),
},
]);
const context = {
userId: "user-123",
country: "CHN", // Different country
requestId: "test-123",
};
await enhancedSecurityLog(
SecurityEventType.AUTHENTICATION,
"login_success",
AuditOutcome.SUCCESS,
context,
AuditSeverity.INFO
);
expect(mockFindMany).toHaveBeenCalled();
});
it("should detect temporal anomalies", async () => {
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
// Mock historical data showing low activity
mockFindMany.mockResolvedValue([
{
id: "1",
eventType: SecurityEventType.AUTHENTICATION,
action: "login_success",
outcome: AuditOutcome.SUCCESS,
userId: "user-123",
companyId: "company-1",
platformUserId: null,
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
country: "USA",
metadata: null,
errorMessage: null,
severity: AuditSeverity.INFO,
sessionId: null,
requestId: "req-1",
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000), // 24 hours ago
},
]);
// Simulate multiple events in short time
for (let i = 0; i < 10; i++) {
await enhancedSecurityLog(
SecurityEventType.AUTHENTICATION,
"login_success",
AuditOutcome.SUCCESS,
{ requestId: `test-${i}` },
AuditSeverity.INFO
);
}
expect(mockFindMany).toHaveBeenCalled();
});
});
describe("Security Metrics", () => {
it("should calculate comprehensive security metrics", async () => {
const mockEvents = [
{
id: "1",
eventType: SecurityEventType.AUTHENTICATION,
action: "login_success",
outcome: AuditOutcome.SUCCESS,
userId: "user-1",
companyId: "company-1",
platformUserId: null,
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
country: "USA",
metadata: null,
errorMessage: null,
severity: AuditSeverity.INFO,
sessionId: null,
requestId: "req-1",
timestamp: new Date(),
user: { email: "user1@test.com" },
company: { name: "Test Company" },
},
{
id: "2",
eventType: SecurityEventType.AUTHENTICATION,
action: "login_failure",
outcome: AuditOutcome.FAILURE,
userId: "user-2",
companyId: "company-1",
platformUserId: null,
ipAddress: "192.168.1.2",
userAgent: "Mozilla/5.0",
country: "GBR",
metadata: null,
errorMessage: "Invalid password",
severity: AuditSeverity.CRITICAL,
sessionId: null,
requestId: "req-2",
timestamp: new Date(),
user: { email: "user2@test.com" },
company: { name: "Test Company" },
},
];
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockFindMany.mockResolvedValue(mockEvents);
const timeRange = {
start: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: new Date(),
};
const metrics = await securityMonitoring.getSecurityMetrics(timeRange);
expect(metrics).toMatchObject({
totalEvents: 2,
criticalEvents: 1,
activeAlerts: expect.any(Number),
resolvedAlerts: expect.any(Number),
securityScore: expect.any(Number),
threatLevel: expect.any(String),
eventsByType: expect.any(Object),
alertsByType: expect.any(Object),
topThreats: expect.any(Array),
geoDistribution: expect.any(Object),
timeDistribution: expect.any(Array),
userRiskScores: expect.any(Array),
});
expect(metrics.securityScore).toBeGreaterThanOrEqual(0);
expect(metrics.securityScore).toBeLessThanOrEqual(100);
expect(Object.values(ThreatLevel)).toContain(metrics.threatLevel);
});
it("should calculate user risk scores correctly", async () => {
const mockEvents = [
{
id: "1",
eventType: SecurityEventType.AUTHENTICATION,
action: "login_failure",
outcome: AuditOutcome.FAILURE,
userId: "user-1",
companyId: "company-1",
platformUserId: null,
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
country: "USA",
metadata: null,
errorMessage: "Invalid password",
severity: AuditSeverity.HIGH,
sessionId: null,
requestId: "req-1",
timestamp: new Date(),
user: { email: "highrisk@test.com" },
company: { name: "Test Company" },
},
{
id: "2",
eventType: SecurityEventType.RATE_LIMITING,
action: "rate_limit_exceeded",
outcome: AuditOutcome.RATE_LIMITED,
userId: "user-1",
companyId: "company-1",
platformUserId: null,
ipAddress: "192.168.1.1",
userAgent: "Mozilla/5.0",
country: "USA",
metadata: null,
errorMessage: null,
severity: AuditSeverity.MEDIUM,
sessionId: null,
requestId: "req-2",
timestamp: new Date(),
user: { email: "highrisk@test.com" },
company: { name: "Test Company" },
},
];
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockFindMany.mockResolvedValue(mockEvents);
const timeRange = {
start: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: new Date(),
};
const metrics = await securityMonitoring.getSecurityMetrics(timeRange);
expect(metrics.userRiskScores).toHaveLength(1);
expect(metrics.userRiskScores[0]).toMatchObject({
userId: "user-1",
email: "highrisk@test.com",
riskScore: expect.any(Number),
});
expect(metrics.userRiskScores[0].riskScore).toBeGreaterThan(0);
});
});
describe("IP Threat Analysis", () => {
it("should calculate IP threat level correctly", async () => {
const mockEvents = [
{
eventType: SecurityEventType.AUTHENTICATION,
outcome: AuditOutcome.FAILURE,
userId: "user-1",
ipAddress: "192.168.1.100",
timestamp: new Date(),
},
{
eventType: SecurityEventType.RATE_LIMITING,
outcome: AuditOutcome.RATE_LIMITED,
userId: "user-2",
ipAddress: "192.168.1.100",
timestamp: new Date(),
},
];
const mockFindMany = vi.mocked(prisma.securityAuditLog.findMany);
mockFindMany.mockResolvedValue(mockEvents);
const analysis =
await securityMonitoring.calculateIPThreatLevel("192.168.1.100");
expect(analysis).toMatchObject({
threatLevel: expect.any(String),
riskFactors: expect.any(Array),
recommendations: expect.any(Array),
});
expect(Object.values(ThreatLevel)).toContain(analysis.threatLevel);
expect(analysis.riskFactors.length).toBeGreaterThan(0);
expect(analysis.recommendations.length).toBeGreaterThan(0);
});
});
describe("Configuration Management", () => {
it("should update monitoring configuration", () => {
const newConfig = {
thresholds: {
failedLoginsPerMinute: 3,
failedLoginsPerHour: 15,
},
alerting: {
enabled: false,
},
};
securityMonitoring.updateConfig(newConfig);
const currentConfig = securityMonitoring.getConfig();
expect(currentConfig.thresholds.failedLoginsPerMinute).toBe(3);
expect(currentConfig.thresholds.failedLoginsPerHour).toBe(15);
expect(currentConfig.alerting.enabled).toBe(false);
});
it("should preserve existing config when partially updating", () => {
const originalConfig = securityMonitoring.getConfig();
securityMonitoring.updateConfig({
thresholds: {
failedLoginsPerMinute: 2,
},
});
const updatedConfig = securityMonitoring.getConfig();
expect(updatedConfig.thresholds.failedLoginsPerMinute).toBe(2);
expect(updatedConfig.thresholds.failedLoginsPerHour).toBe(
originalConfig.thresholds.failedLoginsPerHour
);
expect(updatedConfig.alerting.enabled).toBe(
originalConfig.alerting.enabled
);
});
});
describe("Alert Management", () => {
it("should acknowledge alerts correctly", async () => {
// First, generate an alert
const mockCount = vi.mocked(prisma.securityAuditLog.count);
mockCount.mockResolvedValue(6); // Above threshold
await enhancedSecurityLog(
SecurityEventType.AUTHENTICATION,
"login_attempt",
AuditOutcome.FAILURE,
{ ipAddress: "192.168.1.100" },
AuditSeverity.HIGH
);
const activeAlerts = securityMonitoring.getActiveAlerts();
expect(activeAlerts.length).toBeGreaterThan(0);
const alertId = activeAlerts[0].id;
const acknowledged = await securityMonitoring.acknowledgeAlert(
alertId,
"admin-user"
);
expect(acknowledged).toBe(true);
const remainingActiveAlerts = securityMonitoring.getActiveAlerts();
expect(remainingActiveAlerts.length).toBe(activeAlerts.length - 1);
});
it("should filter alerts by severity", async () => {
// Generate alerts of different severities
const mockCount = vi.mocked(prisma.securityAuditLog.count);
mockCount.mockResolvedValue(6);
await enhancedSecurityLog(
SecurityEventType.AUTHENTICATION,
"login_attempt",
AuditOutcome.FAILURE,
{ ipAddress: "192.168.1.100" },
AuditSeverity.HIGH
);
await enhancedSecurityLog(
SecurityEventType.RATE_LIMITING,
"rate_limit",
AuditOutcome.RATE_LIMITED,
{ ipAddress: "192.168.1.101" },
AuditSeverity.MEDIUM
);
const highSeverityAlerts = securityMonitoring.getActiveAlerts(
AlertSeverity.HIGH
);
const allAlerts = securityMonitoring.getActiveAlerts();
expect(highSeverityAlerts.length).toBeLessThanOrEqual(allAlerts.length);
highSeverityAlerts.forEach((alert) => {
expect(alert.severity).toBe(AlertSeverity.HIGH);
});
});
});
describe("Data Export", () => {
it("should export security data in JSON format", () => {
const timeRange = {
start: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: new Date(),
};
const jsonData = securityMonitoring.exportSecurityData("json", timeRange);
expect(() => JSON.parse(jsonData)).not.toThrow();
const parsed = JSON.parse(jsonData);
expect(Array.isArray(parsed)).toBe(true);
});
it("should export security data in CSV format", () => {
const timeRange = {
start: new Date(Date.now() - 24 * 60 * 60 * 1000),
end: new Date(),
};
const csvData = securityMonitoring.exportSecurityData("csv", timeRange);
expect(typeof csvData).toBe("string");
expect(csvData).toContain("timestamp,severity,type,title");
});
});
});