driftdetect 0.4.6 → 0.4.7
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/LICENSE +21 -0
- package/dist/bin/drift.js +0 -0
- package/dist/commands/callgraph.js +4 -4
- package/dist/commands/callgraph.js.map +1 -1
- package/dist/commands/scan.d.ts +35 -0
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/scan.js +99 -5
- package/dist/commands/scan.js.map +1 -1
- package/dist/services/scanner-service.d.ts +210 -0
- package/dist/services/scanner-service.d.ts.map +1 -1
- package/dist/services/scanner-service.js +298 -102
- package/dist/services/scanner-service.js.map +1 -1
- package/dist/workers/detector-worker.d.ts +107 -0
- package/dist/workers/detector-worker.d.ts.map +1 -0
- package/dist/workers/detector-worker.js +293 -0
- package/dist/workers/detector-worker.js.map +1 -0
- package/package.json +18 -17
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scanner Service - Enterprise-grade pattern detection
|
|
2
|
+
* Scanner Service - Enterprise-grade pattern detection with Worker Threads
|
|
3
3
|
*
|
|
4
4
|
* Uses the real detectors from driftdetect-detectors to find
|
|
5
5
|
* high-value architectural patterns and violations.
|
|
6
6
|
*
|
|
7
|
-
* Now
|
|
7
|
+
* Now uses Piscina worker threads for parallel CPU-bound processing.
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'node:fs/promises';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
11
13
|
import { createAllDetectorsArray, getDetectorCounts, } from 'driftdetect-detectors';
|
|
12
14
|
import { ManifestStore, hashContent, } from 'driftdetect-core';
|
|
15
|
+
// Get the directory of this module for worker path resolution
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
17
|
// ============================================================================
|
|
14
18
|
// Location Deduplication
|
|
15
19
|
// ============================================================================
|
|
16
|
-
/**
|
|
17
|
-
* Create a unique key for a basic location to enable deduplication
|
|
18
|
-
*/
|
|
19
20
|
function locationKey(loc) {
|
|
20
21
|
return `${loc.file}:${loc.line}:${loc.column}`;
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Create a unique key for a semantic location to enable deduplication
|
|
24
|
-
*/
|
|
25
23
|
function semanticLocationKey(loc) {
|
|
26
24
|
return `${loc.file}:${loc.range.start}:${loc.range.end}:${loc.name}`;
|
|
27
25
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Add a basic location to an array only if it doesn't already exist
|
|
30
|
-
*/
|
|
31
26
|
function addUniqueLocation(locations, location, seenKeys) {
|
|
32
27
|
const key = locationKey(location);
|
|
33
28
|
const seen = seenKeys || new Set(locations.map(locationKey));
|
|
@@ -38,9 +33,6 @@ function addUniqueLocation(locations, location, seenKeys) {
|
|
|
38
33
|
locations.push(location);
|
|
39
34
|
return true;
|
|
40
35
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Add a semantic location to an array only if it doesn't already exist
|
|
43
|
-
*/
|
|
44
36
|
function addUniqueSemanticLocation(locations, location, seenKeys) {
|
|
45
37
|
const key = semanticLocationKey(location);
|
|
46
38
|
const seen = seenKeys || new Set(locations.map(semanticLocationKey));
|
|
@@ -87,32 +79,25 @@ function isDetectorApplicable(detector, language) {
|
|
|
87
79
|
return info.supportedLanguages.includes(language);
|
|
88
80
|
}
|
|
89
81
|
// ============================================================================
|
|
90
|
-
// Critical Detectors
|
|
82
|
+
// Critical Detectors
|
|
91
83
|
// ============================================================================
|
|
92
84
|
const CRITICAL_DETECTOR_IDS = new Set([
|
|
93
|
-
// Security - CRITICAL
|
|
94
85
|
'security/sql-injection',
|
|
95
86
|
'security/xss-prevention',
|
|
96
87
|
'security/secret-management',
|
|
97
88
|
'security/input-sanitization',
|
|
98
89
|
'security/csrf-protection',
|
|
99
|
-
// Auth - CRITICAL
|
|
100
90
|
'auth/middleware-usage',
|
|
101
91
|
'auth/token-handling',
|
|
102
|
-
// API - HIGH
|
|
103
92
|
'api/route-structure',
|
|
104
93
|
'api/error-format',
|
|
105
94
|
'api/response-envelope',
|
|
106
|
-
// Data Access - HIGH
|
|
107
95
|
'data-access/n-plus-one',
|
|
108
96
|
'data-access/query-patterns',
|
|
109
|
-
// Structural - HIGH
|
|
110
97
|
'structural/circular-deps',
|
|
111
98
|
'structural/module-boundaries',
|
|
112
|
-
// Errors - HIGH
|
|
113
99
|
'errors/exception-hierarchy',
|
|
114
100
|
'errors/try-catch-placement',
|
|
115
|
-
// Logging - HIGH
|
|
116
101
|
'logging/pii-redaction',
|
|
117
102
|
]);
|
|
118
103
|
// ============================================================================
|
|
@@ -121,27 +106,34 @@ const CRITICAL_DETECTOR_IDS = new Set([
|
|
|
121
106
|
/**
|
|
122
107
|
* Scanner Service
|
|
123
108
|
*
|
|
124
|
-
* Orchestrates pattern detection across files using real detectors
|
|
125
|
-
*
|
|
109
|
+
* Orchestrates pattern detection across files using real detectors.
|
|
110
|
+
* Supports both single-threaded and multi-threaded (Piscina) modes.
|
|
126
111
|
*/
|
|
127
112
|
export class ScannerService {
|
|
128
113
|
config;
|
|
129
114
|
detectors = [];
|
|
130
115
|
initialized = false;
|
|
131
116
|
manifestStore = null;
|
|
117
|
+
pool = null;
|
|
118
|
+
PiscinaClass = null;
|
|
132
119
|
constructor(config) {
|
|
133
|
-
this.config =
|
|
120
|
+
this.config = {
|
|
121
|
+
...config,
|
|
122
|
+
// Default to using worker threads
|
|
123
|
+
useWorkerThreads: config.useWorkerThreads ?? true,
|
|
124
|
+
workerThreads: config.workerThreads ?? Math.max(1, os.cpus().length - 1),
|
|
125
|
+
};
|
|
134
126
|
if (config.generateManifest) {
|
|
135
127
|
this.manifestStore = new ManifestStore(config.rootDir);
|
|
136
128
|
}
|
|
137
129
|
}
|
|
138
130
|
/**
|
|
139
|
-
* Initialize the scanner service
|
|
131
|
+
* Initialize the scanner service
|
|
140
132
|
*/
|
|
141
133
|
async initialize() {
|
|
142
134
|
if (this.initialized)
|
|
143
135
|
return;
|
|
144
|
-
// Create all detectors
|
|
136
|
+
// Create all detectors (needed for counts even in worker mode)
|
|
145
137
|
this.detectors = await createAllDetectorsArray();
|
|
146
138
|
// Filter by category if specified
|
|
147
139
|
if (this.config.categories && this.config.categories.length > 0) {
|
|
@@ -155,12 +147,71 @@ export class ScannerService {
|
|
|
155
147
|
if (this.config.criticalOnly) {
|
|
156
148
|
this.detectors = this.detectors.filter(d => CRITICAL_DETECTOR_IDS.has(d.id));
|
|
157
149
|
}
|
|
150
|
+
// Initialize worker pool if using threads
|
|
151
|
+
if (this.config.useWorkerThreads) {
|
|
152
|
+
try {
|
|
153
|
+
const piscinaModule = await import('piscina');
|
|
154
|
+
// Piscina exports as named export, not default
|
|
155
|
+
this.PiscinaClass = (piscinaModule.Piscina || piscinaModule.default);
|
|
156
|
+
if (!this.PiscinaClass || typeof this.PiscinaClass !== 'function') {
|
|
157
|
+
throw new Error(`Piscina class not found. Module exports: ${Object.keys(piscinaModule).join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
// Worker path - compiled JS in dist
|
|
160
|
+
const workerPath = path.join(__dirname, '..', 'workers', 'detector-worker.js');
|
|
161
|
+
if (this.config.verbose) {
|
|
162
|
+
console.log(` Worker path: ${workerPath}`);
|
|
163
|
+
}
|
|
164
|
+
this.pool = new this.PiscinaClass({
|
|
165
|
+
filename: workerPath,
|
|
166
|
+
minThreads: this.config.workerThreads, // Keep all workers alive
|
|
167
|
+
maxThreads: this.config.workerThreads,
|
|
168
|
+
idleTimeout: 60000, // 60s idle timeout
|
|
169
|
+
});
|
|
170
|
+
if (this.config.verbose) {
|
|
171
|
+
console.log(` Worker pool initialized with ${this.config.workerThreads} threads`);
|
|
172
|
+
}
|
|
173
|
+
// Warm up all workers in parallel - this loads detectors once per worker
|
|
174
|
+
await this.warmupWorkers();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
// Fall back to single-threaded mode
|
|
178
|
+
console.log(` Worker threads unavailable, using single-threaded mode: ${error.message}`);
|
|
179
|
+
if (this.config.verbose) {
|
|
180
|
+
console.log(` Full error: ${error.stack}`);
|
|
181
|
+
}
|
|
182
|
+
this.config.useWorkerThreads = false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
158
185
|
// Load existing manifest for incremental scanning
|
|
159
186
|
if (this.manifestStore) {
|
|
160
187
|
await this.manifestStore.load();
|
|
161
188
|
}
|
|
162
189
|
this.initialized = true;
|
|
163
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Warm up all worker threads by preloading detectors
|
|
193
|
+
* This runs warmup tasks in parallel so all workers load detectors simultaneously
|
|
194
|
+
*/
|
|
195
|
+
async warmupWorkers() {
|
|
196
|
+
if (!this.pool)
|
|
197
|
+
return;
|
|
198
|
+
const numWorkers = this.config.workerThreads;
|
|
199
|
+
const warmupTasks = Array(numWorkers).fill(null).map(() => ({
|
|
200
|
+
type: 'warmup',
|
|
201
|
+
categories: this.config.categories,
|
|
202
|
+
criticalOnly: this.config.criticalOnly,
|
|
203
|
+
}));
|
|
204
|
+
if (this.config.verbose) {
|
|
205
|
+
console.log(` Warming up ${numWorkers} workers...`);
|
|
206
|
+
}
|
|
207
|
+
const startTime = Date.now();
|
|
208
|
+
// Run warmup tasks in parallel - each worker gets one task
|
|
209
|
+
await Promise.all(warmupTasks.map(task => this.pool.run(task)));
|
|
210
|
+
const duration = Date.now() - startTime;
|
|
211
|
+
if (this.config.verbose) {
|
|
212
|
+
console.log(` Workers warmed up in ${duration}ms`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
164
215
|
/**
|
|
165
216
|
* Get detector count
|
|
166
217
|
*/
|
|
@@ -174,16 +225,211 @@ export class ScannerService {
|
|
|
174
225
|
return getDetectorCounts();
|
|
175
226
|
}
|
|
176
227
|
/**
|
|
177
|
-
*
|
|
228
|
+
* Check if worker threads are enabled and initialized
|
|
229
|
+
*/
|
|
230
|
+
isUsingWorkerThreads() {
|
|
231
|
+
return this.config.useWorkerThreads === true && this.pool !== null;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Get worker thread count
|
|
235
|
+
*/
|
|
236
|
+
getWorkerThreadCount() {
|
|
237
|
+
return this.config.workerThreads ?? 0;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Scan files for patterns
|
|
178
241
|
*/
|
|
179
242
|
async scanFiles(files, projectContext) {
|
|
243
|
+
if (this.config.useWorkerThreads && this.pool) {
|
|
244
|
+
return this.scanFilesWithWorkers(files, projectContext);
|
|
245
|
+
}
|
|
246
|
+
return this.scanFilesSingleThreaded(files, projectContext);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Scan files using worker threads (parallel)
|
|
250
|
+
*/
|
|
251
|
+
async scanFilesWithWorkers(files, projectContext) {
|
|
252
|
+
const startTime = Date.now();
|
|
253
|
+
const errors = [];
|
|
254
|
+
// Filter to changed files if incremental
|
|
255
|
+
let filesToScan = files;
|
|
256
|
+
if (this.config.incremental && this.manifestStore) {
|
|
257
|
+
const fullPaths = files.map(f => path.join(this.config.rootDir, f));
|
|
258
|
+
const changedPaths = await this.manifestStore.getChangedFiles(fullPaths);
|
|
259
|
+
filesToScan = changedPaths.map(f => path.relative(this.config.rootDir, f));
|
|
260
|
+
for (const file of filesToScan) {
|
|
261
|
+
this.manifestStore.clearFilePatterns(path.join(this.config.rootDir, file));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Create tasks for worker pool
|
|
265
|
+
const tasks = filesToScan.map(file => ({
|
|
266
|
+
file,
|
|
267
|
+
rootDir: this.config.rootDir,
|
|
268
|
+
projectFiles: projectContext.files,
|
|
269
|
+
projectConfig: projectContext.config,
|
|
270
|
+
categories: this.config.categories,
|
|
271
|
+
criticalOnly: this.config.criticalOnly,
|
|
272
|
+
}));
|
|
273
|
+
// Run all tasks in parallel
|
|
274
|
+
const results = await Promise.all(tasks.map(async (task) => {
|
|
275
|
+
try {
|
|
276
|
+
return await this.pool.run(task);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
file: task.file,
|
|
281
|
+
language: null,
|
|
282
|
+
patterns: [],
|
|
283
|
+
violations: [],
|
|
284
|
+
detectorsRan: 0,
|
|
285
|
+
detectorsSkipped: 0,
|
|
286
|
+
duration: 0,
|
|
287
|
+
error: error instanceof Error ? error.message : String(error),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}));
|
|
291
|
+
// Aggregate results
|
|
292
|
+
return this.aggregateWorkerResults(results, files, startTime, errors);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Aggregate results from worker threads
|
|
296
|
+
*/
|
|
297
|
+
async aggregateWorkerResults(results, allFiles, startTime, errors) {
|
|
298
|
+
const fileResults = [];
|
|
299
|
+
const patternMap = new Map();
|
|
300
|
+
const allViolations = [];
|
|
301
|
+
const manifestPatternMap = new Map();
|
|
302
|
+
let totalDetectorsRan = 0;
|
|
303
|
+
let totalDetectorsSkipped = 0;
|
|
304
|
+
for (const result of results) {
|
|
305
|
+
if (result.error) {
|
|
306
|
+
errors.push(`Failed to scan ${result.file}: ${result.error}`);
|
|
307
|
+
}
|
|
308
|
+
totalDetectorsRan += result.detectorsRan;
|
|
309
|
+
totalDetectorsSkipped += result.detectorsSkipped;
|
|
310
|
+
// Convert to FileScanResult
|
|
311
|
+
const filePatterns = result.patterns.map(p => ({
|
|
312
|
+
patternId: p.patternId,
|
|
313
|
+
detectorId: p.detectorId,
|
|
314
|
+
confidence: p.confidence,
|
|
315
|
+
location: p.location,
|
|
316
|
+
}));
|
|
317
|
+
fileResults.push({
|
|
318
|
+
file: result.file,
|
|
319
|
+
patterns: filePatterns,
|
|
320
|
+
violations: result.violations,
|
|
321
|
+
duration: result.duration,
|
|
322
|
+
error: result.error,
|
|
323
|
+
});
|
|
324
|
+
// Aggregate patterns
|
|
325
|
+
for (const match of result.patterns) {
|
|
326
|
+
const key = match.patternId;
|
|
327
|
+
const existing = patternMap.get(key);
|
|
328
|
+
if (existing) {
|
|
329
|
+
addUniqueLocation(existing.locations, match.location);
|
|
330
|
+
existing.occurrences++;
|
|
331
|
+
existing.confidence = Math.max(existing.confidence, match.confidence);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
patternMap.set(key, {
|
|
335
|
+
patternId: match.patternId,
|
|
336
|
+
detectorId: match.detectorId,
|
|
337
|
+
category: match.category,
|
|
338
|
+
subcategory: match.subcategory,
|
|
339
|
+
name: match.detectorName,
|
|
340
|
+
description: match.detectorDescription,
|
|
341
|
+
locations: [match.location],
|
|
342
|
+
confidence: match.confidence,
|
|
343
|
+
occurrences: 1,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Build manifest pattern
|
|
347
|
+
if (this.config.generateManifest && result.language) {
|
|
348
|
+
await this.addToManifest(manifestPatternMap, match, result);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// Aggregate violations
|
|
352
|
+
for (const violation of result.violations) {
|
|
353
|
+
allViolations.push(violation);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// Convert pattern map to array
|
|
357
|
+
const patterns = Array.from(patternMap.values());
|
|
358
|
+
// Build and save manifest
|
|
359
|
+
let manifest;
|
|
360
|
+
if (this.config.generateManifest && this.manifestStore) {
|
|
361
|
+
const manifestPatterns = Array.from(manifestPatternMap.values());
|
|
362
|
+
this.manifestStore.updatePatterns(manifestPatterns);
|
|
363
|
+
await this.manifestStore.save();
|
|
364
|
+
manifest = await this.manifestStore.get();
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
files: fileResults,
|
|
368
|
+
patterns,
|
|
369
|
+
violations: allViolations,
|
|
370
|
+
totalPatterns: patterns.reduce((sum, p) => sum + p.occurrences, 0),
|
|
371
|
+
totalViolations: allViolations.length,
|
|
372
|
+
totalFiles: allFiles.length,
|
|
373
|
+
duration: Date.now() - startTime,
|
|
374
|
+
errors,
|
|
375
|
+
detectorStats: {
|
|
376
|
+
total: this.detectors.length,
|
|
377
|
+
ran: totalDetectorsRan,
|
|
378
|
+
skipped: totalDetectorsSkipped,
|
|
379
|
+
},
|
|
380
|
+
workerStats: this.pool ? {
|
|
381
|
+
threadsUsed: this.pool.threads.length,
|
|
382
|
+
tasksCompleted: this.pool.completed,
|
|
383
|
+
} : undefined,
|
|
384
|
+
manifest,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Add pattern match to manifest
|
|
389
|
+
*/
|
|
390
|
+
async addToManifest(manifestPatternMap, match, result) {
|
|
391
|
+
try {
|
|
392
|
+
const filePath = path.join(this.config.rootDir, result.file);
|
|
393
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
394
|
+
const contentHash = hashContent(content);
|
|
395
|
+
const language = result.language;
|
|
396
|
+
const semanticLoc = this.createSemanticLocation(match.location, content, contentHash, language);
|
|
397
|
+
const manifestKey = `${match.category}/${match.subcategory}/${match.patternId}`;
|
|
398
|
+
const existingManifest = manifestPatternMap.get(manifestKey);
|
|
399
|
+
if (existingManifest) {
|
|
400
|
+
addUniqueSemanticLocation(existingManifest.locations, semanticLoc);
|
|
401
|
+
existingManifest.confidence = Math.max(existingManifest.confidence, match.confidence);
|
|
402
|
+
existingManifest.lastSeen = new Date().toISOString();
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
manifestPatternMap.set(manifestKey, {
|
|
406
|
+
id: manifestKey,
|
|
407
|
+
name: match.detectorName,
|
|
408
|
+
category: match.category,
|
|
409
|
+
subcategory: match.subcategory,
|
|
410
|
+
status: 'discovered',
|
|
411
|
+
confidence: match.confidence,
|
|
412
|
+
locations: [semanticLoc],
|
|
413
|
+
outliers: [],
|
|
414
|
+
description: match.detectorDescription,
|
|
415
|
+
firstSeen: new Date().toISOString(),
|
|
416
|
+
lastSeen: new Date().toISOString(),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// Ignore manifest errors
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Scan files single-threaded (fallback)
|
|
426
|
+
*/
|
|
427
|
+
async scanFilesSingleThreaded(files, projectContext) {
|
|
180
428
|
const startTime = Date.now();
|
|
181
429
|
const fileResults = [];
|
|
182
430
|
const errors = [];
|
|
183
|
-
// Aggregation maps
|
|
184
431
|
const patternMap = new Map();
|
|
185
432
|
const allViolations = [];
|
|
186
|
-
// Manifest pattern aggregation
|
|
187
433
|
const manifestPatternMap = new Map();
|
|
188
434
|
let detectorsRan = 0;
|
|
189
435
|
let detectorsSkipped = 0;
|
|
@@ -193,7 +439,6 @@ export class ScannerService {
|
|
|
193
439
|
const fullPaths = files.map(f => path.join(this.config.rootDir, f));
|
|
194
440
|
const changedPaths = await this.manifestStore.getChangedFiles(fullPaths);
|
|
195
441
|
filesToScan = changedPaths.map(f => path.relative(this.config.rootDir, f));
|
|
196
|
-
// Clear patterns for changed files
|
|
197
442
|
for (const file of filesToScan) {
|
|
198
443
|
this.manifestStore.clearFilePatterns(path.join(this.config.rootDir, file));
|
|
199
444
|
}
|
|
@@ -202,7 +447,6 @@ export class ScannerService {
|
|
|
202
447
|
const fileStart = Date.now();
|
|
203
448
|
const filePath = path.join(this.config.rootDir, file);
|
|
204
449
|
const language = getLanguage(file);
|
|
205
|
-
// Skip files we can't detect language for
|
|
206
450
|
if (!language) {
|
|
207
451
|
fileResults.push({
|
|
208
452
|
file,
|
|
@@ -217,14 +461,13 @@ export class ScannerService {
|
|
|
217
461
|
const contentHash = hashContent(content);
|
|
218
462
|
const filePatterns = [];
|
|
219
463
|
const fileViolations = [];
|
|
220
|
-
// Create detection context
|
|
221
464
|
const context = {
|
|
222
465
|
file,
|
|
223
466
|
content,
|
|
224
467
|
language,
|
|
225
|
-
ast: null,
|
|
226
|
-
imports: [],
|
|
227
|
-
exports: [],
|
|
468
|
+
ast: null,
|
|
469
|
+
imports: [],
|
|
470
|
+
exports: [],
|
|
228
471
|
extension: path.extname(file),
|
|
229
472
|
isTestFile: /\.(test|spec)\.[jt]sx?$/.test(file) || file.includes('__tests__'),
|
|
230
473
|
isTypeDefinition: file.endsWith('.d.ts'),
|
|
@@ -234,7 +477,6 @@ export class ScannerService {
|
|
|
234
477
|
config: projectContext.config,
|
|
235
478
|
},
|
|
236
479
|
};
|
|
237
|
-
// Run applicable detectors
|
|
238
480
|
for (const detector of this.detectors) {
|
|
239
481
|
if (!isDetectorApplicable(detector, language)) {
|
|
240
482
|
detectorsSkipped++;
|
|
@@ -244,7 +486,6 @@ export class ScannerService {
|
|
|
244
486
|
try {
|
|
245
487
|
const result = await detector.detect(context);
|
|
246
488
|
const info = detector.getInfo();
|
|
247
|
-
// Process patterns
|
|
248
489
|
for (const match of result.patterns) {
|
|
249
490
|
filePatterns.push({
|
|
250
491
|
patternId: match.patternId,
|
|
@@ -252,7 +493,6 @@ export class ScannerService {
|
|
|
252
493
|
confidence: match.confidence,
|
|
253
494
|
location: match.location,
|
|
254
495
|
});
|
|
255
|
-
// Aggregate pattern
|
|
256
496
|
const key = match.patternId;
|
|
257
497
|
const existing = patternMap.get(key);
|
|
258
498
|
if (existing) {
|
|
@@ -273,7 +513,6 @@ export class ScannerService {
|
|
|
273
513
|
occurrences: 1,
|
|
274
514
|
});
|
|
275
515
|
}
|
|
276
|
-
// Build manifest pattern with semantic location
|
|
277
516
|
if (this.config.generateManifest) {
|
|
278
517
|
const semanticLoc = this.createSemanticLocation(match.location, content, contentHash, language);
|
|
279
518
|
const manifestKey = `${info.category}/${info.subcategory}/${match.patternId}`;
|
|
@@ -300,17 +539,14 @@ export class ScannerService {
|
|
|
300
539
|
}
|
|
301
540
|
}
|
|
302
541
|
}
|
|
303
|
-
// Process violations
|
|
304
|
-
// First check the standard violations array
|
|
542
|
+
// Process violations (same as before)
|
|
305
543
|
let violationsToProcess = result.violations;
|
|
306
|
-
// If empty, check for violations in custom metadata (many detectors store them there)
|
|
307
544
|
if (violationsToProcess.length === 0 && result.metadata?.custom) {
|
|
308
545
|
const customData = result.metadata.custom;
|
|
309
546
|
const customViolations = customData['violations'];
|
|
310
547
|
if (customViolations && Array.isArray(customViolations)) {
|
|
311
|
-
// Convert custom violations to standard format
|
|
312
548
|
violationsToProcess = customViolations.map(cv => {
|
|
313
|
-
const
|
|
549
|
+
const v = {
|
|
314
550
|
id: `${detector.id}-${cv.file}-${cv.line}-${cv.column}`,
|
|
315
551
|
patternId: detector.id,
|
|
316
552
|
severity: cv.severity || 'warning',
|
|
@@ -328,9 +564,9 @@ export class ScannerService {
|
|
|
328
564
|
occurrences: 1,
|
|
329
565
|
};
|
|
330
566
|
if (cv.type) {
|
|
331
|
-
|
|
567
|
+
v.explanation = `Violation type: ${cv.type}`;
|
|
332
568
|
}
|
|
333
|
-
return
|
|
569
|
+
return v;
|
|
334
570
|
});
|
|
335
571
|
}
|
|
336
572
|
}
|
|
@@ -344,39 +580,13 @@ export class ScannerService {
|
|
|
344
580
|
line: violation.range.start.line + 1,
|
|
345
581
|
column: violation.range.start.character + 1,
|
|
346
582
|
message: violation.message,
|
|
347
|
-
explanation: violation.explanation,
|
|
348
583
|
suggestedFix: violation.expected,
|
|
349
584
|
};
|
|
585
|
+
if (violation.explanation) {
|
|
586
|
+
aggViolation.explanation = violation.explanation;
|
|
587
|
+
}
|
|
350
588
|
fileViolations.push(aggViolation);
|
|
351
589
|
allViolations.push(aggViolation);
|
|
352
|
-
// Add violation as outlier to manifest
|
|
353
|
-
if (this.config.generateManifest) {
|
|
354
|
-
const outlierLoc = this.createSemanticLocation({
|
|
355
|
-
file: violation.file,
|
|
356
|
-
line: violation.range.start.line + 1,
|
|
357
|
-
column: violation.range.start.character + 1,
|
|
358
|
-
}, content, contentHash, language, violation.message);
|
|
359
|
-
const manifestKey = `${info.category}/${info.subcategory}/${violation.patternId}`;
|
|
360
|
-
const existingManifest = manifestPatternMap.get(manifestKey);
|
|
361
|
-
if (existingManifest) {
|
|
362
|
-
existingManifest.outliers.push(outlierLoc);
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
manifestPatternMap.set(manifestKey, {
|
|
366
|
-
id: manifestKey,
|
|
367
|
-
name: info.name,
|
|
368
|
-
category: info.category,
|
|
369
|
-
subcategory: info.subcategory,
|
|
370
|
-
status: 'discovered',
|
|
371
|
-
confidence: 0.5,
|
|
372
|
-
locations: [],
|
|
373
|
-
outliers: [outlierLoc],
|
|
374
|
-
description: info.description,
|
|
375
|
-
firstSeen: new Date().toISOString(),
|
|
376
|
-
lastSeen: new Date().toISOString(),
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
590
|
}
|
|
381
591
|
}
|
|
382
592
|
catch (detectorError) {
|
|
@@ -404,9 +614,7 @@ export class ScannerService {
|
|
|
404
614
|
});
|
|
405
615
|
}
|
|
406
616
|
}
|
|
407
|
-
// Convert pattern map to array
|
|
408
617
|
const patterns = Array.from(patternMap.values());
|
|
409
|
-
// Build and save manifest
|
|
410
618
|
let manifest;
|
|
411
619
|
if (this.config.generateManifest && this.manifestStore) {
|
|
412
620
|
const manifestPatterns = Array.from(manifestPatternMap.values());
|
|
@@ -414,7 +622,7 @@ export class ScannerService {
|
|
|
414
622
|
await this.manifestStore.save();
|
|
415
623
|
manifest = await this.manifestStore.get();
|
|
416
624
|
}
|
|
417
|
-
|
|
625
|
+
return {
|
|
418
626
|
files: fileResults,
|
|
419
627
|
patterns,
|
|
420
628
|
violations: allViolations,
|
|
@@ -428,11 +636,8 @@ export class ScannerService {
|
|
|
428
636
|
ran: detectorsRan,
|
|
429
637
|
skipped: detectorsSkipped,
|
|
430
638
|
},
|
|
639
|
+
manifest,
|
|
431
640
|
};
|
|
432
|
-
if (manifest) {
|
|
433
|
-
result.manifest = manifest;
|
|
434
|
-
}
|
|
435
|
-
return result;
|
|
436
641
|
}
|
|
437
642
|
/**
|
|
438
643
|
* Create a semantic location from a basic location
|
|
@@ -440,7 +645,6 @@ export class ScannerService {
|
|
|
440
645
|
createSemanticLocation(location, content, hash, language, name) {
|
|
441
646
|
const lines = content.split('\n');
|
|
442
647
|
const lineContent = lines[location.line - 1] || '';
|
|
443
|
-
// Try to extract semantic info from the line
|
|
444
648
|
const semanticInfo = this.extractSemanticInfo(lineContent, language);
|
|
445
649
|
const result = {
|
|
446
650
|
file: location.file,
|
|
@@ -465,89 +669,81 @@ export class ScannerService {
|
|
|
465
669
|
*/
|
|
466
670
|
extractSemanticInfo(line, language) {
|
|
467
671
|
const trimmed = line.trim();
|
|
468
|
-
// TypeScript/JavaScript patterns
|
|
469
672
|
if (language === 'typescript' || language === 'javascript') {
|
|
470
|
-
// Class
|
|
471
673
|
const classMatch = trimmed.match(/^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/);
|
|
472
674
|
if (classMatch && classMatch[1]) {
|
|
473
675
|
return { type: 'class', name: classMatch[1], signature: trimmed.substring(0, 80) };
|
|
474
676
|
}
|
|
475
|
-
// Function
|
|
476
677
|
const funcMatch = trimmed.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
477
678
|
if (funcMatch && funcMatch[1]) {
|
|
478
679
|
return { type: 'function', name: funcMatch[1], signature: trimmed.substring(0, 80) };
|
|
479
680
|
}
|
|
480
|
-
// Arrow function / const
|
|
481
681
|
const arrowMatch = trimmed.match(/^(?:export\s+)?const\s+(\w+)\s*=/);
|
|
482
682
|
if (arrowMatch && arrowMatch[1]) {
|
|
483
683
|
return { type: 'function', name: arrowMatch[1], signature: trimmed.substring(0, 80) };
|
|
484
684
|
}
|
|
485
|
-
// Interface
|
|
486
685
|
const interfaceMatch = trimmed.match(/^(?:export\s+)?interface\s+(\w+)/);
|
|
487
686
|
if (interfaceMatch && interfaceMatch[1]) {
|
|
488
687
|
return { type: 'interface', name: interfaceMatch[1], signature: trimmed.substring(0, 80) };
|
|
489
688
|
}
|
|
490
|
-
// Type
|
|
491
689
|
const typeMatch = trimmed.match(/^(?:export\s+)?type\s+(\w+)/);
|
|
492
690
|
if (typeMatch && typeMatch[1]) {
|
|
493
691
|
return { type: 'type', name: typeMatch[1], signature: trimmed.substring(0, 80) };
|
|
494
692
|
}
|
|
495
693
|
}
|
|
496
|
-
// Python patterns
|
|
497
694
|
if (language === 'python') {
|
|
498
|
-
// Class
|
|
499
695
|
const classMatch = trimmed.match(/^class\s+(\w+)/);
|
|
500
696
|
if (classMatch && classMatch[1]) {
|
|
501
697
|
return { type: 'class', name: classMatch[1], signature: trimmed.substring(0, 80) };
|
|
502
698
|
}
|
|
503
|
-
// Function/method
|
|
504
699
|
const defMatch = trimmed.match(/^(?:async\s+)?def\s+(\w+)/);
|
|
505
700
|
if (defMatch && defMatch[1]) {
|
|
506
701
|
return { type: 'function', name: defMatch[1], signature: trimmed.substring(0, 80) };
|
|
507
702
|
}
|
|
508
|
-
// Decorator
|
|
509
703
|
const decoratorMatch = trimmed.match(/^@(\w+)/);
|
|
510
704
|
if (decoratorMatch && decoratorMatch[1]) {
|
|
511
705
|
return { type: 'decorator', name: decoratorMatch[1], signature: trimmed };
|
|
512
706
|
}
|
|
513
707
|
}
|
|
514
|
-
// PHP patterns
|
|
515
708
|
if (language === 'php') {
|
|
516
|
-
// Class
|
|
517
709
|
const classMatch = trimmed.match(/^(?:abstract\s+|final\s+)?class\s+(\w+)/);
|
|
518
710
|
if (classMatch && classMatch[1]) {
|
|
519
711
|
return { type: 'class', name: classMatch[1], signature: trimmed.substring(0, 80) };
|
|
520
712
|
}
|
|
521
|
-
// Interface
|
|
522
713
|
const interfaceMatch = trimmed.match(/^interface\s+(\w+)/);
|
|
523
714
|
if (interfaceMatch && interfaceMatch[1]) {
|
|
524
715
|
return { type: 'interface', name: interfaceMatch[1], signature: trimmed.substring(0, 80) };
|
|
525
716
|
}
|
|
526
|
-
// Trait
|
|
527
717
|
const traitMatch = trimmed.match(/^trait\s+(\w+)/);
|
|
528
718
|
if (traitMatch && traitMatch[1]) {
|
|
529
719
|
return { type: 'class', name: traitMatch[1], signature: trimmed.substring(0, 80) };
|
|
530
720
|
}
|
|
531
|
-
// Function/method
|
|
532
721
|
const funcMatch = trimmed.match(/^(?:public|protected|private)?\s*(?:static\s+)?function\s+(\w+)/);
|
|
533
722
|
if (funcMatch && funcMatch[1]) {
|
|
534
723
|
return { type: 'function', name: funcMatch[1], signature: trimmed.substring(0, 80) };
|
|
535
724
|
}
|
|
536
|
-
// PHP 8 Attribute
|
|
537
725
|
const attrMatch = trimmed.match(/^#\[(\w+)/);
|
|
538
726
|
if (attrMatch && attrMatch[1]) {
|
|
539
727
|
return { type: 'decorator', name: attrMatch[1], signature: trimmed };
|
|
540
728
|
}
|
|
541
729
|
}
|
|
542
|
-
// Default to block
|
|
543
730
|
return { type: 'block' };
|
|
544
731
|
}
|
|
545
732
|
/**
|
|
546
|
-
* Get the manifest store
|
|
733
|
+
* Get the manifest store
|
|
547
734
|
*/
|
|
548
735
|
getManifestStore() {
|
|
549
736
|
return this.manifestStore;
|
|
550
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* Destroy the worker pool
|
|
740
|
+
*/
|
|
741
|
+
async destroy() {
|
|
742
|
+
if (this.pool) {
|
|
743
|
+
await this.pool.destroy();
|
|
744
|
+
this.pool = null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
551
747
|
}
|
|
552
748
|
/**
|
|
553
749
|
* Create a scanner service
|