mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 18:12:08 +01:00
- 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
528 lines
16 KiB
TypeScript
528 lines
16 KiB
TypeScript
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");
|
|
});
|
|
});
|
|
});
|