mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:32:11 +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
514 lines
15 KiB
TypeScript
514 lines
15 KiB
TypeScript
import { prisma } from "./prisma";
|
|
import {
|
|
AuditOutcome,
|
|
createAuditMetadata,
|
|
SecurityEventType,
|
|
securityAuditLogger,
|
|
} from "./securityAuditLogger";
|
|
|
|
export interface RetentionPolicy {
|
|
name: string;
|
|
maxAgeDays: number;
|
|
severityFilter?: string[];
|
|
eventTypeFilter?: string[];
|
|
archiveBeforeDelete?: boolean;
|
|
}
|
|
|
|
export const DEFAULT_RETENTION_POLICIES: RetentionPolicy[] = [
|
|
{
|
|
name: "Critical Events",
|
|
maxAgeDays: 2555, // 7 years for critical security events
|
|
severityFilter: ["CRITICAL"],
|
|
archiveBeforeDelete: true,
|
|
},
|
|
{
|
|
name: "High Severity Events",
|
|
maxAgeDays: 1095, // 3 years for high severity events
|
|
severityFilter: ["HIGH"],
|
|
archiveBeforeDelete: true,
|
|
},
|
|
{
|
|
name: "Authentication Events",
|
|
maxAgeDays: 730, // 2 years for authentication events
|
|
eventTypeFilter: ["AUTHENTICATION", "AUTHORIZATION", "PASSWORD_RESET"],
|
|
archiveBeforeDelete: true,
|
|
},
|
|
{
|
|
name: "Platform Admin Events",
|
|
maxAgeDays: 1095, // 3 years for platform admin activities
|
|
eventTypeFilter: ["PLATFORM_ADMIN", "COMPANY_MANAGEMENT"],
|
|
archiveBeforeDelete: true,
|
|
},
|
|
{
|
|
name: "User Management Events",
|
|
maxAgeDays: 730, // 2 years for user management
|
|
eventTypeFilter: ["USER_MANAGEMENT"],
|
|
archiveBeforeDelete: true,
|
|
},
|
|
{
|
|
name: "General Events",
|
|
maxAgeDays: 365, // 1 year for general events
|
|
severityFilter: ["INFO", "LOW", "MEDIUM"],
|
|
archiveBeforeDelete: false,
|
|
},
|
|
];
|
|
|
|
export class AuditLogRetentionManager {
|
|
private policies: RetentionPolicy[];
|
|
private isDryRun: boolean;
|
|
|
|
constructor(
|
|
policies: RetentionPolicy[] = DEFAULT_RETENTION_POLICIES,
|
|
isDryRun = false
|
|
) {
|
|
this.policies = policies;
|
|
this.isDryRun = isDryRun;
|
|
}
|
|
|
|
async executeRetentionPolicies(): Promise<{
|
|
totalProcessed: number;
|
|
totalDeleted: number;
|
|
totalArchived: number;
|
|
policyResults: Array<{
|
|
policyName: string;
|
|
processed: number;
|
|
deleted: number;
|
|
archived: number;
|
|
errors: string[];
|
|
}>;
|
|
}> {
|
|
const results = {
|
|
totalProcessed: 0,
|
|
totalDeleted: 0,
|
|
totalArchived: 0,
|
|
policyResults: [] as Array<{
|
|
policyName: string;
|
|
processed: number;
|
|
deleted: number;
|
|
archived: number;
|
|
errors: string[];
|
|
}>,
|
|
};
|
|
|
|
// Log retention policy execution start
|
|
await securityAuditLogger.log({
|
|
eventType: SecurityEventType.SYSTEM_CONFIG,
|
|
action: this.isDryRun
|
|
? "audit_log_retention_dry_run_started"
|
|
: "audit_log_retention_started",
|
|
outcome: AuditOutcome.SUCCESS,
|
|
context: {
|
|
metadata: createAuditMetadata({
|
|
policiesCount: this.policies.length,
|
|
isDryRun: this.isDryRun,
|
|
policies: this.policies.map((p) => ({
|
|
name: p.name,
|
|
maxAgeDays: p.maxAgeDays,
|
|
hasArchive: p.archiveBeforeDelete,
|
|
})),
|
|
}),
|
|
},
|
|
});
|
|
|
|
for (const policy of this.policies) {
|
|
const policyResult = {
|
|
policyName: policy.name,
|
|
processed: 0,
|
|
deleted: 0,
|
|
archived: 0,
|
|
errors: [] as string[],
|
|
};
|
|
|
|
try {
|
|
const cutoffDate = new Date();
|
|
cutoffDate.setDate(cutoffDate.getDate() - policy.maxAgeDays);
|
|
|
|
// Build where clause based on policy filters
|
|
const whereClause: any = {
|
|
timestamp: { lt: cutoffDate },
|
|
};
|
|
|
|
if (policy.severityFilter && policy.severityFilter.length > 0) {
|
|
whereClause.severity = { in: policy.severityFilter };
|
|
}
|
|
|
|
if (policy.eventTypeFilter && policy.eventTypeFilter.length > 0) {
|
|
whereClause.eventType = { in: policy.eventTypeFilter };
|
|
}
|
|
|
|
// Count logs to be processed
|
|
const logsToProcess = await prisma.securityAuditLog.count({
|
|
where: whereClause,
|
|
});
|
|
|
|
policyResult.processed = logsToProcess;
|
|
|
|
if (logsToProcess === 0) {
|
|
console.log(
|
|
`Policy "${policy.name}": No logs found for retention processing`
|
|
);
|
|
continue;
|
|
}
|
|
|
|
console.log(
|
|
`Policy "${policy.name}": Processing ${logsToProcess} logs older than ${policy.maxAgeDays} days`
|
|
);
|
|
|
|
if (this.isDryRun) {
|
|
console.log(
|
|
`DRY RUN: Would process ${logsToProcess} logs for policy "${policy.name}"`
|
|
);
|
|
if (policy.archiveBeforeDelete) {
|
|
policyResult.archived = logsToProcess;
|
|
} else {
|
|
policyResult.deleted = logsToProcess;
|
|
}
|
|
} else {
|
|
if (policy.archiveBeforeDelete) {
|
|
// In a real implementation, you would export/archive these logs
|
|
// For now, we'll just log the archival action
|
|
await securityAuditLogger.log({
|
|
eventType: SecurityEventType.DATA_PRIVACY,
|
|
action: "audit_logs_archived",
|
|
outcome: AuditOutcome.SUCCESS,
|
|
context: {
|
|
metadata: createAuditMetadata({
|
|
policyName: policy.name,
|
|
logsArchived: logsToProcess,
|
|
cutoffDate: cutoffDate.toISOString(),
|
|
}),
|
|
},
|
|
});
|
|
|
|
policyResult.archived = logsToProcess;
|
|
console.log(
|
|
`Policy "${policy.name}": Archived ${logsToProcess} logs`
|
|
);
|
|
}
|
|
|
|
// Delete the logs
|
|
const deleteResult = await prisma.securityAuditLog.deleteMany({
|
|
where: whereClause,
|
|
});
|
|
|
|
policyResult.deleted = deleteResult.count;
|
|
console.log(
|
|
`Policy "${policy.name}": Deleted ${deleteResult.count} logs`
|
|
);
|
|
|
|
// Log deletion action
|
|
await securityAuditLogger.log({
|
|
eventType: SecurityEventType.DATA_PRIVACY,
|
|
action: "audit_logs_deleted",
|
|
outcome: AuditOutcome.SUCCESS,
|
|
context: {
|
|
metadata: createAuditMetadata({
|
|
policyName: policy.name,
|
|
logsDeleted: deleteResult.count,
|
|
cutoffDate: cutoffDate.toISOString(),
|
|
wasArchived: policy.archiveBeforeDelete,
|
|
}),
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = `Error processing policy "${policy.name}": ${error}`;
|
|
policyResult.errors.push(errorMessage);
|
|
console.error(errorMessage);
|
|
|
|
// Log retention policy error
|
|
await securityAuditLogger.log({
|
|
eventType: SecurityEventType.SYSTEM_CONFIG,
|
|
action: "audit_log_retention_policy_error",
|
|
outcome: AuditOutcome.FAILURE,
|
|
errorMessage: errorMessage,
|
|
context: {
|
|
metadata: createAuditMetadata({
|
|
policyName: policy.name,
|
|
error: "retention_policy_error",
|
|
}),
|
|
},
|
|
});
|
|
}
|
|
|
|
results.policyResults.push(policyResult);
|
|
results.totalProcessed += policyResult.processed;
|
|
results.totalDeleted += policyResult.deleted;
|
|
results.totalArchived += policyResult.archived;
|
|
}
|
|
|
|
// Log retention policy execution completion
|
|
await securityAuditLogger.log({
|
|
eventType: SecurityEventType.SYSTEM_CONFIG,
|
|
action: this.isDryRun
|
|
? "audit_log_retention_dry_run_completed"
|
|
: "audit_log_retention_completed",
|
|
outcome: AuditOutcome.SUCCESS,
|
|
context: {
|
|
metadata: createAuditMetadata({
|
|
totalProcessed: results.totalProcessed,
|
|
totalDeleted: results.totalDeleted,
|
|
totalArchived: results.totalArchived,
|
|
policiesExecuted: this.policies.length,
|
|
isDryRun: this.isDryRun,
|
|
results: results.policyResults,
|
|
}),
|
|
},
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
async getRetentionStatistics(): Promise<{
|
|
totalLogs: number;
|
|
logsByEventType: Record<string, number>;
|
|
logsBySeverity: Record<string, number>;
|
|
logsByAge: Array<{ age: string; count: number }>;
|
|
oldestLog?: Date;
|
|
newestLog?: Date;
|
|
}> {
|
|
const [totalLogs, logsByEventType, logsBySeverity, oldestLog, newestLog] =
|
|
await Promise.all([
|
|
// Total count
|
|
prisma.securityAuditLog.count(),
|
|
|
|
// Group by event type
|
|
prisma.securityAuditLog.groupBy({
|
|
by: ["eventType"],
|
|
_count: { id: true },
|
|
}),
|
|
|
|
// Group by severity
|
|
prisma.securityAuditLog.groupBy({
|
|
by: ["severity"],
|
|
_count: { id: true },
|
|
}),
|
|
|
|
// Oldest log
|
|
prisma.securityAuditLog.findFirst({
|
|
orderBy: { timestamp: "asc" },
|
|
select: { timestamp: true },
|
|
}),
|
|
|
|
// Newest log
|
|
prisma.securityAuditLog.findFirst({
|
|
orderBy: { timestamp: "desc" },
|
|
select: { timestamp: true },
|
|
}),
|
|
]);
|
|
|
|
// Calculate logs by age buckets
|
|
const now = new Date();
|
|
const ageBuckets = [
|
|
{ name: "Last 24 hours", days: 1 },
|
|
{ name: "Last 7 days", days: 7 },
|
|
{ name: "Last 30 days", days: 30 },
|
|
{ name: "Last 90 days", days: 90 },
|
|
{ name: "Last 365 days", days: 365 },
|
|
{ name: "Older than 1 year", days: Number.POSITIVE_INFINITY },
|
|
];
|
|
|
|
const logsByAge: Array<{ age: string; count: number }> = [];
|
|
let previousDate = now;
|
|
|
|
for (const bucket of ageBuckets) {
|
|
const bucketDate =
|
|
bucket.days === Number.POSITIVE_INFINITY
|
|
? new Date(0)
|
|
: new Date(now.getTime() - bucket.days * 24 * 60 * 60 * 1000);
|
|
|
|
const count = await prisma.securityAuditLog.count({
|
|
where: {
|
|
timestamp: {
|
|
gte: bucketDate,
|
|
lt: previousDate,
|
|
},
|
|
},
|
|
});
|
|
|
|
logsByAge.push({
|
|
age: bucket.name,
|
|
count,
|
|
});
|
|
|
|
previousDate = bucketDate;
|
|
}
|
|
|
|
return {
|
|
totalLogs,
|
|
logsByEventType: Object.fromEntries(
|
|
logsByEventType.map((item) => [item.eventType, item._count.id])
|
|
),
|
|
logsBySeverity: Object.fromEntries(
|
|
logsBySeverity.map((item) => [item.severity, item._count.id])
|
|
),
|
|
logsByAge,
|
|
oldestLog: oldestLog?.timestamp,
|
|
newestLog: newestLog?.timestamp,
|
|
};
|
|
}
|
|
|
|
async validateRetentionPolicies(): Promise<{
|
|
valid: boolean;
|
|
errors: string[];
|
|
warnings: string[];
|
|
}> {
|
|
const errors: string[] = [];
|
|
const warnings: string[] = [];
|
|
|
|
for (const policy of this.policies) {
|
|
// Validate policy structure
|
|
if (!policy.name || policy.name.trim() === "") {
|
|
errors.push("Policy must have a non-empty name");
|
|
}
|
|
|
|
if (!policy.maxAgeDays || policy.maxAgeDays <= 0) {
|
|
errors.push(
|
|
`Policy "${policy.name}": maxAgeDays must be a positive number`
|
|
);
|
|
}
|
|
|
|
// Validate filters
|
|
if (policy.severityFilter && policy.eventTypeFilter) {
|
|
warnings.push(
|
|
`Policy "${policy.name}": Has both severity and event type filters, ensure this is intentional`
|
|
);
|
|
}
|
|
|
|
if (!policy.severityFilter && !policy.eventTypeFilter) {
|
|
warnings.push(
|
|
`Policy "${policy.name}": No filters specified, will apply to all logs`
|
|
);
|
|
}
|
|
|
|
// Warn about very short retention periods
|
|
if (policy.maxAgeDays < 30) {
|
|
warnings.push(
|
|
`Policy "${policy.name}": Very short retention period (${policy.maxAgeDays} days)`
|
|
);
|
|
}
|
|
|
|
// Warn about very long retention periods without archiving
|
|
if (policy.maxAgeDays > 1095 && !policy.archiveBeforeDelete) {
|
|
warnings.push(
|
|
`Policy "${policy.name}": Long retention period without archiving may impact performance`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check for overlapping policies that might conflict
|
|
const overlaps = this.findPolicyOverlaps();
|
|
if (overlaps.length > 0) {
|
|
warnings.push(
|
|
...overlaps.map(
|
|
(overlap) =>
|
|
`Potential policy overlap: "${overlap.policy1}" and "${overlap.policy2}"`
|
|
)
|
|
);
|
|
}
|
|
|
|
return {
|
|
valid: errors.length === 0,
|
|
errors,
|
|
warnings,
|
|
};
|
|
}
|
|
|
|
private findPolicyOverlaps(): Array<{ policy1: string; policy2: string }> {
|
|
const overlaps: Array<{ policy1: string; policy2: string }> = [];
|
|
|
|
for (let i = 0; i < this.policies.length; i++) {
|
|
for (let j = i + 1; j < this.policies.length; j++) {
|
|
const policy1 = this.policies[i];
|
|
const policy2 = this.policies[j];
|
|
|
|
// Check if policies have overlapping filters
|
|
const hasOverlappingSeverity = this.arraysOverlap(
|
|
policy1.severityFilter || [],
|
|
policy2.severityFilter || []
|
|
);
|
|
|
|
const hasOverlappingEventType = this.arraysOverlap(
|
|
policy1.eventTypeFilter || [],
|
|
policy2.eventTypeFilter || []
|
|
);
|
|
|
|
if (hasOverlappingSeverity || hasOverlappingEventType) {
|
|
overlaps.push({ policy1: policy1.name, policy2: policy2.name });
|
|
}
|
|
}
|
|
}
|
|
|
|
return overlaps;
|
|
}
|
|
|
|
private arraysOverlap(arr1: string[], arr2: string[]): boolean {
|
|
if (arr1.length === 0 || arr2.length === 0) return false;
|
|
return arr1.some((item) => arr2.includes(item));
|
|
}
|
|
}
|
|
|
|
// Utility function for scheduled retention execution
|
|
export async function executeScheduledRetention(
|
|
isDryRun = false
|
|
): Promise<void> {
|
|
const manager = new AuditLogRetentionManager(
|
|
DEFAULT_RETENTION_POLICIES,
|
|
isDryRun
|
|
);
|
|
|
|
console.log(
|
|
`Starting scheduled audit log retention (dry run: ${isDryRun})...`
|
|
);
|
|
|
|
try {
|
|
// Validate policies first
|
|
const validation = await manager.validateRetentionPolicies();
|
|
if (!validation.valid) {
|
|
throw new Error(
|
|
`Invalid retention policies: ${validation.errors.join(", ")}`
|
|
);
|
|
}
|
|
|
|
if (validation.warnings.length > 0) {
|
|
console.warn("Retention policy warnings:", validation.warnings);
|
|
}
|
|
|
|
// Execute retention
|
|
const results = await manager.executeRetentionPolicies();
|
|
|
|
console.log("Retention execution completed:");
|
|
console.log(` Total processed: ${results.totalProcessed}`);
|
|
console.log(` Total deleted: ${results.totalDeleted}`);
|
|
console.log(` Total archived: ${results.totalArchived}`);
|
|
|
|
// Log detailed results
|
|
for (const policyResult of results.policyResults) {
|
|
console.log(` Policy "${policyResult.policyName}":`);
|
|
console.log(` Processed: ${policyResult.processed}`);
|
|
console.log(` Deleted: ${policyResult.deleted}`);
|
|
console.log(` Archived: ${policyResult.archived}`);
|
|
if (policyResult.errors.length > 0) {
|
|
console.log(` Errors: ${policyResult.errors.join(", ")}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Scheduled retention execution failed:", error);
|
|
|
|
await securityAuditLogger.log({
|
|
eventType: SecurityEventType.SYSTEM_CONFIG,
|
|
action: "scheduled_retention_failed",
|
|
outcome: AuditOutcome.FAILURE,
|
|
errorMessage: `Scheduled retention failed: ${error}`,
|
|
context: {
|
|
metadata: createAuditMetadata({
|
|
isDryRun,
|
|
error: "scheduled_retention_failure",
|
|
}),
|
|
},
|
|
});
|
|
|
|
throw error;
|
|
}
|
|
}
|