circle-ir 3.25.0 → 3.28.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/analysis/passes/scan-secrets-pass.d.ts +60 -0
- package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -0
- package/dist/analysis/passes/scan-secrets-pass.js +345 -0
- package/dist/analysis/passes/scan-secrets-pass.js.map +1 -0
- package/dist/analyzer.d.ts +1 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +272 -249
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +507 -213
- package/dist/core/circle-ir-core.cjs +17 -1
- package/dist/core/circle-ir-core.js +17 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/parser.d.ts +19 -1
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +53 -2
- package/dist/core/parser.js.map +1 -1
- package/dist/graph/analysis-pass.d.ts +10 -0
- package/dist/graph/analysis-pass.d.ts.map +1 -1
- package/dist/graph/analysis-pass.js +3 -0
- package/dist/graph/analysis-pass.js.map +1 -1
- package/package.json +1 -1
package/dist/analyzer.js
CHANGED
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
* 38. MissingStreamPass — whole-file read without streaming (performance)
|
|
46
46
|
* 39. GodClassPass — class with high WMC/LCOM2/CBO metrics (CWE-1060)
|
|
47
47
|
* 40. NamingConventionPass — class/method names violate language conventions
|
|
48
|
+
* 41. ScanSecretsPass — hardcoded credentials: provider regexes + Shannon entropy (CWE-798)
|
|
48
49
|
*
|
|
49
50
|
* Removed from default pipeline (raw IR signals still available for circle-ir-ai):
|
|
50
51
|
* – MissingGuardDomPass — false positives in framework-auth codebases (see pass file)
|
|
51
52
|
* – FeatureEnvyPass — fires on legitimate delegation patterns (see pass file)
|
|
52
53
|
*/
|
|
53
|
-
import { initParser, parse, extractMeta, extractTypes, extractCalls, extractImports, extractExports, buildCFG, buildDFG, collectAllNodes, } from './core/index.js';
|
|
54
|
+
import { initParser, parse, disposeTree, extractMeta, extractTypes, extractCalls, extractImports, extractExports, buildCFG, buildDFG, collectAllNodes, } from './core/index.js';
|
|
54
55
|
import { analyzeTaint, getDefaultConfig, detectUnresolved, analyzeConstantPropagation, isFalsePositive, } from './analysis/index.js';
|
|
55
56
|
import { registerBuiltinPlugins } from './languages/index.js';
|
|
56
57
|
import { logger } from './utils/logger.js';
|
|
@@ -102,6 +103,7 @@ import { MissingStreamPass } from './analysis/passes/missing-stream-pass.js';
|
|
|
102
103
|
import { GodClassPass } from './analysis/passes/god-class-pass.js';
|
|
103
104
|
import { NamingConventionPass } from './analysis/passes/naming-convention-pass.js';
|
|
104
105
|
import { SecurityHeadersPass, checkInheritedCorsHeaders } from './analysis/passes/security-headers-pass.js';
|
|
106
|
+
import { ScanSecretsPass } from './analysis/passes/scan-secrets-pass.js';
|
|
105
107
|
// Project-level pass imports
|
|
106
108
|
import { ImportGraph } from './graph/import-graph.js';
|
|
107
109
|
import { CircularDependencyPass } from './analysis/passes/circular-dependency-pass.js';
|
|
@@ -247,136 +249,147 @@ export async function analyze(code, filePath, language, options = {}) {
|
|
|
247
249
|
return analyzeHtmlFile(code, filePath, options);
|
|
248
250
|
}
|
|
249
251
|
logger.debug('Analyzing file', { filePath, language, codeLength: code.length });
|
|
250
|
-
// Parse the code
|
|
252
|
+
// Parse the code. The Tree holds tree-sitter WASM memory; we MUST dispose
|
|
253
|
+
// it before returning, otherwise the WASM heap grows unboundedly across
|
|
254
|
+
// many analyze() calls in the same process (issue #16).
|
|
251
255
|
const tree = await parse(code, language);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
256
|
+
try {
|
|
257
|
+
logger.trace('Parsed AST', { rootNodeType: tree.rootNode.type });
|
|
258
|
+
// Collect all node types in a single traversal for better performance
|
|
259
|
+
const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
|
|
260
|
+
// Extract all IR components
|
|
261
|
+
const meta = extractMeta(code, tree, filePath, language);
|
|
262
|
+
const types = extractTypes(tree, nodeCache, language);
|
|
263
|
+
const calls = extractCalls(tree, nodeCache, language);
|
|
264
|
+
const imports = extractImports(tree, language);
|
|
265
|
+
const exports = extractExports(types);
|
|
266
|
+
const cfg = buildCFG(tree, language);
|
|
267
|
+
const dfg = buildDFG(tree, nodeCache, language);
|
|
268
|
+
// Build CodeGraph once — shared across all passes.
|
|
269
|
+
// Taint is empty at construction time; sources/sinks/sanitizers are populated by passes.
|
|
270
|
+
const graph = new CodeGraph({
|
|
271
|
+
meta, types, calls, cfg, dfg,
|
|
272
|
+
taint: { sources: [], sinks: [], sanitizers: [] },
|
|
273
|
+
imports, exports, unresolved: [], enriched: {},
|
|
274
|
+
});
|
|
275
|
+
const config = options.taintConfig ?? getDefaultConfig();
|
|
276
|
+
// Build the analysis pipeline with configurable pass options
|
|
277
|
+
const disabledPasses = new Set(options.disabledPasses ?? []);
|
|
278
|
+
const passOpts = options.passOptions ?? {};
|
|
279
|
+
const pipeline = new AnalysisPipeline();
|
|
280
|
+
// Core taint analysis passes (always enabled)
|
|
281
|
+
pipeline.add(new TaintMatcherPass());
|
|
282
|
+
pipeline.add(new ConstantPropagationPass(tree));
|
|
283
|
+
pipeline.add(new LanguageSourcesPass());
|
|
284
|
+
pipeline.add(new SinkFilterPass());
|
|
285
|
+
pipeline.add(new TaintPropagationPass());
|
|
286
|
+
pipeline.add(new InterproceduralPass());
|
|
287
|
+
// Secret scanner runs after LanguageSourcesPass so the legacy Bash
|
|
288
|
+
// `hardcoded-credential` findings are already in the dedup buffer.
|
|
289
|
+
if (!disabledPasses.has('scan-secrets'))
|
|
290
|
+
pipeline.add(new ScanSecretsPass());
|
|
291
|
+
// Optional passes — can be disabled via disabledPasses
|
|
292
|
+
if (!disabledPasses.has('dead-code'))
|
|
293
|
+
pipeline.add(new DeadCodePass());
|
|
294
|
+
if (!disabledPasses.has('missing-await'))
|
|
295
|
+
pipeline.add(new MissingAwaitPass());
|
|
296
|
+
if (!disabledPasses.has('n-plus-one'))
|
|
297
|
+
pipeline.add(new NPlusOnePass());
|
|
298
|
+
if (!disabledPasses.has('missing-public-doc'))
|
|
299
|
+
pipeline.add(new MissingPublicDocPass());
|
|
300
|
+
if (!disabledPasses.has('todo-in-prod'))
|
|
301
|
+
pipeline.add(new TodoInProdPass());
|
|
302
|
+
if (!disabledPasses.has('string-concat-loop'))
|
|
303
|
+
pipeline.add(new StringConcatLoopPass());
|
|
304
|
+
if (!disabledPasses.has('sync-io-async'))
|
|
305
|
+
pipeline.add(new SyncIoAsyncPass());
|
|
306
|
+
if (!disabledPasses.has('unchecked-return'))
|
|
307
|
+
pipeline.add(new UncheckedReturnPass());
|
|
308
|
+
if (!disabledPasses.has('null-deref'))
|
|
309
|
+
pipeline.add(new NullDerefPass());
|
|
310
|
+
if (!disabledPasses.has('resource-leak'))
|
|
311
|
+
pipeline.add(new ResourceLeakPass());
|
|
312
|
+
if (!disabledPasses.has('variable-shadowing'))
|
|
313
|
+
pipeline.add(new VariableShadowingPass());
|
|
314
|
+
if (!disabledPasses.has('leaked-global'))
|
|
315
|
+
pipeline.add(new LeakedGlobalPass());
|
|
316
|
+
if (!disabledPasses.has('unused-variable'))
|
|
317
|
+
pipeline.add(new UnusedVariablePass());
|
|
318
|
+
if (!disabledPasses.has('dependency-fan-out'))
|
|
319
|
+
pipeline.add(new DependencyFanOutPass(passOpts.dependencyFanOut));
|
|
320
|
+
if (!disabledPasses.has('stale-doc-ref'))
|
|
321
|
+
pipeline.add(new StaleDocRefPass());
|
|
322
|
+
if (!disabledPasses.has('infinite-loop'))
|
|
323
|
+
pipeline.add(new InfiniteLoopPass());
|
|
324
|
+
if (!disabledPasses.has('deep-inheritance'))
|
|
325
|
+
pipeline.add(new DeepInheritancePass());
|
|
326
|
+
if (!disabledPasses.has('redundant-loop-computation'))
|
|
327
|
+
pipeline.add(new RedundantLoopPass());
|
|
328
|
+
if (!disabledPasses.has('unbounded-collection'))
|
|
329
|
+
pipeline.add(new UnboundedCollectionPass(passOpts.unboundedCollection));
|
|
330
|
+
if (!disabledPasses.has('serial-await'))
|
|
331
|
+
pipeline.add(new SerialAwaitPass());
|
|
332
|
+
if (!disabledPasses.has('react-inline-jsx'))
|
|
333
|
+
pipeline.add(new ReactInlineJsxPass());
|
|
334
|
+
if (!disabledPasses.has('swallowed-exception'))
|
|
335
|
+
pipeline.add(new SwallowedExceptionPass());
|
|
336
|
+
if (!disabledPasses.has('broad-catch'))
|
|
337
|
+
pipeline.add(new BroadCatchPass());
|
|
338
|
+
if (!disabledPasses.has('unhandled-exception'))
|
|
339
|
+
pipeline.add(new UnhandledExceptionPass());
|
|
340
|
+
if (!disabledPasses.has('double-close'))
|
|
341
|
+
pipeline.add(new DoubleClosePass());
|
|
342
|
+
if (!disabledPasses.has('use-after-close'))
|
|
343
|
+
pipeline.add(new UseAfterClosePass());
|
|
344
|
+
if (!disabledPasses.has('cleanup-verify'))
|
|
345
|
+
pipeline.add(new CleanupVerifyPass());
|
|
346
|
+
if (!disabledPasses.has('missing-override'))
|
|
347
|
+
pipeline.add(new MissingOverridePass());
|
|
348
|
+
if (!disabledPasses.has('unused-interface-method'))
|
|
349
|
+
pipeline.add(new UnusedInterfaceMethodPass());
|
|
350
|
+
if (!disabledPasses.has('blocking-main-thread'))
|
|
351
|
+
pipeline.add(new BlockingMainThreadPass());
|
|
352
|
+
if (!disabledPasses.has('excessive-allocation'))
|
|
353
|
+
pipeline.add(new ExcessiveAllocationPass());
|
|
354
|
+
if (!disabledPasses.has('missing-stream'))
|
|
355
|
+
pipeline.add(new MissingStreamPass());
|
|
356
|
+
if (!disabledPasses.has('god-class'))
|
|
357
|
+
pipeline.add(new GodClassPass());
|
|
358
|
+
if (!disabledPasses.has('naming-convention'))
|
|
359
|
+
pipeline.add(new NamingConventionPass(passOpts.namingConvention));
|
|
360
|
+
if (!disabledPasses.has('security-headers'))
|
|
361
|
+
pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
|
|
362
|
+
// Run the pipeline
|
|
363
|
+
const { results, findings } = pipeline.run(graph, code, language, config);
|
|
364
|
+
const sinkFilter = results.get('sink-filter');
|
|
365
|
+
const interProc = results.get('interprocedural');
|
|
366
|
+
const taint = {
|
|
367
|
+
sources: sinkFilter.sources,
|
|
368
|
+
sinks: [...sinkFilter.sinks, ...interProc.additionalSinks],
|
|
369
|
+
sanitizers: sinkFilter.sanitizers,
|
|
370
|
+
flows: interProc.additionalFlows,
|
|
371
|
+
interprocedural: interProc.interprocedural,
|
|
372
|
+
};
|
|
373
|
+
const unresolved = detectUnresolved(calls, types, dfg);
|
|
374
|
+
const enriched = buildEnriched(types, calls, taint.sources, taint.sinks);
|
|
375
|
+
// Compute software metrics (CK suite, Halstead, composite scores)
|
|
376
|
+
const metricValues = new MetricRunner().run({ meta, types, calls, cfg, dfg, taint, imports, exports, unresolved, enriched }, code, language);
|
|
377
|
+
logger.debug('Analysis complete', {
|
|
378
|
+
filePath,
|
|
379
|
+
finalSources: taint.sources.length,
|
|
380
|
+
finalSinks: taint.sinks.length,
|
|
381
|
+
flows: taint.flows?.length ?? 0,
|
|
382
|
+
unresolvedItems: unresolved.length,
|
|
383
|
+
});
|
|
384
|
+
return {
|
|
385
|
+
meta, types, calls, cfg, dfg, taint, imports, exports, unresolved, enriched,
|
|
386
|
+
findings: findings.length > 0 ? findings : undefined,
|
|
387
|
+
metrics: { file: filePath, metrics: metricValues },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
finally {
|
|
391
|
+
disposeTree(tree);
|
|
392
|
+
}
|
|
380
393
|
}
|
|
381
394
|
// ---------------------------------------------------------------------------
|
|
382
395
|
// HTML preprocessor
|
|
@@ -390,64 +403,69 @@ async function analyzeHtmlFile(code, filePath, options) {
|
|
|
390
403
|
logger.debug('Analyzing HTML file', { filePath, codeLength: code.length });
|
|
391
404
|
// Parse HTML
|
|
392
405
|
const tree = await parse(code, 'html');
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
block.scriptType === '
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
406
|
+
try {
|
|
407
|
+
const meta = extractMeta(code, tree, filePath, 'html');
|
|
408
|
+
// Extract script blocks and event handlers
|
|
409
|
+
const { scriptBlocks, eventHandlers } = extractHtmlContent(tree.rootNode);
|
|
410
|
+
logger.debug('HTML extraction', {
|
|
411
|
+
filePath,
|
|
412
|
+
inlineScripts: scriptBlocks.filter(b => b.kind === 'inline').length,
|
|
413
|
+
externalScripts: scriptBlocks.filter(b => b.kind === 'external-src').length,
|
|
414
|
+
eventHandlers: eventHandlers.length,
|
|
415
|
+
});
|
|
416
|
+
// Analyze each inline script block via standard JS pipeline
|
|
417
|
+
const scriptResults = [];
|
|
418
|
+
for (const block of scriptBlocks) {
|
|
419
|
+
if (block.kind !== 'inline' || !block.code.trim())
|
|
420
|
+
continue;
|
|
421
|
+
// Determine script language from type/lang attribute
|
|
422
|
+
const scriptLang = block.scriptType === 'ts' || block.scriptType === 'typescript' ||
|
|
423
|
+
block.scriptType === 'text/typescript'
|
|
424
|
+
? 'typescript'
|
|
425
|
+
: 'javascript';
|
|
426
|
+
try {
|
|
427
|
+
const ir = await analyze(block.code, filePath, scriptLang, options);
|
|
428
|
+
scriptResults.push({ ir, lineOffset: block.lineOffset });
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
logger.warn('Failed to analyze script block', {
|
|
432
|
+
filePath,
|
|
433
|
+
lineOffset: block.lineOffset,
|
|
434
|
+
error: e instanceof Error ? e.message : String(e),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
415
437
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
438
|
+
// Analyze inline event handlers (wrap in synthetic function)
|
|
439
|
+
for (const handler of eventHandlers) {
|
|
440
|
+
const wrappedCode = `function __${handler.eventName}_handler() { ${handler.code} }`;
|
|
441
|
+
try {
|
|
442
|
+
const ir = await analyze(wrappedCode, filePath, 'javascript', options);
|
|
443
|
+
scriptResults.push({ ir, lineOffset: handler.line });
|
|
444
|
+
}
|
|
445
|
+
catch (e) {
|
|
446
|
+
logger.warn('Failed to analyze event handler', {
|
|
447
|
+
filePath,
|
|
448
|
+
eventName: handler.eventName,
|
|
449
|
+
line: handler.line,
|
|
450
|
+
error: e instanceof Error ? e.message : String(e),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
422
453
|
}
|
|
454
|
+
// Run attribute-level security checks
|
|
455
|
+
const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
|
|
456
|
+
// Merge everything
|
|
457
|
+
const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
|
|
458
|
+
logger.debug('HTML analysis complete', {
|
|
459
|
+
filePath,
|
|
460
|
+
scriptBlocks: scriptResults.length,
|
|
461
|
+
attributeFindings: attributeFindings.length,
|
|
462
|
+
totalFindings: result.findings?.length ?? 0,
|
|
463
|
+
});
|
|
464
|
+
return result;
|
|
423
465
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const wrappedCode = `function __${handler.eventName}_handler() { ${handler.code} }`;
|
|
427
|
-
try {
|
|
428
|
-
const ir = await analyze(wrappedCode, filePath, 'javascript', options);
|
|
429
|
-
scriptResults.push({ ir, lineOffset: handler.line });
|
|
430
|
-
}
|
|
431
|
-
catch (e) {
|
|
432
|
-
logger.warn('Failed to analyze event handler', {
|
|
433
|
-
filePath,
|
|
434
|
-
eventName: handler.eventName,
|
|
435
|
-
line: handler.line,
|
|
436
|
-
error: e instanceof Error ? e.message : String(e),
|
|
437
|
-
});
|
|
438
|
-
}
|
|
466
|
+
finally {
|
|
467
|
+
disposeTree(tree);
|
|
439
468
|
}
|
|
440
|
-
// Run attribute-level security checks
|
|
441
|
-
const attributeFindings = runHtmlAttributeSecurityChecks(tree.rootNode, filePath);
|
|
442
|
-
// Merge everything
|
|
443
|
-
const result = mergeHtmlResults(meta, scriptResults, attributeFindings);
|
|
444
|
-
logger.debug('HTML analysis complete', {
|
|
445
|
-
filePath,
|
|
446
|
-
scriptBlocks: scriptResults.length,
|
|
447
|
-
attributeFindings: attributeFindings.length,
|
|
448
|
-
totalFindings: result.findings?.length ?? 0,
|
|
449
|
-
});
|
|
450
|
-
return result;
|
|
451
469
|
}
|
|
452
470
|
// ---------------------------------------------------------------------------
|
|
453
471
|
// Simplified API response format
|
|
@@ -463,75 +481,80 @@ export async function analyzeForAPI(code, filePath, language, options = {}) {
|
|
|
463
481
|
const parseStart = performance.now();
|
|
464
482
|
const tree = await parse(code, language);
|
|
465
483
|
const parseTime = performance.now() - parseStart;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
484
|
+
try {
|
|
485
|
+
const analysisStart = performance.now();
|
|
486
|
+
const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
|
|
487
|
+
const types = extractTypes(tree, nodeCache, language);
|
|
488
|
+
const calls = extractCalls(tree, nodeCache, language);
|
|
489
|
+
// Run constant propagation
|
|
490
|
+
const constPropResult = analyzeConstantPropagation(tree, code);
|
|
491
|
+
const config = options.taintConfig ?? getDefaultConfig();
|
|
492
|
+
const taint = analyzeTaint(calls, types, config);
|
|
493
|
+
// Filter sinks in dead code
|
|
494
|
+
let filteredSinks = taint.sinks.filter(sink => !constPropResult.unreachableLines.has(sink.line));
|
|
495
|
+
// Filter sinks whose arguments are proven clean (string literals, constants, etc.)
|
|
496
|
+
filteredSinks = filterCleanVariableSinks(filteredSinks, calls, constPropResult.tainted, constPropResult.symbols, undefined, constPropResult.sanitizedVars, constPropResult.synchronizedLines);
|
|
497
|
+
// Filter sinks wrapped by sanitizers on the same line
|
|
498
|
+
filteredSinks = filterSanitizedSinks(filteredSinks, taint.sanitizers ?? [], calls);
|
|
499
|
+
// Python: reduce XPath false-positives using forward taint propagation +
|
|
500
|
+
// apostrophe-guard sanitizer detection.
|
|
501
|
+
let pythonTaintedVars = new Map();
|
|
502
|
+
if (language === 'python') {
|
|
503
|
+
pythonTaintedVars = buildPythonTaintedVars(code);
|
|
504
|
+
const pythonSanitizedVars = buildPythonSanitizedVars(code, pythonTaintedVars);
|
|
505
|
+
const sourceLines = code.split('\n');
|
|
506
|
+
filteredSinks = filteredSinks.filter(sink => {
|
|
507
|
+
if (sink.type !== 'xpath_injection')
|
|
508
|
+
return true;
|
|
509
|
+
const sinkLineText = sourceLines[sink.line - 1] ?? '';
|
|
510
|
+
const taintedVarOnLine = [...pythonTaintedVars.keys()].find(v => new RegExp(`\\b${v}\\b`).test(sinkLineText));
|
|
511
|
+
if (!taintedVarOnLine)
|
|
512
|
+
return false;
|
|
513
|
+
if (pythonSanitizedVars.has(taintedVarOnLine))
|
|
514
|
+
return false;
|
|
515
|
+
if (new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${taintedVarOnLine}\\b`).test(sinkLineText))
|
|
516
|
+
return false;
|
|
489
517
|
return true;
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
vulnerabilities.push({
|
|
510
|
-
type: 'trust_boundary',
|
|
511
|
-
cwe: 'CWE-501',
|
|
512
|
-
severity: 'medium',
|
|
513
|
-
source: { line: v.sourceLine, type: 'http_param' },
|
|
514
|
-
sink: { line: v.sinkLine, type: 'trust_boundary' },
|
|
515
|
-
confidence: 0.85,
|
|
516
|
-
});
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
// Generate vulnerabilities from source-sink pairs
|
|
521
|
+
const vulnerabilities = findVulnerabilities(taint.sources, filteredSinks, calls, constPropResult);
|
|
522
|
+
// Python: detect trust boundary violations (flask.session[key] = taintedVal)
|
|
523
|
+
if (language === 'python') {
|
|
524
|
+
const trustViolations = findPythonTrustBoundaryViolations(code, pythonTaintedVars);
|
|
525
|
+
for (const v of trustViolations) {
|
|
526
|
+
const alreadyReported = vulnerabilities.some(existing => existing.sink.line === v.sinkLine && existing.type === 'trust_boundary');
|
|
527
|
+
if (!alreadyReported) {
|
|
528
|
+
vulnerabilities.push({
|
|
529
|
+
type: 'trust_boundary',
|
|
530
|
+
cwe: 'CWE-501',
|
|
531
|
+
severity: 'medium',
|
|
532
|
+
source: { line: v.sourceLine, type: 'http_param' },
|
|
533
|
+
sink: { line: v.sinkLine, type: 'trust_boundary' },
|
|
534
|
+
confidence: 0.85,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
517
537
|
}
|
|
518
538
|
}
|
|
539
|
+
const analysisTime = performance.now() - analysisStart;
|
|
540
|
+
const totalTime = performance.now() - startTime;
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
analysis: {
|
|
544
|
+
sources: taint.sources,
|
|
545
|
+
sinks: filteredSinks,
|
|
546
|
+
vulnerabilities,
|
|
547
|
+
},
|
|
548
|
+
meta: {
|
|
549
|
+
parseTimeMs: Math.round(parseTime),
|
|
550
|
+
analysisTimeMs: Math.round(analysisTime),
|
|
551
|
+
totalTimeMs: Math.round(totalTime),
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
finally {
|
|
556
|
+
disposeTree(tree);
|
|
519
557
|
}
|
|
520
|
-
const analysisTime = performance.now() - analysisStart;
|
|
521
|
-
const totalTime = performance.now() - startTime;
|
|
522
|
-
return {
|
|
523
|
-
success: true,
|
|
524
|
-
analysis: {
|
|
525
|
-
sources: taint.sources,
|
|
526
|
-
sinks: filteredSinks,
|
|
527
|
-
vulnerabilities,
|
|
528
|
-
},
|
|
529
|
-
meta: {
|
|
530
|
-
parseTimeMs: Math.round(parseTime),
|
|
531
|
-
analysisTimeMs: Math.round(analysisTime),
|
|
532
|
-
totalTimeMs: Math.round(totalTime),
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
558
|
}
|
|
536
559
|
// ---------------------------------------------------------------------------
|
|
537
560
|
// Vulnerability matching (used by analyzeForAPI)
|