thuban 0.3.3 → 0.3.4
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/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +12 -7
- package/cli.js +0 -2627
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -758
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -276
- package/packages/scanner/dependency-graph.js +0 -541
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-collector.js +0 -64
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -301
- package/packages/scanner/hallucination-detector.js +0 -822
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -100
- package/packages/scanner/license-manager.js +0 -331
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/monitor-notifier.js +0 -64
- package/packages/scanner/monitor-service.js +0 -103
- package/packages/scanner/monitor-store.js +0 -117
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/scan-diff.js +0 -50
- package/packages/scanner/scan-runner.js +0 -172
- package/packages/scanner/secret-scanner.js +0 -612
- package/packages/scanner/secret-scanner.test.js +0 -103
- package/packages/scanner/sentinel-core.js +0 -616
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -583
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Thuban AI Confidence Scorer
|
|
3
|
-
*
|
|
4
|
-
* @purpose Score each function's probability of being AI-generated
|
|
5
|
-
* @module thuban
|
|
6
|
-
* @layer scanner
|
|
7
|
-
* @exports-to cli
|
|
8
|
-
* @critical-for USP — nobody else gives per-function AI probability scores
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
|
|
14
|
-
// Signals that indicate AI-generated code
|
|
15
|
-
const AI_SIGNALS = {
|
|
16
|
-
// Naming patterns AI tends to use
|
|
17
|
-
genericNames: ['data', 'result', 'response', 'output', 'input', 'temp', 'item', 'element', 'val', 'obj', 'arr', 'str', 'num', 'flag', 'status'],
|
|
18
|
-
|
|
19
|
-
// Comment patterns AI adds
|
|
20
|
-
aiCommentPatterns: [
|
|
21
|
-
/\/\/\s*TODO:?\s*implement/i,
|
|
22
|
-
/\/\/\s*handle\s+error/i,
|
|
23
|
-
/\/\/\s*add\s+error\s+handling/i,
|
|
24
|
-
/\/\/\s*you\s+(can|may|might|should)/i,
|
|
25
|
-
/\/\/\s*replace\s+(this|with)/i,
|
|
26
|
-
/\/\/\s*this\s+(function|method|class)\s+(takes|accepts|returns|handles)/i,
|
|
27
|
-
/\/\/\s*example\s+usage/i,
|
|
28
|
-
/\/\/\s*note:?\s*this/i,
|
|
29
|
-
/\/\*\*[\s\S]*?@description\s/,
|
|
30
|
-
/\/\/\s*import.*if\s+needed/i,
|
|
31
|
-
],
|
|
32
|
-
|
|
33
|
-
// Structural patterns
|
|
34
|
-
aiStructurePatterns: [
|
|
35
|
-
/try\s*\{[\s\S]*?\}\s*catch\s*\(\s*(?:error|err|e)\s*\)\s*\{[\s\S]*?console\.(log|error)\s*\(\s*(?:error|err|e)\s*\)/, // Generic try/catch with console.log
|
|
36
|
-
/if\s*\(!.*\)\s*\{?\s*(?:return|throw)\s/, // Guard clause pattern (AI loves these)
|
|
37
|
-
],
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
class AIConfidenceScorer {
|
|
41
|
-
constructor(opts = {}) {
|
|
42
|
-
this.rootPath = opts.rootPath || process.cwd();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Score all functions in the given files
|
|
47
|
-
*/
|
|
48
|
-
scan(files) {
|
|
49
|
-
const results = [];
|
|
50
|
-
|
|
51
|
-
for (const file of files) {
|
|
52
|
-
let content;
|
|
53
|
-
try {
|
|
54
|
-
const stat = fs.statSync(file);
|
|
55
|
-
if (stat.size > 512 * 1024) continue;
|
|
56
|
-
content = fs.readFileSync(file, 'utf-8');
|
|
57
|
-
} catch (e) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const relPath = path.relative(this.rootPath, file);
|
|
62
|
-
if (this._isSkippable(relPath)) continue;
|
|
63
|
-
|
|
64
|
-
const functions = this._extractFunctions(content, relPath);
|
|
65
|
-
results.push(...functions);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Sort by confidence descending
|
|
69
|
-
results.sort((a, b) => b.confidence - a.confidence);
|
|
70
|
-
|
|
71
|
-
const highConfidence = results.filter(r => r.confidence >= 70);
|
|
72
|
-
const mediumConfidence = results.filter(r => r.confidence >= 40 && r.confidence < 70);
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
functions: results,
|
|
76
|
-
highConfidence,
|
|
77
|
-
mediumConfidence,
|
|
78
|
-
totalFunctions: results.length,
|
|
79
|
-
aiLikelyCount: highConfidence.length,
|
|
80
|
-
aiPossibleCount: mediumConfidence.length,
|
|
81
|
-
aiPercentage: results.length > 0
|
|
82
|
-
? Math.round((highConfidence.length / results.length) * 100)
|
|
83
|
-
: 0,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ─── Private ─────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
_extractFunctions(content, filePath) {
|
|
90
|
-
const lines = content.split('\n');
|
|
91
|
-
const functions = [];
|
|
92
|
-
|
|
93
|
-
for (let i = 0; i < lines.length; i++) {
|
|
94
|
-
const fn = this._detectFunction(lines, i);
|
|
95
|
-
if (!fn) continue;
|
|
96
|
-
|
|
97
|
-
const endLine = this._findFunctionEnd(lines, i);
|
|
98
|
-
const body = lines.slice(i, endLine + 1).join('\n');
|
|
99
|
-
const bodyLines = lines.slice(i, endLine + 1);
|
|
100
|
-
|
|
101
|
-
const signals = this._scoreFunction(fn.name, body, bodyLines, lines, i);
|
|
102
|
-
|
|
103
|
-
functions.push({
|
|
104
|
-
file: filePath,
|
|
105
|
-
name: fn.name,
|
|
106
|
-
line: i + 1,
|
|
107
|
-
lineCount: endLine - i + 1,
|
|
108
|
-
confidence: signals.score,
|
|
109
|
-
signals: signals.reasons,
|
|
110
|
-
testCoverage: this._hasTests(fn.name, filePath) ? 'found' : 'none',
|
|
111
|
-
category: 'ai_confidence',
|
|
112
|
-
severity: signals.score >= 80 ? 'high' : signals.score >= 50 ? 'medium' : 'low',
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
i = endLine; // Skip past the function
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return functions;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
_scoreFunction(name, body, bodyLines, allLines, startLine) {
|
|
122
|
-
let score = 0;
|
|
123
|
-
const reasons = [];
|
|
124
|
-
|
|
125
|
-
// Signal 1: Generic variable names (0-15 points)
|
|
126
|
-
const genericCount = AI_SIGNALS.genericNames.filter(g => {
|
|
127
|
-
const regex = new RegExp(`\\b${g}\\b`, 'g');
|
|
128
|
-
return (body.match(regex) || []).length >= 2;
|
|
129
|
-
}).length;
|
|
130
|
-
if (genericCount >= 3) {
|
|
131
|
-
score += 15;
|
|
132
|
-
reasons.push(`${genericCount} generic variable names (data, result, response...)`);
|
|
133
|
-
} else if (genericCount >= 1) {
|
|
134
|
-
score += 8;
|
|
135
|
-
reasons.push(`${genericCount} generic variable name(s)`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Signal 2: AI-style comments (0-20 points)
|
|
139
|
-
const aiComments = AI_SIGNALS.aiCommentPatterns.filter(p => p.test(body)).length;
|
|
140
|
-
if (aiComments >= 2) {
|
|
141
|
-
score += 20;
|
|
142
|
-
reasons.push(`${aiComments} AI-style comments (TODO: implement, handle error, etc.)`);
|
|
143
|
-
} else if (aiComments >= 1) {
|
|
144
|
-
score += 10;
|
|
145
|
-
reasons.push('AI-style comment detected');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Signal 3: Overly thorough JSDoc (0-10 points)
|
|
149
|
-
if (startLine > 0) {
|
|
150
|
-
const precedingLines = allLines.slice(Math.max(0, startLine - 10), startLine).join('\n');
|
|
151
|
-
if (/\/\*\*[\s\S]*@param[\s\S]*@returns?[\s\S]*@(?:throws|example)/s.test(precedingLines)) {
|
|
152
|
-
score += 10;
|
|
153
|
-
reasons.push('Exhaustive JSDoc (params + returns + throws/example)');
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Signal 4: Perfect error handling pattern (0-10 points)
|
|
158
|
-
const tryCatchCount = (body.match(/try\s*\{/g) || []).length;
|
|
159
|
-
if (tryCatchCount >= 2) {
|
|
160
|
-
score += 10;
|
|
161
|
-
reasons.push('Multiple try/catch blocks (AI over-handles errors)');
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Signal 5: Function is long and self-contained (0-15 points)
|
|
165
|
-
const lineCount = bodyLines.length;
|
|
166
|
-
if (lineCount >= 50) {
|
|
167
|
-
score += 15;
|
|
168
|
-
reasons.push(`${lineCount} lines — large self-contained function`);
|
|
169
|
-
} else if (lineCount >= 25) {
|
|
170
|
-
score += 8;
|
|
171
|
-
reasons.push(`${lineCount} lines`);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Signal 6: No references to other functions in the same file (0-10 points)
|
|
175
|
-
// AI tends to generate isolated, self-contained functions
|
|
176
|
-
const internalCalls = allLines
|
|
177
|
-
.slice(0, startLine)
|
|
178
|
-
.concat(allLines.slice(startLine + lineCount))
|
|
179
|
-
.filter(l => l.includes(name)).length;
|
|
180
|
-
if (internalCalls === 0 && lineCount > 10) {
|
|
181
|
-
score += 10;
|
|
182
|
-
reasons.push('Isolated — no internal references');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Signal 7: console.log in error handling (0-10 points)
|
|
186
|
-
if (/catch\s*\([^)]*\)\s*\{[^}]*console\.(log|error)\s*\(/.test(body)) {
|
|
187
|
-
score += 8;
|
|
188
|
-
reasons.push('console.log/error in catch block (AI default pattern)');
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Signal 8: Arrow functions with immediate destructuring (0-5 points)
|
|
192
|
-
if (/=>\s*\{[\s\n\r]*const\s*\{/.test(body)) {
|
|
193
|
-
score += 5;
|
|
194
|
-
reasons.push('Immediate destructuring after arrow (common AI pattern)');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Signal 9: Template literal heavy (0-5 points)
|
|
198
|
-
const templateCount = (body.match(/`[^`]*\$\{/g) || []).length;
|
|
199
|
-
if (templateCount >= 4) {
|
|
200
|
-
score += 5;
|
|
201
|
-
reasons.push('Heavy template literal usage');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Cap at 100
|
|
205
|
-
score = Math.min(100, score);
|
|
206
|
-
|
|
207
|
-
return { score, reasons };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
_detectFunction(lines, lineIndex) {
|
|
211
|
-
const line = lines[lineIndex].trim();
|
|
212
|
-
|
|
213
|
-
let match = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
|
214
|
-
if (match) return { name: match[1] };
|
|
215
|
-
|
|
216
|
-
match = line.match(/^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:function|\()/);
|
|
217
|
-
if (match && (line.includes('=>') || line.includes('function'))) return { name: match[1] };
|
|
218
|
-
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
_findFunctionEnd(lines, startLine) {
|
|
223
|
-
let depth = 0;
|
|
224
|
-
let started = false;
|
|
225
|
-
|
|
226
|
-
for (let i = startLine; i < lines.length && i < startLine + 300; i++) {
|
|
227
|
-
for (const ch of lines[i]) {
|
|
228
|
-
if (ch === '{') { depth++; started = true; }
|
|
229
|
-
if (ch === '}') depth--;
|
|
230
|
-
}
|
|
231
|
-
if (started && depth <= 0) return i;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return Math.min(startLine + 20, lines.length - 1);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
_hasTests(funcName, filePath) {
|
|
238
|
-
// Check if a test file exists for this file
|
|
239
|
-
const testVariants = [
|
|
240
|
-
filePath.replace(/\.(js|ts)$/, '.test.$1'),
|
|
241
|
-
filePath.replace(/\.(js|ts)$/, '.spec.$1'),
|
|
242
|
-
filePath.replace(/src\//, 'test/'),
|
|
243
|
-
filePath.replace(/src\//, '__tests__/'),
|
|
244
|
-
];
|
|
245
|
-
return testVariants.some(t => {
|
|
246
|
-
try { return fs.existsSync(path.resolve(this.rootPath, t)); } catch (e) { return false; }
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
_isSkippable(relPath) {
|
|
251
|
-
const skip = [
|
|
252
|
-
/\.test\./i, /\.spec\./i, /test[\/\\]/i, /spec[\/\\]/i,
|
|
253
|
-
/__test__/i, /__mocks__/i, /\.config\./i, /\.d\.ts$/,
|
|
254
|
-
/node_modules/i, /\.min\./i,
|
|
255
|
-
];
|
|
256
|
-
return skip.some(r => r.test(relPath));
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
module.exports = AIConfidenceScorer;
|
|
@@ -1,398 +0,0 @@
|
|
|
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;
|