thuban 0.3.2 → 0.3.3
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/README.md +137 -102
- package/cli.js +310 -95
- package/package.json +5 -2
- package/packages/scanner/alert-manager.js +398 -398
- package/packages/scanner/code-scanner.js +758 -713
- package/packages/scanner/copy-paste-detector.js +33 -7
- package/packages/scanner/dependency-graph.js +541 -536
- package/packages/scanner/drift-detector.js +423 -423
- package/packages/scanner/file-collector.js +64 -0
- package/packages/scanner/file-watcher.js +323 -323
- package/packages/scanner/ghost-code-detector.js +80 -5
- package/packages/scanner/hallucination-detector.js +189 -19
- package/packages/scanner/health-checker.js +586 -586
- package/packages/scanner/index.js +100 -92
- package/packages/scanner/license-manager.js +15 -2
- package/packages/scanner/master-health-checker.js +513 -513
- package/packages/scanner/monitor-notifier.js +64 -0
- package/packages/scanner/monitor-service.js +103 -0
- package/packages/scanner/monitor-store.js +117 -0
- package/packages/scanner/pre-commit-gate.js +1 -1
- package/packages/scanner/scan-diff.js +50 -0
- package/packages/scanner/scan-runner.js +172 -0
- package/packages/scanner/secret-scanner.js +612 -0
- package/packages/scanner/secret-scanner.test.js +103 -0
- package/packages/scanner/sentinel-core.js +616 -608
- package/packages/scanner/sentinel-knowledge.js +322 -322
- package/packages/scanner/tech-debt-analyzer.js +46 -28
- package/packages/scanner/widget-generator.js +415 -415
|
@@ -1,398 +1,398 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Alert Manager
|
|
3
|
-
*
|
|
4
|
-
* @purpose Event-driven AlertManager component
|
|
5
|
-
* @module sentinel
|
|
6
|
-
* @layer application
|
|
7
|
-
* @imports-from fs, path, events
|
|
8
|
-
* @exports-to sentinel
|
|
9
|
-
* @side-effects File system writes, File system reads, Console output, Event emission
|
|
10
|
-
* @critical-for sentinel
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Sentinel Alert Manager
|
|
15
|
-
*
|
|
16
|
-
* Handles alerting and reporting for the Sentinel system.
|
|
17
|
-
* Outputs: Console, files, and future integrations (Slack, email, GitHub issues)
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const EventEmitter = require('events');
|
|
23
|
-
|
|
24
|
-
class AlertManager extends EventEmitter {
|
|
25
|
-
constructor(config = {}) {
|
|
26
|
-
super();
|
|
27
|
-
|
|
28
|
-
this.config = {
|
|
29
|
-
rootPath: config.rootPath || process.cwd(),
|
|
30
|
-
alertsDir: config.alertsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'alerts'),
|
|
31
|
-
reportsDir: config.reportsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'reports'),
|
|
32
|
-
consoleOutput: config.consoleOutput !== false,
|
|
33
|
-
fileOutput: config.fileOutput !== false,
|
|
34
|
-
maxAlertsPerFile: config.maxAlertsPerFile || 1000,
|
|
35
|
-
slackWebhook: config.slackWebhook || null,
|
|
36
|
-
emailConfig: config.emailConfig || null,
|
|
37
|
-
...config
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Ensure directories exist
|
|
41
|
-
this._ensureDirectories();
|
|
42
|
-
|
|
43
|
-
// Alert history for deduplication
|
|
44
|
-
this.recentAlerts = new Map();
|
|
45
|
-
this.alertDedupeWindowMs = config.alertDedupeWindowMs || 60000; // 1 minute
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Send an alert
|
|
50
|
-
*/
|
|
51
|
-
async sendAlert(alert) {
|
|
52
|
-
const normalizedAlert = this._normalizeAlert(alert);
|
|
53
|
-
|
|
54
|
-
// Check for duplicate
|
|
55
|
-
if (this._isDuplicate(normalizedAlert)) {
|
|
56
|
-
return { sent: false, reason: 'duplicate' };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Record alert
|
|
60
|
-
this._recordAlert(normalizedAlert);
|
|
61
|
-
|
|
62
|
-
// Output to configured channels
|
|
63
|
-
const results = {
|
|
64
|
-
console: false,
|
|
65
|
-
file: false,
|
|
66
|
-
slack: false,
|
|
67
|
-
email: false
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Console output
|
|
71
|
-
if (this.config.consoleOutput) {
|
|
72
|
-
results.console = this._outputToConsole(normalizedAlert);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// File output
|
|
76
|
-
if (this.config.fileOutput) {
|
|
77
|
-
results.file = await this._outputToFile(normalizedAlert);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Slack output (if configured and critical/error)
|
|
81
|
-
if (this.config.slackWebhook && ['critical', 'error'].includes(normalizedAlert.level)) {
|
|
82
|
-
results.slack = await this._outputToSlack(normalizedAlert);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.emit('alertSent', { alert: normalizedAlert, results });
|
|
86
|
-
|
|
87
|
-
return { sent: true, alert: normalizedAlert, results };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Send daily digest
|
|
92
|
-
*/
|
|
93
|
-
async sendDigest(digest) {
|
|
94
|
-
console.log('\n' + '='.repeat(60));
|
|
95
|
-
console.log('[SENTINEL] DAILY DIGEST - ' + digest.date);
|
|
96
|
-
console.log('='.repeat(60));
|
|
97
|
-
|
|
98
|
-
console.log(`\nSummary:`);
|
|
99
|
-
console.log(` Issues Found: ${digest.summary.issuesFound}`);
|
|
100
|
-
console.log(` Critical: ${digest.summary.criticalIssues}`);
|
|
101
|
-
console.log(` Warnings: ${digest.summary.warningIssues}`);
|
|
102
|
-
console.log(` Files Watched: ${digest.summary.filesWatched}`);
|
|
103
|
-
console.log(` Last Scan: ${digest.summary.lastScan || 'N/A'}`);
|
|
104
|
-
|
|
105
|
-
if (digest.topIssues && digest.topIssues.length > 0) {
|
|
106
|
-
console.log(`\nTop Issues:`);
|
|
107
|
-
for (const issue of digest.topIssues.slice(0, 5)) {
|
|
108
|
-
console.log(` [${issue.severity.toUpperCase()}] ${issue.message}`);
|
|
109
|
-
if (issue.file) {
|
|
110
|
-
console.log(` File: ${path.relative(this.config.rootPath, issue.file)}`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (digest.recommendations && digest.recommendations.length > 0) {
|
|
116
|
-
console.log(`\nRecommendations:`);
|
|
117
|
-
for (const rec of digest.recommendations) {
|
|
118
|
-
console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
console.log('\n' + '='.repeat(60) + '\n');
|
|
123
|
-
|
|
124
|
-
// Save digest to file
|
|
125
|
-
if (this.config.fileOutput) {
|
|
126
|
-
const digestPath = path.join(
|
|
127
|
-
this.config.reportsDir,
|
|
128
|
-
`digest-${digest.date}.json`
|
|
129
|
-
);
|
|
130
|
-
fs.writeFileSync(digestPath, JSON.stringify(digest, null, 2));
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
this.emit('digestSent', digest);
|
|
134
|
-
return { sent: true };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Send weekly report
|
|
139
|
-
*/
|
|
140
|
-
async sendWeeklyReport(report) {
|
|
141
|
-
console.log('\n' + '='.repeat(70));
|
|
142
|
-
console.log('[SENTINEL] WEEKLY REPORT - Week of ' + report.weekOf);
|
|
143
|
-
console.log('='.repeat(70));
|
|
144
|
-
|
|
145
|
-
console.log(`\nMetrics:`);
|
|
146
|
-
console.log(` Total Scans: ${report.summary.totalScans}`);
|
|
147
|
-
console.log(` Issues Detected: ${report.summary.issuesDetected}`);
|
|
148
|
-
console.log(` Issues Resolved: ${report.summary.issuesResolved}`);
|
|
149
|
-
console.log(` Avg Scan Time: ${report.summary.averageScanTime}ms`);
|
|
150
|
-
|
|
151
|
-
console.log(`\nCode Quality:`);
|
|
152
|
-
console.log(` Score: ${report.codeQuality.score}/100 (${report.codeQuality.grade})`);
|
|
153
|
-
console.log(` Complexity: ${report.codeQuality.factors.complexity}`);
|
|
154
|
-
console.log(` Maintainability: ${report.codeQuality.factors.maintainability}`);
|
|
155
|
-
console.log(` Test Coverage: ${report.codeQuality.factors.testCoverage}%`);
|
|
156
|
-
|
|
157
|
-
console.log(`\nTechnical Debt:`);
|
|
158
|
-
console.log(` Estimated Hours: ${report.technicalDebt.estimatedHours}`);
|
|
159
|
-
console.log(` Refactoring: ${report.technicalDebt.categories.refactoring}h`);
|
|
160
|
-
console.log(` Documentation: ${report.technicalDebt.categories.documentation}h`);
|
|
161
|
-
console.log(` Testing: ${report.technicalDebt.categories.testing}h`);
|
|
162
|
-
|
|
163
|
-
if (report.recommendations && report.recommendations.length > 0) {
|
|
164
|
-
console.log(`\nRecommendations:`);
|
|
165
|
-
for (const rec of report.recommendations) {
|
|
166
|
-
console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
console.log('\n' + '='.repeat(70) + '\n');
|
|
171
|
-
|
|
172
|
-
// Save report to file
|
|
173
|
-
if (this.config.fileOutput) {
|
|
174
|
-
const reportPath = path.join(
|
|
175
|
-
this.config.reportsDir,
|
|
176
|
-
`weekly-report-${report.weekOf}.json`
|
|
177
|
-
);
|
|
178
|
-
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
this.emit('weeklyReportSent', report);
|
|
182
|
-
return { sent: true };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Get alert history
|
|
187
|
-
*/
|
|
188
|
-
getAlertHistory(options = {}) {
|
|
189
|
-
const { days = 7, level = null, category = null } = options;
|
|
190
|
-
|
|
191
|
-
const cutoff = new Date();
|
|
192
|
-
cutoff.setDate(cutoff.getDate() - days);
|
|
193
|
-
|
|
194
|
-
// Read from alert files
|
|
195
|
-
const alerts = [];
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
if (fs.existsSync(this.config.alertsDir)) {
|
|
199
|
-
const files = fs.readdirSync(this.config.alertsDir)
|
|
200
|
-
.filter(f => f.startsWith('alerts-') && f.endsWith('.json'))
|
|
201
|
-
.sort()
|
|
202
|
-
.reverse();
|
|
203
|
-
|
|
204
|
-
for (const file of files) {
|
|
205
|
-
const fileDate = file.match(/alerts-(\d{4}-\d{2}-\d{2})/);
|
|
206
|
-
if (fileDate && new Date(fileDate[1]) < cutoff) {
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
try {
|
|
211
|
-
const content = JSON.parse(fs.readFileSync(
|
|
212
|
-
path.join(this.config.alertsDir, file),
|
|
213
|
-
'utf8'
|
|
214
|
-
));
|
|
215
|
-
alerts.push(...(content.alerts || []));
|
|
216
|
-
} catch (e) {
|
|
217
|
-
// Skip corrupted files
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
} catch (error) {
|
|
222
|
-
console.error('[ALERT-MANAGER] Error reading alert history:', error.message);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Filter
|
|
226
|
-
let filtered = alerts;
|
|
227
|
-
if (level) {
|
|
228
|
-
filtered = filtered.filter(a => a.level === level);
|
|
229
|
-
}
|
|
230
|
-
if (category) {
|
|
231
|
-
filtered = filtered.filter(a => a.category === category);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return filtered;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Clear old alerts
|
|
239
|
-
*/
|
|
240
|
-
async clearOldAlerts(daysToKeep = 30) {
|
|
241
|
-
const cutoff = new Date();
|
|
242
|
-
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
243
|
-
|
|
244
|
-
let deletedCount = 0;
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
if (fs.existsSync(this.config.alertsDir)) {
|
|
248
|
-
const files = fs.readdirSync(this.config.alertsDir);
|
|
249
|
-
|
|
250
|
-
for (const file of files) {
|
|
251
|
-
const fileDate = file.match(/(\d{4}-\d{2}-\d{2})/);
|
|
252
|
-
if (fileDate && new Date(fileDate[1]) < cutoff) {
|
|
253
|
-
fs.unlinkSync(path.join(this.config.alertsDir, file));
|
|
254
|
-
deletedCount++;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
} catch (error) {
|
|
259
|
-
console.error('[ALERT-MANAGER] Error clearing old alerts:', error.message);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return { deleted: deletedCount };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Private methods
|
|
266
|
-
|
|
267
|
-
_ensureDirectories() {
|
|
268
|
-
try {
|
|
269
|
-
if (!fs.existsSync(this.config.alertsDir)) {
|
|
270
|
-
fs.mkdirSync(this.config.alertsDir, { recursive: true });
|
|
271
|
-
}
|
|
272
|
-
if (!fs.existsSync(this.config.reportsDir)) {
|
|
273
|
-
fs.mkdirSync(this.config.reportsDir, { recursive: true });
|
|
274
|
-
}
|
|
275
|
-
} catch (error) {
|
|
276
|
-
console.warn('[ALERT-MANAGER] Could not create directories:', error.message);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
_normalizeAlert(alert) {
|
|
281
|
-
return {
|
|
282
|
-
id: alert.id || this._generateAlertId(),
|
|
283
|
-
timestamp: alert.timestamp || new Date().toISOString(),
|
|
284
|
-
level: alert.level || 'info',
|
|
285
|
-
category: alert.category || 'general',
|
|
286
|
-
title: alert.title || 'Alert',
|
|
287
|
-
message: alert.message || '',
|
|
288
|
-
file: alert.file || null,
|
|
289
|
-
line: alert.line || null,
|
|
290
|
-
details: alert.details || {}
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
_generateAlertId() {
|
|
295
|
-
return `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
_isDuplicate(alert) {
|
|
299
|
-
const key = `${alert.level}:${alert.message}:${alert.file}`;
|
|
300
|
-
const lastSeen = this.recentAlerts.get(key);
|
|
301
|
-
|
|
302
|
-
if (lastSeen && (Date.now() - lastSeen) < this.alertDedupeWindowMs) {
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
_recordAlert(alert) {
|
|
310
|
-
const key = `${alert.level}:${alert.message}:${alert.file}`;
|
|
311
|
-
this.recentAlerts.set(key, Date.now());
|
|
312
|
-
|
|
313
|
-
// Cleanup old entries
|
|
314
|
-
const cutoff = Date.now() - this.alertDedupeWindowMs;
|
|
315
|
-
for (const [k, v] of this.recentAlerts) {
|
|
316
|
-
if (v < cutoff) {
|
|
317
|
-
this.recentAlerts.delete(k);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
_outputToConsole(alert) {
|
|
323
|
-
const levelColors = {
|
|
324
|
-
critical: '\x1b[31m', // Red
|
|
325
|
-
error: '\x1b[31m', // Red
|
|
326
|
-
warning: '\x1b[33m', // Yellow
|
|
327
|
-
info: '\x1b[36m' // Cyan
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
const reset = '\x1b[0m';
|
|
331
|
-
const color = levelColors[alert.level] || '';
|
|
332
|
-
|
|
333
|
-
console.log(`${color}[SENTINEL:${alert.level.toUpperCase()}]${reset} ${alert.title}`);
|
|
334
|
-
console.log(` ${alert.message}`);
|
|
335
|
-
|
|
336
|
-
if (alert.file) {
|
|
337
|
-
const relPath = path.relative(this.config.rootPath, alert.file);
|
|
338
|
-
console.log(` File: ${relPath}${alert.line ? ':' + alert.line : ''}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async _outputToFile(alert) {
|
|
345
|
-
try {
|
|
346
|
-
const dateStr = new Date().toISOString().split('T')[0];
|
|
347
|
-
const filePath = path.join(this.config.alertsDir, `alerts-${dateStr}.json`);
|
|
348
|
-
|
|
349
|
-
let fileData = { date: dateStr, alerts: [] };
|
|
350
|
-
|
|
351
|
-
if (fs.existsSync(filePath)) {
|
|
352
|
-
try {
|
|
353
|
-
fileData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
354
|
-
} catch (e) {
|
|
355
|
-
// File corrupted, start fresh
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
fileData.alerts.push(alert);
|
|
360
|
-
|
|
361
|
-
// Limit alerts per file
|
|
362
|
-
if (fileData.alerts.length > this.config.maxAlertsPerFile) {
|
|
363
|
-
fileData.alerts = fileData.alerts.slice(-this.config.maxAlertsPerFile);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
fs.writeFileSync(filePath, JSON.stringify(fileData, null, 2));
|
|
367
|
-
return true;
|
|
368
|
-
} catch (error) {
|
|
369
|
-
console.error('[ALERT-MANAGER] File output failed:', error.message);
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async _outputToSlack(alert) {
|
|
375
|
-
if (!this.config.slackWebhook) {
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
try {
|
|
380
|
-
// Would use fetch or axios here
|
|
381
|
-
// For now, just log that we would send to Slack
|
|
382
|
-
console.log(`[ALERT-MANAGER] Would send to Slack: ${alert.title}`);
|
|
383
|
-
return true;
|
|
384
|
-
} catch (error) {
|
|
385
|
-
console.error('[ALERT-MANAGER] Slack output failed:', error.message);
|
|
386
|
-
return false;
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
async _createGitHubIssue(alert) {
|
|
391
|
-
// Future: Create GitHub issues for critical alerts
|
|
392
|
-
// Would use GitHub API or gh CLI
|
|
393
|
-
console.log(`[ALERT-MANAGER] Would create GitHub issue: ${alert.title}`);
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
module.exports = AlertManager;
|
|
1
|
+
/**
|
|
2
|
+
* Alert Manager
|
|
3
|
+
*
|
|
4
|
+
* @purpose Event-driven AlertManager component
|
|
5
|
+
* @module sentinel
|
|
6
|
+
* @layer application
|
|
7
|
+
* @imports-from fs, path, events
|
|
8
|
+
* @exports-to sentinel
|
|
9
|
+
* @side-effects File system writes, File system reads, Console output, Event emission
|
|
10
|
+
* @critical-for sentinel
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Sentinel Alert Manager
|
|
15
|
+
*
|
|
16
|
+
* Handles alerting and reporting for the Sentinel system.
|
|
17
|
+
* Outputs: Console, files, and future integrations (Slack, email, GitHub issues)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const EventEmitter = require('events');
|
|
23
|
+
|
|
24
|
+
class AlertManager extends EventEmitter {
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
super();
|
|
27
|
+
|
|
28
|
+
this.config = {
|
|
29
|
+
rootPath: config.rootPath || process.cwd(),
|
|
30
|
+
alertsDir: config.alertsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'alerts'),
|
|
31
|
+
reportsDir: config.reportsDir || path.join(config.rootPath || process.cwd(), 'sentinel', 'reports'),
|
|
32
|
+
consoleOutput: config.consoleOutput !== false,
|
|
33
|
+
fileOutput: config.fileOutput !== false,
|
|
34
|
+
maxAlertsPerFile: config.maxAlertsPerFile || 1000,
|
|
35
|
+
slackWebhook: config.slackWebhook || null,
|
|
36
|
+
emailConfig: config.emailConfig || null,
|
|
37
|
+
...config
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Ensure directories exist
|
|
41
|
+
this._ensureDirectories();
|
|
42
|
+
|
|
43
|
+
// Alert history for deduplication
|
|
44
|
+
this.recentAlerts = new Map();
|
|
45
|
+
this.alertDedupeWindowMs = config.alertDedupeWindowMs || 60000; // 1 minute
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Send an alert
|
|
50
|
+
*/
|
|
51
|
+
async sendAlert(alert) {
|
|
52
|
+
const normalizedAlert = this._normalizeAlert(alert);
|
|
53
|
+
|
|
54
|
+
// Check for duplicate
|
|
55
|
+
if (this._isDuplicate(normalizedAlert)) {
|
|
56
|
+
return { sent: false, reason: 'duplicate' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Record alert
|
|
60
|
+
this._recordAlert(normalizedAlert);
|
|
61
|
+
|
|
62
|
+
// Output to configured channels
|
|
63
|
+
const results = {
|
|
64
|
+
console: false,
|
|
65
|
+
file: false,
|
|
66
|
+
slack: false,
|
|
67
|
+
email: false
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Console output
|
|
71
|
+
if (this.config.consoleOutput) {
|
|
72
|
+
results.console = this._outputToConsole(normalizedAlert);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// File output
|
|
76
|
+
if (this.config.fileOutput) {
|
|
77
|
+
results.file = await this._outputToFile(normalizedAlert);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Slack output (if configured and critical/error)
|
|
81
|
+
if (this.config.slackWebhook && ['critical', 'error'].includes(normalizedAlert.level)) {
|
|
82
|
+
results.slack = await this._outputToSlack(normalizedAlert);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.emit('alertSent', { alert: normalizedAlert, results });
|
|
86
|
+
|
|
87
|
+
return { sent: true, alert: normalizedAlert, results };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Send daily digest
|
|
92
|
+
*/
|
|
93
|
+
async sendDigest(digest) {
|
|
94
|
+
console.log('\n' + '='.repeat(60));
|
|
95
|
+
console.log('[SENTINEL] DAILY DIGEST - ' + digest.date);
|
|
96
|
+
console.log('='.repeat(60));
|
|
97
|
+
|
|
98
|
+
console.log(`\nSummary:`);
|
|
99
|
+
console.log(` Issues Found: ${digest.summary.issuesFound}`);
|
|
100
|
+
console.log(` Critical: ${digest.summary.criticalIssues}`);
|
|
101
|
+
console.log(` Warnings: ${digest.summary.warningIssues}`);
|
|
102
|
+
console.log(` Files Watched: ${digest.summary.filesWatched}`);
|
|
103
|
+
console.log(` Last Scan: ${digest.summary.lastScan || 'N/A'}`);
|
|
104
|
+
|
|
105
|
+
if (digest.topIssues && digest.topIssues.length > 0) {
|
|
106
|
+
console.log(`\nTop Issues:`);
|
|
107
|
+
for (const issue of digest.topIssues.slice(0, 5)) {
|
|
108
|
+
console.log(` [${issue.severity.toUpperCase()}] ${issue.message}`);
|
|
109
|
+
if (issue.file) {
|
|
110
|
+
console.log(` File: ${path.relative(this.config.rootPath, issue.file)}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (digest.recommendations && digest.recommendations.length > 0) {
|
|
116
|
+
console.log(`\nRecommendations:`);
|
|
117
|
+
for (const rec of digest.recommendations) {
|
|
118
|
+
console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log('\n' + '='.repeat(60) + '\n');
|
|
123
|
+
|
|
124
|
+
// Save digest to file
|
|
125
|
+
if (this.config.fileOutput) {
|
|
126
|
+
const digestPath = path.join(
|
|
127
|
+
this.config.reportsDir,
|
|
128
|
+
`digest-${digest.date}.json`
|
|
129
|
+
);
|
|
130
|
+
fs.writeFileSync(digestPath, JSON.stringify(digest, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
this.emit('digestSent', digest);
|
|
134
|
+
return { sent: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Send weekly report
|
|
139
|
+
*/
|
|
140
|
+
async sendWeeklyReport(report) {
|
|
141
|
+
console.log('\n' + '='.repeat(70));
|
|
142
|
+
console.log('[SENTINEL] WEEKLY REPORT - Week of ' + report.weekOf);
|
|
143
|
+
console.log('='.repeat(70));
|
|
144
|
+
|
|
145
|
+
console.log(`\nMetrics:`);
|
|
146
|
+
console.log(` Total Scans: ${report.summary.totalScans}`);
|
|
147
|
+
console.log(` Issues Detected: ${report.summary.issuesDetected}`);
|
|
148
|
+
console.log(` Issues Resolved: ${report.summary.issuesResolved}`);
|
|
149
|
+
console.log(` Avg Scan Time: ${report.summary.averageScanTime}ms`);
|
|
150
|
+
|
|
151
|
+
console.log(`\nCode Quality:`);
|
|
152
|
+
console.log(` Score: ${report.codeQuality.score}/100 (${report.codeQuality.grade})`);
|
|
153
|
+
console.log(` Complexity: ${report.codeQuality.factors.complexity}`);
|
|
154
|
+
console.log(` Maintainability: ${report.codeQuality.factors.maintainability}`);
|
|
155
|
+
console.log(` Test Coverage: ${report.codeQuality.factors.testCoverage}%`);
|
|
156
|
+
|
|
157
|
+
console.log(`\nTechnical Debt:`);
|
|
158
|
+
console.log(` Estimated Hours: ${report.technicalDebt.estimatedHours}`);
|
|
159
|
+
console.log(` Refactoring: ${report.technicalDebt.categories.refactoring}h`);
|
|
160
|
+
console.log(` Documentation: ${report.technicalDebt.categories.documentation}h`);
|
|
161
|
+
console.log(` Testing: ${report.technicalDebt.categories.testing}h`);
|
|
162
|
+
|
|
163
|
+
if (report.recommendations && report.recommendations.length > 0) {
|
|
164
|
+
console.log(`\nRecommendations:`);
|
|
165
|
+
for (const rec of report.recommendations) {
|
|
166
|
+
console.log(` [${rec.priority.toUpperCase()}] ${rec.message}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log('\n' + '='.repeat(70) + '\n');
|
|
171
|
+
|
|
172
|
+
// Save report to file
|
|
173
|
+
if (this.config.fileOutput) {
|
|
174
|
+
const reportPath = path.join(
|
|
175
|
+
this.config.reportsDir,
|
|
176
|
+
`weekly-report-${report.weekOf}.json`
|
|
177
|
+
);
|
|
178
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.emit('weeklyReportSent', report);
|
|
182
|
+
return { sent: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get alert history
|
|
187
|
+
*/
|
|
188
|
+
getAlertHistory(options = {}) {
|
|
189
|
+
const { days = 7, level = null, category = null } = options;
|
|
190
|
+
|
|
191
|
+
const cutoff = new Date();
|
|
192
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
193
|
+
|
|
194
|
+
// Read from alert files
|
|
195
|
+
const alerts = [];
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
if (fs.existsSync(this.config.alertsDir)) {
|
|
199
|
+
const files = fs.readdirSync(this.config.alertsDir)
|
|
200
|
+
.filter(f => f.startsWith('alerts-') && f.endsWith('.json'))
|
|
201
|
+
.sort()
|
|
202
|
+
.reverse();
|
|
203
|
+
|
|
204
|
+
for (const file of files) {
|
|
205
|
+
const fileDate = file.match(/alerts-(\d{4}-\d{2}-\d{2})/);
|
|
206
|
+
if (fileDate && new Date(fileDate[1]) < cutoff) {
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const content = JSON.parse(fs.readFileSync(
|
|
212
|
+
path.join(this.config.alertsDir, file),
|
|
213
|
+
'utf8'
|
|
214
|
+
));
|
|
215
|
+
alerts.push(...(content.alerts || []));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Skip corrupted files
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error('[ALERT-MANAGER] Error reading alert history:', error.message);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Filter
|
|
226
|
+
let filtered = alerts;
|
|
227
|
+
if (level) {
|
|
228
|
+
filtered = filtered.filter(a => a.level === level);
|
|
229
|
+
}
|
|
230
|
+
if (category) {
|
|
231
|
+
filtered = filtered.filter(a => a.category === category);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return filtered;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Clear old alerts
|
|
239
|
+
*/
|
|
240
|
+
async clearOldAlerts(daysToKeep = 30) {
|
|
241
|
+
const cutoff = new Date();
|
|
242
|
+
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
243
|
+
|
|
244
|
+
let deletedCount = 0;
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
if (fs.existsSync(this.config.alertsDir)) {
|
|
248
|
+
const files = fs.readdirSync(this.config.alertsDir);
|
|
249
|
+
|
|
250
|
+
for (const file of files) {
|
|
251
|
+
const fileDate = file.match(/(\d{4}-\d{2}-\d{2})/);
|
|
252
|
+
if (fileDate && new Date(fileDate[1]) < cutoff) {
|
|
253
|
+
fs.unlinkSync(path.join(this.config.alertsDir, file));
|
|
254
|
+
deletedCount++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.error('[ALERT-MANAGER] Error clearing old alerts:', error.message);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { deleted: deletedCount };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Private methods
|
|
266
|
+
|
|
267
|
+
_ensureDirectories() {
|
|
268
|
+
try {
|
|
269
|
+
if (!fs.existsSync(this.config.alertsDir)) {
|
|
270
|
+
fs.mkdirSync(this.config.alertsDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
if (!fs.existsSync(this.config.reportsDir)) {
|
|
273
|
+
fs.mkdirSync(this.config.reportsDir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.warn('[ALERT-MANAGER] Could not create directories:', error.message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_normalizeAlert(alert) {
|
|
281
|
+
return {
|
|
282
|
+
id: alert.id || this._generateAlertId(),
|
|
283
|
+
timestamp: alert.timestamp || new Date().toISOString(),
|
|
284
|
+
level: alert.level || 'info',
|
|
285
|
+
category: alert.category || 'general',
|
|
286
|
+
title: alert.title || 'Alert',
|
|
287
|
+
message: alert.message || '',
|
|
288
|
+
file: alert.file || null,
|
|
289
|
+
line: alert.line || null,
|
|
290
|
+
details: alert.details || {}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_generateAlertId() {
|
|
295
|
+
return `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_isDuplicate(alert) {
|
|
299
|
+
const key = `${alert.level}:${alert.message}:${alert.file}`;
|
|
300
|
+
const lastSeen = this.recentAlerts.get(key);
|
|
301
|
+
|
|
302
|
+
if (lastSeen && (Date.now() - lastSeen) < this.alertDedupeWindowMs) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_recordAlert(alert) {
|
|
310
|
+
const key = `${alert.level}:${alert.message}:${alert.file}`;
|
|
311
|
+
this.recentAlerts.set(key, Date.now());
|
|
312
|
+
|
|
313
|
+
// Cleanup old entries
|
|
314
|
+
const cutoff = Date.now() - this.alertDedupeWindowMs;
|
|
315
|
+
for (const [k, v] of this.recentAlerts) {
|
|
316
|
+
if (v < cutoff) {
|
|
317
|
+
this.recentAlerts.delete(k);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_outputToConsole(alert) {
|
|
323
|
+
const levelColors = {
|
|
324
|
+
critical: '\x1b[31m', // Red
|
|
325
|
+
error: '\x1b[31m', // Red
|
|
326
|
+
warning: '\x1b[33m', // Yellow
|
|
327
|
+
info: '\x1b[36m' // Cyan
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const reset = '\x1b[0m';
|
|
331
|
+
const color = levelColors[alert.level] || '';
|
|
332
|
+
|
|
333
|
+
console.log(`${color}[SENTINEL:${alert.level.toUpperCase()}]${reset} ${alert.title}`);
|
|
334
|
+
console.log(` ${alert.message}`);
|
|
335
|
+
|
|
336
|
+
if (alert.file) {
|
|
337
|
+
const relPath = path.relative(this.config.rootPath, alert.file);
|
|
338
|
+
console.log(` File: ${relPath}${alert.line ? ':' + alert.line : ''}`);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async _outputToFile(alert) {
|
|
345
|
+
try {
|
|
346
|
+
const dateStr = new Date().toISOString().split('T')[0];
|
|
347
|
+
const filePath = path.join(this.config.alertsDir, `alerts-${dateStr}.json`);
|
|
348
|
+
|
|
349
|
+
let fileData = { date: dateStr, alerts: [] };
|
|
350
|
+
|
|
351
|
+
if (fs.existsSync(filePath)) {
|
|
352
|
+
try {
|
|
353
|
+
fileData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
354
|
+
} catch (e) {
|
|
355
|
+
// File corrupted, start fresh
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fileData.alerts.push(alert);
|
|
360
|
+
|
|
361
|
+
// Limit alerts per file
|
|
362
|
+
if (fileData.alerts.length > this.config.maxAlertsPerFile) {
|
|
363
|
+
fileData.alerts = fileData.alerts.slice(-this.config.maxAlertsPerFile);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
fs.writeFileSync(filePath, JSON.stringify(fileData, null, 2));
|
|
367
|
+
return true;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.error('[ALERT-MANAGER] File output failed:', error.message);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async _outputToSlack(alert) {
|
|
375
|
+
if (!this.config.slackWebhook) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
// Would use fetch or axios here
|
|
381
|
+
// For now, just log that we would send to Slack
|
|
382
|
+
console.log(`[ALERT-MANAGER] Would send to Slack: ${alert.title}`);
|
|
383
|
+
return true;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.error('[ALERT-MANAGER] Slack output failed:', error.message);
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async _createGitHubIssue(alert) {
|
|
391
|
+
// Future: Create GitHub issues for critical alerts
|
|
392
|
+
// Would use GitHub API or gh CLI
|
|
393
|
+
console.log(`[ALERT-MANAGER] Would create GitHub issue: ${alert.title}`);
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = AlertManager;
|