guardrail-compliance 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/emitter.d.ts +97 -0
- package/dist/audit/emitter.d.ts.map +1 -0
- package/dist/audit/emitter.js +197 -0
- package/dist/audit/events.d.ts +304 -0
- package/dist/audit/events.d.ts.map +1 -0
- package/dist/audit/events.js +267 -0
- package/dist/audit/index.d.ts +11 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +51 -0
- package/dist/audit/storage.d.ts +93 -0
- package/dist/audit/storage.d.ts.map +1 -0
- package/dist/audit/storage.js +337 -0
- package/dist/automation/__tests__/compliance-scheduler.test.d.ts +2 -0
- package/dist/automation/__tests__/compliance-scheduler.test.d.ts.map +1 -0
- package/dist/automation/__tests__/compliance-scheduler.test.js +140 -0
- package/dist/automation/audit-logger.d.ts +129 -0
- package/dist/automation/audit-logger.d.ts.map +1 -0
- package/dist/automation/audit-logger.js +473 -0
- package/dist/automation/compliance-scheduler-fixed.d.ts +1 -0
- package/dist/automation/compliance-scheduler-fixed.d.ts.map +1 -0
- package/dist/automation/compliance-scheduler-fixed.js +1 -0
- package/dist/automation/compliance-scheduler.d.ts +83 -0
- package/dist/automation/compliance-scheduler.d.ts.map +1 -0
- package/dist/automation/compliance-scheduler.js +414 -0
- package/dist/automation/dashboard.d.ts +194 -0
- package/dist/automation/dashboard.d.ts.map +1 -0
- package/dist/automation/dashboard.js +768 -0
- package/dist/automation/email-service.d.ts +69 -0
- package/dist/automation/email-service.d.ts.map +1 -0
- package/dist/automation/email-service.js +218 -0
- package/dist/automation/evidence-collector.d.ts +140 -0
- package/dist/automation/evidence-collector.d.ts.map +1 -0
- package/dist/automation/evidence-collector.js +682 -0
- package/dist/automation/index.d.ts +8 -0
- package/dist/automation/index.d.ts.map +1 -0
- package/dist/automation/index.js +24 -0
- package/dist/automation/pdf-exporter.d.ts +90 -0
- package/dist/automation/pdf-exporter.d.ts.map +1 -0
- package/dist/automation/pdf-exporter.js +381 -0
- package/dist/automation/reporting-engine.d.ts +116 -0
- package/dist/automation/reporting-engine.d.ts.map +1 -0
- package/dist/automation/reporting-engine.js +329 -0
- package/dist/container/index.d.ts +4 -0
- package/dist/container/index.d.ts.map +1 -0
- package/dist/container/index.js +19 -0
- package/dist/container/kubernetes.d.ts +94 -0
- package/dist/container/kubernetes.d.ts.map +1 -0
- package/dist/container/kubernetes.js +268 -0
- package/dist/container/rules.d.ts +27 -0
- package/dist/container/rules.d.ts.map +1 -0
- package/dist/container/rules.js +216 -0
- package/dist/container/scanner.d.ts +50 -0
- package/dist/container/scanner.d.ts.map +1 -0
- package/dist/container/scanner.js +143 -0
- package/dist/frameworks/engine.d.ts +108 -0
- package/dist/frameworks/engine.d.ts.map +1 -0
- package/dist/frameworks/engine.js +206 -0
- package/dist/frameworks/gdpr.d.ts +6 -0
- package/dist/frameworks/gdpr.d.ts.map +1 -0
- package/dist/frameworks/gdpr.js +198 -0
- package/dist/frameworks/hipaa.d.ts +6 -0
- package/dist/frameworks/hipaa.d.ts.map +1 -0
- package/dist/frameworks/hipaa.js +183 -0
- package/dist/frameworks/index.d.ts +8 -0
- package/dist/frameworks/index.d.ts.map +1 -0
- package/dist/frameworks/index.js +30 -0
- package/dist/frameworks/iso27001.d.ts +63 -0
- package/dist/frameworks/iso27001.d.ts.map +1 -0
- package/dist/frameworks/iso27001.js +331 -0
- package/dist/frameworks/nist.d.ts +62 -0
- package/dist/frameworks/nist.d.ts.map +1 -0
- package/dist/frameworks/nist.js +424 -0
- package/dist/frameworks/pci.d.ts +6 -0
- package/dist/frameworks/pci.d.ts.map +1 -0
- package/dist/frameworks/pci.js +201 -0
- package/dist/frameworks/soc2.d.ts +7 -0
- package/dist/frameworks/soc2.d.ts.map +1 -0
- package/dist/frameworks/soc2.js +248 -0
- package/dist/iac/drift-detector.d.ts +64 -0
- package/dist/iac/drift-detector.d.ts.map +1 -0
- package/dist/iac/drift-detector.js +134 -0
- package/dist/iac/index.d.ts +4 -0
- package/dist/iac/index.d.ts.map +1 -0
- package/dist/iac/index.js +19 -0
- package/dist/iac/rules.d.ts +17 -0
- package/dist/iac/rules.d.ts.map +1 -0
- package/dist/iac/rules.js +385 -0
- package/dist/iac/scanner.d.ts +104 -0
- package/dist/iac/scanner.d.ts.map +1 -0
- package/dist/iac/scanner.js +343 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/pii/data-flow.d.ts +58 -0
- package/dist/pii/data-flow.d.ts.map +1 -0
- package/dist/pii/data-flow.js +154 -0
- package/dist/pii/detector.d.ts +60 -0
- package/dist/pii/detector.d.ts.map +1 -0
- package/dist/pii/detector.js +267 -0
- package/dist/pii/index.d.ts +4 -0
- package/dist/pii/index.d.ts.map +1 -0
- package/dist/pii/index.js +19 -0
- package/dist/pii/patterns.d.ts +36 -0
- package/dist/pii/patterns.d.ts.map +1 -0
- package/dist/pii/patterns.js +108 -0
- package/dist/policy/index.d.ts +5 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +20 -0
- package/dist/policy/opa-engine.d.ts +121 -0
- package/dist/policy/opa-engine.d.ts.map +1 -0
- package/dist/policy/opa-engine.js +423 -0
- package/package.json +31 -0
- package/src/audit/emitter.ts +383 -0
- package/src/audit/events.ts +351 -0
- package/src/audit/index.ts +35 -0
- package/src/audit/storage.ts +394 -0
- package/src/automation/__tests__/compliance-scheduler.test.ts +183 -0
- package/src/automation/audit-logger.ts +629 -0
- package/src/automation/compliance-scheduler-fixed.ts +0 -0
- package/src/automation/compliance-scheduler.ts +516 -0
- package/src/automation/dashboard.ts +947 -0
- package/src/automation/email-service.ts +230 -0
- package/src/automation/evidence-collector.ts +866 -0
- package/src/automation/index.ts +8 -0
- package/src/automation/pdf-exporter.ts +434 -0
- package/src/automation/reporting-engine.ts +462 -0
- package/src/container/index.ts +3 -0
- package/src/container/kubernetes.ts +379 -0
- package/src/container/rules.ts +244 -0
- package/src/container/scanner.ts +202 -0
- package/src/frameworks/engine.ts +298 -0
- package/src/frameworks/gdpr.ts +204 -0
- package/src/frameworks/hipaa.ts +209 -0
- package/src/frameworks/index.ts +23 -0
- package/src/frameworks/iso27001.ts +398 -0
- package/src/frameworks/nist.ts +518 -0
- package/src/frameworks/pci.ts +226 -0
- package/src/frameworks/soc2.ts +281 -0
- package/src/iac/drift-detector.ts +197 -0
- package/src/iac/index.ts +3 -0
- package/src/iac/rules.ts +420 -0
- package/src/iac/scanner.ts +445 -0
- package/src/index.ts +17 -0
- package/src/pii/data-flow.ts +216 -0
- package/src/pii/detector.ts +327 -0
- package/src/pii/index.ts +3 -0
- package/src/pii/patterns.ts +128 -0
- package/src/policy/index.ts +5 -0
- package/src/policy/opa-engine.ts +504 -0
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
import { prisma } from "@guardrail/database";
|
|
2
|
+
import { auditLogger } from "./audit-logger";
|
|
3
|
+
|
|
4
|
+
interface DashboardData {
|
|
5
|
+
projectId: string;
|
|
6
|
+
overview: {
|
|
7
|
+
overallScore: number;
|
|
8
|
+
status: "compliant" | "partial" | "non-compliant";
|
|
9
|
+
lastAssessment: Date;
|
|
10
|
+
nextAssessment?: Date;
|
|
11
|
+
totalControls: number;
|
|
12
|
+
activeFrameworks: string[];
|
|
13
|
+
};
|
|
14
|
+
trends: {
|
|
15
|
+
scores: Array<{
|
|
16
|
+
date: Date;
|
|
17
|
+
score: number;
|
|
18
|
+
framework: string;
|
|
19
|
+
}>;
|
|
20
|
+
violations: Array<{
|
|
21
|
+
date: Date;
|
|
22
|
+
count: number;
|
|
23
|
+
severity: string;
|
|
24
|
+
}>;
|
|
25
|
+
remediation: Array<{
|
|
26
|
+
date: Date;
|
|
27
|
+
completed: number;
|
|
28
|
+
pending: number;
|
|
29
|
+
}>;
|
|
30
|
+
};
|
|
31
|
+
alerts: Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
type: "violation" | "deadline" | "score_drop" | "system";
|
|
34
|
+
severity: "low" | "medium" | "high" | "critical";
|
|
35
|
+
message: string;
|
|
36
|
+
timestamp: Date;
|
|
37
|
+
acknowledged: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
frameworkStatus: Array<{
|
|
40
|
+
frameworkId: string;
|
|
41
|
+
score: number;
|
|
42
|
+
status: string;
|
|
43
|
+
lastRun: Date;
|
|
44
|
+
nextRun?: Date;
|
|
45
|
+
gaps: number;
|
|
46
|
+
}>;
|
|
47
|
+
recentActivity: Array<{
|
|
48
|
+
type: string;
|
|
49
|
+
description: string;
|
|
50
|
+
timestamp: Date;
|
|
51
|
+
user?: string;
|
|
52
|
+
}>;
|
|
53
|
+
upcomingTasks: Array<{
|
|
54
|
+
id: string;
|
|
55
|
+
type: "assessment" | "remediation" | "review";
|
|
56
|
+
title: string;
|
|
57
|
+
dueDate: Date;
|
|
58
|
+
priority: "low" | "medium" | "high" | "critical";
|
|
59
|
+
assignedTo?: string;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface AlertConfig {
|
|
64
|
+
id: string;
|
|
65
|
+
projectId: string;
|
|
66
|
+
type:
|
|
67
|
+
| "score_threshold"
|
|
68
|
+
| "violation_detected"
|
|
69
|
+
| "deadline_approaching"
|
|
70
|
+
| "system_error";
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
threshold?: number;
|
|
73
|
+
recipients: {
|
|
74
|
+
email?: string[];
|
|
75
|
+
slack?: string;
|
|
76
|
+
webhook?: string;
|
|
77
|
+
};
|
|
78
|
+
conditions: any;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Compliance Dashboard and Alerting System
|
|
83
|
+
*
|
|
84
|
+
* Provides real-time compliance monitoring, dashboards,
|
|
85
|
+
* and intelligent alerting for compliance issues
|
|
86
|
+
*/
|
|
87
|
+
export class ComplianceDashboard {
|
|
88
|
+
private alertConfigs: Map<string, AlertConfig> = new Map();
|
|
89
|
+
private alertIntervals: Map<string, NodeJS.Timeout> = new Map();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get dashboard data for a project
|
|
93
|
+
*/
|
|
94
|
+
async getDashboardData(projectId: string): Promise<DashboardData> {
|
|
95
|
+
// Get project details
|
|
96
|
+
const project = await prisma.project.findUnique({
|
|
97
|
+
where: { id: projectId },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!project) {
|
|
101
|
+
throw new Error(`Project ${projectId} not found`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Get assessments
|
|
105
|
+
let assessments: any[] = [];
|
|
106
|
+
try {
|
|
107
|
+
assessments = await prisma.complianceAssessment.findMany({
|
|
108
|
+
where: { projectId },
|
|
109
|
+
orderBy: { createdAt: "desc" },
|
|
110
|
+
take: 10,
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn("Could not fetch assessments:", error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Get schedules
|
|
117
|
+
let schedules: any[] = [];
|
|
118
|
+
try {
|
|
119
|
+
schedules = await prisma.complianceSchedule.findMany({
|
|
120
|
+
where: { projectId, enabled: true },
|
|
121
|
+
orderBy: { nextRun: "asc" },
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn("Could not fetch schedules:", error);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Build dashboard data
|
|
128
|
+
const overview = this.buildOverview(assessments, schedules);
|
|
129
|
+
const trends = await this.getTrends(projectId);
|
|
130
|
+
const alerts = await this.getActiveAlerts(projectId);
|
|
131
|
+
const frameworkStatus = this.buildFrameworkStatus(assessments, schedules);
|
|
132
|
+
const recentActivity = await this.getRecentActivity(projectId);
|
|
133
|
+
const upcomingTasks = await this.getUpcomingTasks(projectId, schedules);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
projectId,
|
|
137
|
+
overview,
|
|
138
|
+
trends,
|
|
139
|
+
alerts,
|
|
140
|
+
frameworkStatus,
|
|
141
|
+
recentActivity,
|
|
142
|
+
upcomingTasks,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Configure alerts for a project
|
|
148
|
+
*/
|
|
149
|
+
async configureAlerts(config: AlertConfig): Promise<void> {
|
|
150
|
+
// Save configuration
|
|
151
|
+
try {
|
|
152
|
+
await prisma.alertConfig.upsert({
|
|
153
|
+
where: { id: config.id },
|
|
154
|
+
update: {
|
|
155
|
+
type: config.type,
|
|
156
|
+
enabled: config.enabled,
|
|
157
|
+
threshold: config.threshold,
|
|
158
|
+
// conditions not in schema
|
|
159
|
+
// conditions: config.conditions
|
|
160
|
+
},
|
|
161
|
+
create: {
|
|
162
|
+
name: `${config.type} Alert` as any,
|
|
163
|
+
projectId: config.projectId,
|
|
164
|
+
type: config.type,
|
|
165
|
+
enabled: config.enabled,
|
|
166
|
+
threshold: config.threshold,
|
|
167
|
+
config: {
|
|
168
|
+
recipients: config.recipients,
|
|
169
|
+
conditions: config.conditions,
|
|
170
|
+
} as any,
|
|
171
|
+
} as any,
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.warn("Could not save alert config to database:", error);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.alertConfigs.set(config.id, config);
|
|
178
|
+
|
|
179
|
+
// Start monitoring if enabled
|
|
180
|
+
if (config.enabled) {
|
|
181
|
+
this.startAlertMonitoring(config);
|
|
182
|
+
} else {
|
|
183
|
+
this.stopAlertMonitoring(config.id);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Trigger manual compliance check
|
|
189
|
+
*/
|
|
190
|
+
async triggerCheck(
|
|
191
|
+
projectId: string,
|
|
192
|
+
frameworkId: string,
|
|
193
|
+
userId?: string,
|
|
194
|
+
): Promise<string> {
|
|
195
|
+
// Log the trigger
|
|
196
|
+
await auditLogger.logEvent({
|
|
197
|
+
type: "compliance_check_triggered",
|
|
198
|
+
category: "compliance",
|
|
199
|
+
projectId,
|
|
200
|
+
userId,
|
|
201
|
+
timestamp: new Date(),
|
|
202
|
+
severity: "low",
|
|
203
|
+
source: "dashboard",
|
|
204
|
+
details: {
|
|
205
|
+
action: "Manual compliance check triggered",
|
|
206
|
+
framework: frameworkId,
|
|
207
|
+
triggeredBy: userId || "anonymous",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// In production, would actually trigger the compliance check
|
|
212
|
+
const executionId = `manual_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
213
|
+
|
|
214
|
+
return executionId;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Acknowledge an alert
|
|
219
|
+
*/
|
|
220
|
+
async acknowledgeAlert(alertId: string, userId: string): Promise<void> {
|
|
221
|
+
try {
|
|
222
|
+
await prisma.alert.update({
|
|
223
|
+
where: { id: alertId },
|
|
224
|
+
data: {
|
|
225
|
+
acknowledged: true,
|
|
226
|
+
acknowledgedBy: userId,
|
|
227
|
+
acknowledgedAt: new Date(),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.warn("Could not acknowledge alert in database:", error);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Log acknowledgment
|
|
235
|
+
await auditLogger.logEvent({
|
|
236
|
+
type: "alert_acknowledged",
|
|
237
|
+
category: "system",
|
|
238
|
+
timestamp: new Date(),
|
|
239
|
+
severity: "low",
|
|
240
|
+
source: "dashboard",
|
|
241
|
+
metadata: {
|
|
242
|
+
alertId,
|
|
243
|
+
acknowledgedBy: userId,
|
|
244
|
+
},
|
|
245
|
+
details: {
|
|
246
|
+
action: "Alert acknowledged",
|
|
247
|
+
alertId,
|
|
248
|
+
user: userId,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get compliance metrics for widgets
|
|
255
|
+
*/
|
|
256
|
+
async getWidgetData(
|
|
257
|
+
projectId: string,
|
|
258
|
+
widgetType: "score" | "trends" | "violations" | "remediation",
|
|
259
|
+
): Promise<any> {
|
|
260
|
+
switch (widgetType) {
|
|
261
|
+
case "score":
|
|
262
|
+
return this.getScoreWidget(projectId);
|
|
263
|
+
case "trends":
|
|
264
|
+
return this.getTrendsWidget(projectId);
|
|
265
|
+
case "violations":
|
|
266
|
+
return this.getViolationsWidget(projectId);
|
|
267
|
+
case "remediation":
|
|
268
|
+
return this.getRemediationWidget(projectId);
|
|
269
|
+
default:
|
|
270
|
+
throw new Error(`Unknown widget type: ${widgetType}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Export dashboard data
|
|
276
|
+
*/
|
|
277
|
+
async exportData(
|
|
278
|
+
projectId: string,
|
|
279
|
+
format: "json" | "csv",
|
|
280
|
+
_dateRange?: { start: Date; end: Date },
|
|
281
|
+
): Promise<string> {
|
|
282
|
+
const data = await this.getDashboardData(projectId);
|
|
283
|
+
|
|
284
|
+
if (format === "json") {
|
|
285
|
+
return JSON.stringify(data, null, 2);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (format === "csv") {
|
|
289
|
+
return this.convertToCSV(data);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Build overview section
|
|
297
|
+
*/
|
|
298
|
+
private buildOverview(
|
|
299
|
+
assessments: any[],
|
|
300
|
+
schedules: any[],
|
|
301
|
+
): DashboardData["overview"] {
|
|
302
|
+
if (assessments.length === 0) {
|
|
303
|
+
return {
|
|
304
|
+
overallScore: 0,
|
|
305
|
+
status: "non-compliant",
|
|
306
|
+
lastAssessment: new Date(),
|
|
307
|
+
totalControls: 0,
|
|
308
|
+
activeFrameworks: [],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const latest = assessments[0];
|
|
313
|
+
const frameworks = [...new Set(assessments.map((a: any) => a.frameworkId))];
|
|
314
|
+
|
|
315
|
+
// Handle JSON value type for summary
|
|
316
|
+
const summary = latest.summary as any;
|
|
317
|
+
const score = typeof summary?.score === "number" ? summary.score : 0;
|
|
318
|
+
const totalControls =
|
|
319
|
+
typeof summary?.totalControls === "number" ? summary.totalControls : 0;
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
overallScore: score,
|
|
323
|
+
status:
|
|
324
|
+
score >= 90 ? "compliant" : score >= 70 ? "partial" : "non-compliant",
|
|
325
|
+
lastAssessment: latest.createdAt,
|
|
326
|
+
nextAssessment: schedules[0]?.nextRun,
|
|
327
|
+
totalControls,
|
|
328
|
+
activeFrameworks: frameworks,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get compliance trends
|
|
334
|
+
*/
|
|
335
|
+
private async getTrends(projectId: string): Promise<DashboardData["trends"]> {
|
|
336
|
+
// Get score history
|
|
337
|
+
let scoreHistory: any[] = [];
|
|
338
|
+
try {
|
|
339
|
+
scoreHistory = await prisma.complianceAssessment.findMany({
|
|
340
|
+
where: { projectId },
|
|
341
|
+
orderBy: { createdAt: "asc" },
|
|
342
|
+
take: 30,
|
|
343
|
+
});
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.warn("Could not fetch score history:", error);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const scores = scoreHistory.map((h: any) => {
|
|
349
|
+
const summary = h.summary as any;
|
|
350
|
+
return {
|
|
351
|
+
date: h.createdAt,
|
|
352
|
+
score: typeof summary?.score === "number" ? summary.score : 0,
|
|
353
|
+
framework: h.frameworkId,
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Get violation trends
|
|
358
|
+
const violations = await this.getViolationTrends(projectId);
|
|
359
|
+
|
|
360
|
+
// Get remediation trends
|
|
361
|
+
const remediation = await this.getRemediationTrends(projectId);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
scores,
|
|
365
|
+
violations,
|
|
366
|
+
remediation,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get active alerts
|
|
372
|
+
*/
|
|
373
|
+
private async getActiveAlerts(
|
|
374
|
+
projectId: string,
|
|
375
|
+
): Promise<DashboardData["alerts"]> {
|
|
376
|
+
let alerts: any[] = [];
|
|
377
|
+
try {
|
|
378
|
+
alerts = await prisma.alert.findMany({
|
|
379
|
+
where: {
|
|
380
|
+
projectId,
|
|
381
|
+
// resolved not in schema
|
|
382
|
+
// resolved: false,
|
|
383
|
+
createdAt: {
|
|
384
|
+
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last 7 days
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
orderBy: { createdAt: "desc" },
|
|
388
|
+
take: 50,
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.warn("Could not fetch alerts from database:", error);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return alerts.map((a: any) => ({
|
|
395
|
+
id: a.id,
|
|
396
|
+
type: a.type as any,
|
|
397
|
+
severity: a.severity.toLowerCase() as any,
|
|
398
|
+
message: a.message,
|
|
399
|
+
timestamp: a.createdAt,
|
|
400
|
+
acknowledged: a.acknowledged,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Build framework status
|
|
406
|
+
*/
|
|
407
|
+
private buildFrameworkStatus(
|
|
408
|
+
assessments: any[],
|
|
409
|
+
schedules: any[],
|
|
410
|
+
): DashboardData["frameworkStatus"] {
|
|
411
|
+
const statusMap = new Map<string, any>();
|
|
412
|
+
|
|
413
|
+
// Group assessments by framework
|
|
414
|
+
for (const assessment of assessments) {
|
|
415
|
+
if (!statusMap.has(assessment.frameworkId)) {
|
|
416
|
+
statusMap.set(assessment.frameworkId, assessment);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Build status array
|
|
421
|
+
const frameworkStatus: DashboardData["frameworkStatus"] = [];
|
|
422
|
+
|
|
423
|
+
for (const [frameworkId, assessment] of statusMap) {
|
|
424
|
+
const schedule = schedules.find((s) => s.frameworkId === frameworkId);
|
|
425
|
+
|
|
426
|
+
// Handle JSON value type for summary
|
|
427
|
+
const summary = assessment.summary as any;
|
|
428
|
+
const score = typeof summary?.score === "number" ? summary.score : 0;
|
|
429
|
+
|
|
430
|
+
frameworkStatus.push({
|
|
431
|
+
frameworkId,
|
|
432
|
+
score,
|
|
433
|
+
status:
|
|
434
|
+
score >= 90 ? "compliant" : score >= 70 ? "partial" : "non-compliant",
|
|
435
|
+
lastRun: assessment.createdAt,
|
|
436
|
+
nextRun: schedule?.nextRun,
|
|
437
|
+
gaps: assessment.gaps?.length || 0,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return frameworkStatus;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get recent activity
|
|
446
|
+
*/
|
|
447
|
+
private async getRecentActivity(
|
|
448
|
+
projectId: string,
|
|
449
|
+
): Promise<DashboardData["recentActivity"]> {
|
|
450
|
+
let activity: any[] = [];
|
|
451
|
+
try {
|
|
452
|
+
// Try to get from audit events table
|
|
453
|
+
activity = await prisma.auditEvent.findMany({
|
|
454
|
+
where: {
|
|
455
|
+
projectId,
|
|
456
|
+
timestamp: {
|
|
457
|
+
gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last 7 days
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
orderBy: { timestamp: "desc" },
|
|
461
|
+
take: 20,
|
|
462
|
+
});
|
|
463
|
+
} catch (error) {
|
|
464
|
+
console.warn("Could not fetch recent activity from audit events:", error);
|
|
465
|
+
// Return empty array if table doesn't exist
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return activity.map((a: any) => ({
|
|
470
|
+
type: a.type,
|
|
471
|
+
description: `${a.type} - ${a.category}`,
|
|
472
|
+
timestamp: a.timestamp,
|
|
473
|
+
user: a.userId,
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Get upcoming tasks
|
|
479
|
+
*/
|
|
480
|
+
private async getUpcomingTasks(
|
|
481
|
+
projectId: string,
|
|
482
|
+
schedules: any[],
|
|
483
|
+
): Promise<DashboardData["upcomingTasks"]> {
|
|
484
|
+
const tasks: DashboardData["upcomingTasks"] = [];
|
|
485
|
+
|
|
486
|
+
// Add scheduled assessments
|
|
487
|
+
for (const schedule of schedules) {
|
|
488
|
+
if (schedule.nextRun) {
|
|
489
|
+
tasks.push({
|
|
490
|
+
id: `sched_${schedule.id}`,
|
|
491
|
+
type: "assessment" as const,
|
|
492
|
+
title: `Scheduled ${schedule.frameworkId} assessment`,
|
|
493
|
+
dueDate: schedule.nextRun,
|
|
494
|
+
priority: "medium" as const,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Add remediation tasks
|
|
500
|
+
let remediations: any[] = [];
|
|
501
|
+
try {
|
|
502
|
+
// @ts-ignore - remediationTask may not exist in schema
|
|
503
|
+
remediations = await prisma.remediationTask.findMany({
|
|
504
|
+
where: {
|
|
505
|
+
projectId,
|
|
506
|
+
status: "pending",
|
|
507
|
+
dueDate: {
|
|
508
|
+
gte: new Date(),
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
orderBy: { dueDate: "asc" },
|
|
512
|
+
take: 10,
|
|
513
|
+
});
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.warn("Could not fetch remediation tasks:", error);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
for (const task of remediations) {
|
|
519
|
+
tasks.push({
|
|
520
|
+
id: task.id,
|
|
521
|
+
type: "remediation" as const,
|
|
522
|
+
title: task.title,
|
|
523
|
+
dueDate: task.dueDate,
|
|
524
|
+
priority: task.priority?.toLowerCase() as any,
|
|
525
|
+
assignedTo: task.assignedTo,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return tasks.sort((a, b) => a.dueDate.getTime() - b.dueDate.getTime());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Start alert monitoring
|
|
534
|
+
*/
|
|
535
|
+
private startAlertMonitoring(config: AlertConfig): void {
|
|
536
|
+
// Clear existing interval
|
|
537
|
+
this.stopAlertMonitoring(config.id);
|
|
538
|
+
|
|
539
|
+
// Check every 5 minutes
|
|
540
|
+
const interval = setInterval(
|
|
541
|
+
async () => {
|
|
542
|
+
await this.checkAlertConditions(config);
|
|
543
|
+
},
|
|
544
|
+
5 * 60 * 1000,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
this.alertIntervals.set(config.id, interval);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Stop alert monitoring
|
|
552
|
+
*/
|
|
553
|
+
private stopAlertMonitoring(configId: string): void {
|
|
554
|
+
if (this.alertIntervals.has(configId)) {
|
|
555
|
+
clearInterval(this.alertIntervals.get(configId)!);
|
|
556
|
+
this.alertIntervals.delete(configId);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Check alert conditions
|
|
562
|
+
*/
|
|
563
|
+
private async checkAlertConditions(config: AlertConfig): Promise<void> {
|
|
564
|
+
switch (config.type) {
|
|
565
|
+
case "score_threshold":
|
|
566
|
+
await this.checkScoreThreshold(config);
|
|
567
|
+
break;
|
|
568
|
+
case "violation_detected":
|
|
569
|
+
await this.checkViolations(config);
|
|
570
|
+
break;
|
|
571
|
+
case "deadline_approaching":
|
|
572
|
+
await this.checkDeadlines(config);
|
|
573
|
+
break;
|
|
574
|
+
case "system_error":
|
|
575
|
+
await this.checkSystemErrors(config);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Check score threshold
|
|
582
|
+
*/
|
|
583
|
+
private async checkScoreThreshold(config: AlertConfig): Promise<void> {
|
|
584
|
+
const latest = await prisma.complianceAssessment.findFirst({
|
|
585
|
+
where: { projectId: config.projectId },
|
|
586
|
+
orderBy: { createdAt: "desc" },
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
if (latest) {
|
|
590
|
+
const summary = latest.summary as any;
|
|
591
|
+
const score = typeof summary?.score === "number" ? summary.score : 0;
|
|
592
|
+
|
|
593
|
+
if (score < (config.threshold || 70)) {
|
|
594
|
+
await this.createAlert({
|
|
595
|
+
projectId: config.projectId,
|
|
596
|
+
type: "score_drop",
|
|
597
|
+
severity: "high",
|
|
598
|
+
message: `Compliance score dropped to ${score}%`,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Check for violations
|
|
606
|
+
*/
|
|
607
|
+
private async checkViolations(config: AlertConfig): Promise<void> {
|
|
608
|
+
const recentViolations = await prisma.auditEvent.count({
|
|
609
|
+
where: {
|
|
610
|
+
projectId: config.projectId,
|
|
611
|
+
type: "compliance_violation",
|
|
612
|
+
timestamp: {
|
|
613
|
+
gte: new Date(Date.now() - 24 * 60 * 60 * 1000), // Last 24 hours
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
if (recentViolations > 0) {
|
|
619
|
+
await this.createAlert({
|
|
620
|
+
projectId: config.projectId,
|
|
621
|
+
type: "violation",
|
|
622
|
+
severity: "high",
|
|
623
|
+
message: `${recentViolations} compliance violations detected in the last 24 hours`,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Check approaching deadlines
|
|
630
|
+
*/
|
|
631
|
+
private async checkDeadlines(config: AlertConfig): Promise<void> {
|
|
632
|
+
const upcoming = await prisma.complianceSchedule.findMany({
|
|
633
|
+
where: {
|
|
634
|
+
projectId: config.projectId,
|
|
635
|
+
nextRun: {
|
|
636
|
+
lte: new Date(Date.now() + 24 * 60 * 60 * 1000), // Next 24 hours
|
|
637
|
+
gte: new Date(),
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
for (const schedule of upcoming) {
|
|
643
|
+
await this.createAlert({
|
|
644
|
+
projectId: config.projectId,
|
|
645
|
+
type: "deadline",
|
|
646
|
+
severity: "medium",
|
|
647
|
+
message: `${schedule.frameworkId} assessment scheduled for ${schedule.nextRun}`,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Check for system errors
|
|
654
|
+
*/
|
|
655
|
+
private async checkSystemErrors(config: AlertConfig): Promise<void> {
|
|
656
|
+
const errors = await prisma.auditEvent.count({
|
|
657
|
+
where: {
|
|
658
|
+
projectId: config.projectId,
|
|
659
|
+
type: "compliance_check_failed",
|
|
660
|
+
timestamp: {
|
|
661
|
+
gte: new Date(Date.now() - 60 * 60 * 1000), // Last hour
|
|
662
|
+
},
|
|
663
|
+
},
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
if (errors > 0) {
|
|
667
|
+
await this.createAlert({
|
|
668
|
+
projectId: config.projectId,
|
|
669
|
+
type: "system",
|
|
670
|
+
severity: "critical",
|
|
671
|
+
message: `${errors} compliance check failures in the last hour`,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Create an alert
|
|
678
|
+
*/
|
|
679
|
+
private async createAlert(alert: {
|
|
680
|
+
projectId: string;
|
|
681
|
+
type: string;
|
|
682
|
+
severity: string;
|
|
683
|
+
message: string;
|
|
684
|
+
}): Promise<void> {
|
|
685
|
+
// Check if similar alert already exists
|
|
686
|
+
try {
|
|
687
|
+
// @ts-ignore - resolved field may not exist in schema
|
|
688
|
+
const existing = await prisma.alert.findFirst({
|
|
689
|
+
where: {
|
|
690
|
+
projectId: alert.projectId,
|
|
691
|
+
type: alert.type,
|
|
692
|
+
message: alert.message,
|
|
693
|
+
// resolved not in schema
|
|
694
|
+
// resolved: false,
|
|
695
|
+
createdAt: {
|
|
696
|
+
gte: new Date(Date.now() - 60 * 60 * 1000), // Last hour
|
|
697
|
+
},
|
|
698
|
+
},
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
if (existing) return;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
// resolved field may not exist - skip duplicate check
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Create new alert
|
|
707
|
+
try {
|
|
708
|
+
// @ts-ignore - resolved field may not exist in schema
|
|
709
|
+
await prisma.alert.create({
|
|
710
|
+
data: {
|
|
711
|
+
id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
712
|
+
projectId: alert.projectId,
|
|
713
|
+
type: alert.type,
|
|
714
|
+
severity: alert.severity.toUpperCase(),
|
|
715
|
+
title: alert.message as any,
|
|
716
|
+
message: alert.message,
|
|
717
|
+
acknowledged: false,
|
|
718
|
+
// resolved not in schema
|
|
719
|
+
// resolved: false
|
|
720
|
+
} as any,
|
|
721
|
+
});
|
|
722
|
+
} catch (error) {
|
|
723
|
+
// resolved field may not exist - create without it
|
|
724
|
+
await prisma.alert.create({
|
|
725
|
+
data: {
|
|
726
|
+
id: `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
727
|
+
projectId: alert.projectId,
|
|
728
|
+
type: alert.type,
|
|
729
|
+
severity: alert.severity.toUpperCase(),
|
|
730
|
+
title: alert.message as any,
|
|
731
|
+
message: alert.message,
|
|
732
|
+
acknowledged: false,
|
|
733
|
+
} as any,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Get violation trends
|
|
740
|
+
*/
|
|
741
|
+
private async getViolationTrends(projectId: string): Promise<any[]> {
|
|
742
|
+
let violations: any[] = [];
|
|
743
|
+
try {
|
|
744
|
+
violations = await prisma.auditEvent.findMany({
|
|
745
|
+
where: {
|
|
746
|
+
projectId,
|
|
747
|
+
type: "compliance_violation",
|
|
748
|
+
timestamp: {
|
|
749
|
+
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Last 30 days
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
orderBy: { timestamp: "asc" },
|
|
753
|
+
});
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.warn("Could not fetch violation trends:", error);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Group by day
|
|
759
|
+
const daily = violations.reduce(
|
|
760
|
+
(acc: any, v: any) => {
|
|
761
|
+
const day = v.timestamp.toISOString().split("T")[0];
|
|
762
|
+
if (!acc[day]) acc[day] = 0;
|
|
763
|
+
acc[day]++;
|
|
764
|
+
return acc;
|
|
765
|
+
},
|
|
766
|
+
{} as Record<string, number>,
|
|
767
|
+
);
|
|
768
|
+
|
|
769
|
+
return Object.entries(daily).map(([date, count]) => ({
|
|
770
|
+
date: new Date(date),
|
|
771
|
+
count,
|
|
772
|
+
severity: "high", // Simplified
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Get remediation trends
|
|
777
|
+
*/
|
|
778
|
+
private async getRemediationTrends(projectId: string): Promise<any[]> {
|
|
779
|
+
let remediations: any[] = [];
|
|
780
|
+
try {
|
|
781
|
+
remediations = await prisma.auditEvent.findMany({
|
|
782
|
+
where: {
|
|
783
|
+
projectId,
|
|
784
|
+
type: "remediation_performed",
|
|
785
|
+
timestamp: {
|
|
786
|
+
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
orderBy: { timestamp: "asc" },
|
|
790
|
+
});
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.warn("Could not fetch remediation trends:", error);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Group by day
|
|
796
|
+
const daily = remediations.reduce(
|
|
797
|
+
(acc: any, r: any) => {
|
|
798
|
+
const day = r.timestamp.toISOString().split("T")[0];
|
|
799
|
+
if (!acc[day]) acc[day] = { completed: 0, pending: 0 };
|
|
800
|
+
acc[day].completed++;
|
|
801
|
+
return acc;
|
|
802
|
+
},
|
|
803
|
+
{} as Record<string, any>,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
return Object.entries(daily).map(([date, data]: [string, any]) => ({
|
|
807
|
+
date: new Date(date),
|
|
808
|
+
completed: data.completed,
|
|
809
|
+
pending: data.pending || 0,
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Widget data getters
|
|
815
|
+
*/
|
|
816
|
+
private async getScoreWidget(projectId: string): Promise<any> {
|
|
817
|
+
try {
|
|
818
|
+
const latest = await prisma.complianceAssessment.findFirst({
|
|
819
|
+
where: { projectId },
|
|
820
|
+
orderBy: { createdAt: "desc" },
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
const summary = latest?.summary as any;
|
|
824
|
+
const score = typeof summary?.score === "number" ? summary.score : 0;
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
score,
|
|
828
|
+
status:
|
|
829
|
+
score >= 90 ? "compliant" : score >= 70 ? "partial" : "non-compliant",
|
|
830
|
+
lastUpdated: latest?.createdAt,
|
|
831
|
+
};
|
|
832
|
+
} catch (error) {
|
|
833
|
+
console.warn("Could not get score widget:", error);
|
|
834
|
+
return {
|
|
835
|
+
score: 0,
|
|
836
|
+
status: "non-compliant",
|
|
837
|
+
lastUpdated: new Date(),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private async getTrendsWidget(projectId: string): Promise<any> {
|
|
843
|
+
const trends = await this.getTrends(projectId);
|
|
844
|
+
return {
|
|
845
|
+
scores: trends.scores.slice(-7), // Last 7 days
|
|
846
|
+
trend: this.calculateTrend(trends.scores.map((s: any) => s.score)),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private async getViolationsWidget(projectId: string): Promise<any> {
|
|
851
|
+
const violations = await this.getViolationTrends(projectId);
|
|
852
|
+
return {
|
|
853
|
+
total: violations.reduce((sum: number, v: any) => sum + v.count, 0),
|
|
854
|
+
recent: violations.slice(-7), // Last 7 days
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private async getRemediationWidget(projectId: string): Promise<any> {
|
|
859
|
+
try {
|
|
860
|
+
let pending = 0;
|
|
861
|
+
let completed = 0;
|
|
862
|
+
|
|
863
|
+
// Try to get from database, but handle missing table
|
|
864
|
+
try {
|
|
865
|
+
// @ts-ignore - remediationTask may not exist in schema
|
|
866
|
+
pending = await prisma.remediationTask.count({
|
|
867
|
+
where: { projectId, status: "pending" },
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// @ts-ignore - remediationTask may not exist in schema
|
|
871
|
+
completed = await prisma.remediationTask.count({
|
|
872
|
+
where: {
|
|
873
|
+
projectId,
|
|
874
|
+
status: "completed",
|
|
875
|
+
completedAt: {
|
|
876
|
+
gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
} catch (error) {
|
|
881
|
+
console.warn("Remediation tasks table not available:", error);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
pending,
|
|
886
|
+
completedThisMonth: completed,
|
|
887
|
+
};
|
|
888
|
+
} catch (error) {
|
|
889
|
+
console.warn("Could not get remediation widget:", error);
|
|
890
|
+
return {
|
|
891
|
+
pending: 0,
|
|
892
|
+
completedThisMonth: 0,
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Calculate trend direction
|
|
899
|
+
*/
|
|
900
|
+
private calculateTrend(scores: number[]): "up" | "down" | "stable" {
|
|
901
|
+
if (scores.length < 2) return "stable";
|
|
902
|
+
|
|
903
|
+
const recent = scores.slice(-3);
|
|
904
|
+
const older = scores.slice(-6, -3);
|
|
905
|
+
|
|
906
|
+
const recentAvg =
|
|
907
|
+
recent.reduce((a: number, b: number) => a + b, 0) / recent.length;
|
|
908
|
+
const olderAvg =
|
|
909
|
+
older.reduce((a: number, b: number) => a + b, 0) / older.length;
|
|
910
|
+
|
|
911
|
+
if (recentAvg > olderAvg + 5) return "up";
|
|
912
|
+
if (recentAvg < olderAvg - 5) return "down";
|
|
913
|
+
return "stable";
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Convert dashboard data to CSV
|
|
918
|
+
*/
|
|
919
|
+
private convertToCSV(data: DashboardData): string {
|
|
920
|
+
const rows = [
|
|
921
|
+
["Metric", "Value"],
|
|
922
|
+
["Project ID", data.projectId],
|
|
923
|
+
["Overall Score", data.overview.overallScore.toString()],
|
|
924
|
+
["Status", data.overview.status],
|
|
925
|
+
["Last Assessment", data.overview.lastAssessment.toISOString()],
|
|
926
|
+
["Total Controls", data.overview.totalControls.toString()],
|
|
927
|
+
["Active Frameworks", data.overview.activeFrameworks.join(", ")],
|
|
928
|
+
["Total Alerts", data.alerts.length.toString()],
|
|
929
|
+
["Upcoming Tasks", data.upcomingTasks.length.toString()],
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
return rows.map((row) => row.join(",")).join("\n");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Shutdown dashboard monitoring
|
|
937
|
+
*/
|
|
938
|
+
async shutdown(): Promise<void> {
|
|
939
|
+
for (const interval of this.alertIntervals.values()) {
|
|
940
|
+
clearInterval(interval);
|
|
941
|
+
}
|
|
942
|
+
this.alertIntervals.clear();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Export singleton instance
|
|
947
|
+
export const complianceDashboard = new ComplianceDashboard();
|