mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 16:52:08 +01:00
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:
513
lib/auditLogRetention.ts
Normal file
513
lib/auditLogRetention.ts
Normal file
@ -0,0 +1,513 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user