ferret-scan 2.2.0 → 2.3.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.
Files changed (159) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +104 -8
  4. package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
  5. package/dist/__tests__/AgentMonitor.test.js +235 -0
  6. package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
  7. package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
  8. package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
  9. package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
  10. package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
  11. package/dist/__tests__/IndicatorMatcher.test.js +245 -0
  12. package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
  13. package/dist/__tests__/MarketplaceScanner.test.js +212 -0
  14. package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
  15. package/dist/__tests__/RuleGenerator.test.js +207 -0
  16. package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
  17. package/dist/__tests__/ThreatFeed.test.js +359 -0
  18. package/dist/__tests__/WatchMode.test.d.ts +6 -0
  19. package/dist/__tests__/WatchMode.test.js +104 -0
  20. package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
  21. package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
  22. package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
  23. package/dist/__tests__/astAnalyzerFull.test.js +138 -0
  24. package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
  25. package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
  26. package/dist/__tests__/atlas.test.d.ts +6 -0
  27. package/dist/__tests__/atlas.test.js +319 -0
  28. package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
  29. package/dist/__tests__/atlasCatalog.test.js +200 -0
  30. package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
  31. package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
  32. package/dist/__tests__/baseline.test.d.ts +6 -0
  33. package/dist/__tests__/baseline.test.js +321 -0
  34. package/dist/__tests__/baselineExtra.test.d.ts +6 -0
  35. package/dist/__tests__/baselineExtra.test.js +317 -0
  36. package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
  37. package/dist/__tests__/capabilityMapping.test.js +49 -0
  38. package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
  39. package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
  40. package/dist/__tests__/complianceExtra.test.d.ts +6 -0
  41. package/dist/__tests__/complianceExtra.test.js +121 -0
  42. package/dist/__tests__/config.test.js +1 -1
  43. package/dist/__tests__/configLoader.test.d.ts +6 -0
  44. package/dist/__tests__/configLoader.test.js +225 -0
  45. package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
  46. package/dist/__tests__/configLoaderExtra.test.js +186 -0
  47. package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
  48. package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
  49. package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
  50. package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
  51. package/dist/__tests__/customRules.extra.test.d.ts +6 -0
  52. package/dist/__tests__/customRules.extra.test.js +245 -0
  53. package/dist/__tests__/customRules.test.d.ts +7 -0
  54. package/dist/__tests__/customRules.test.js +347 -0
  55. package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
  56. package/dist/__tests__/dependencyRisk.test.js +248 -0
  57. package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
  58. package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
  59. package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
  60. package/dist/__tests__/featureExitCodes.test.js +332 -0
  61. package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
  62. package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
  63. package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
  64. package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
  65. package/dist/__tests__/fixer.extra.test.d.ts +6 -0
  66. package/dist/__tests__/fixer.extra.test.js +135 -0
  67. package/dist/__tests__/fixerApply.test.d.ts +6 -0
  68. package/dist/__tests__/fixerApply.test.js +132 -0
  69. package/dist/__tests__/gitHooks.test.d.ts +7 -0
  70. package/dist/__tests__/gitHooks.test.js +188 -0
  71. package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
  72. package/dist/__tests__/htmlReporter.extra.test.js +126 -0
  73. package/dist/__tests__/interactiveTui.test.d.ts +6 -0
  74. package/dist/__tests__/interactiveTui.test.js +180 -0
  75. package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
  76. package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
  77. package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
  78. package/dist/__tests__/interactiveTuiMore.test.js +194 -0
  79. package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
  80. package/dist/__tests__/interactiveTuiSession.test.js +173 -0
  81. package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
  82. package/dist/__tests__/llmAnalysis.test.js +229 -0
  83. package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
  84. package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
  85. package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
  86. package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
  87. package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
  88. package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
  89. package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
  90. package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
  91. package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
  92. package/dist/__tests__/llmGroqTPM.test.js +89 -0
  93. package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
  94. package/dist/__tests__/llmProviderRetry.test.js +172 -0
  95. package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
  96. package/dist/__tests__/mcpValidator.extra.test.js +270 -0
  97. package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
  98. package/dist/__tests__/patternMatcherExtra.test.js +198 -0
  99. package/dist/__tests__/patternsCommon.test.d.ts +6 -0
  100. package/dist/__tests__/patternsCommon.test.js +107 -0
  101. package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
  102. package/dist/__tests__/policyEnforcement.test.js +510 -0
  103. package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
  104. package/dist/__tests__/quarantineExtra.test.js +214 -0
  105. package/dist/__tests__/redactionExtra.test.d.ts +6 -0
  106. package/dist/__tests__/redactionExtra.test.js +228 -0
  107. package/dist/__tests__/scanDiff.test.d.ts +7 -0
  108. package/dist/__tests__/scanDiff.test.js +266 -0
  109. package/dist/__tests__/scanFull.test.d.ts +6 -0
  110. package/dist/__tests__/scanFull.test.js +158 -0
  111. package/dist/__tests__/scannerDampening.test.d.ts +6 -0
  112. package/dist/__tests__/scannerDampening.test.js +160 -0
  113. package/dist/__tests__/scannerExtra.test.d.ts +6 -0
  114. package/dist/__tests__/scannerExtra.test.js +194 -0
  115. package/dist/__tests__/scannerMitre.test.d.ts +5 -0
  116. package/dist/__tests__/scannerMitre.test.js +141 -0
  117. package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
  118. package/dist/__tests__/scannerSSRF.test.js +149 -0
  119. package/dist/__tests__/schemas.test.d.ts +6 -0
  120. package/dist/__tests__/schemas.test.js +125 -0
  121. package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
  122. package/dist/__tests__/webhooks.extra.test.js +144 -0
  123. package/dist/__tests__/webhooks.test.d.ts +6 -0
  124. package/dist/__tests__/webhooks.test.js +154 -0
  125. package/dist/features/customRules.js +22 -29
  126. package/dist/features/mcpTrustScore.d.ts +17 -0
  127. package/dist/features/mcpTrustScore.js +74 -0
  128. package/dist/features/mcpValidator.d.ts +2 -0
  129. package/dist/features/mcpValidator.js +13 -0
  130. package/dist/features/policyEnforcement.d.ts +22 -22
  131. package/dist/intelligence/ThreatFeed.js +207 -62
  132. package/dist/remediation/Quarantine.js +24 -6
  133. package/dist/reporters/ConsoleReporter.js +10 -0
  134. package/dist/reporters/HtmlReporter.js +5 -0
  135. package/dist/reporters/SarifReporter.d.ts +1 -0
  136. package/dist/reporters/SarifReporter.js +1 -0
  137. package/dist/scanner/IAnalyzer.d.ts +19 -0
  138. package/dist/scanner/IAnalyzer.js +5 -0
  139. package/dist/scanner/Scanner.js +64 -125
  140. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  141. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  142. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  143. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  144. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  145. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  146. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  147. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  148. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  149. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  150. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  151. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  152. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  153. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  154. package/dist/types.d.ts +17 -0
  155. package/dist/types.js +1 -1
  156. package/dist/utils/safeRegex.d.ts +12 -51
  157. package/dist/utils/safeRegex.js +45 -62
  158. package/dist/utils/schemas.d.ts +64 -64
  159. package/package.json +24 -18
@@ -9,18 +9,18 @@ import { discoverFiles } from './FileDiscovery.js';
9
9
  import { matchRules } from './PatternMatcher.js';
10
10
  import { getRulesForScan } from '../rules/index.js';
11
11
  import { loadCustomRules, loadCustomRulesSource } from '../features/customRules.js';
12
- import { analyzeFile as analyzeFileSemantics, shouldAnalyze as shouldAnalyzeSemantics, getMemoryUsage } from '../analyzers/AstAnalyzer.js';
13
12
  import { analyzeCorrelations, shouldAnalyzeCorrelations } from '../analyzers/CorrelationAnalyzer.js';
14
- import { loadThreatDatabase } from '../intelligence/ThreatFeed.js';
15
- import { matchIndicators, shouldMatchIndicators } from '../intelligence/IndicatorMatcher.js';
16
- import { analyzeEntropy, entropyFindingsToFindings } from '../features/entropyAnalysis.js';
17
- import { validateMcpConfigContent, mcpAssessmentsToFindings } from '../features/mcpValidator.js';
18
- import { analyzeDependencies, dependencyAssessmentsToFindings } from '../features/dependencyRisk.js';
19
- import { analyzeCapabilitiesContent, capabilityProfileToFindings } from '../features/capabilityMapping.js';
20
13
  import { parseIgnoreComments, shouldIgnoreFinding } from '../features/ignoreComments.js';
21
14
  import { annotateFindingsWithMitreAtlas, setMitreAtlasTechniqueCatalog } from '../mitre/atlas.js';
22
15
  import { loadMitreAtlasTechniqueCatalog } from '../mitre/atlasCatalog.js';
23
- import { createLlmProvider, analyzeWithLlm } from '../features/llmAnalysis.js';
16
+ import { createLlmProvider } from '../features/llmAnalysis.js';
17
+ import { EntropyAnalyzer } from './analyzers/EntropyAnalyzer.js';
18
+ import { McpAnalyzer } from './analyzers/McpAnalyzer.js';
19
+ import { DependencyAnalyzer } from './analyzers/DependencyAnalyzer.js';
20
+ import { CapabilityAnalyzer } from './analyzers/CapabilityAnalyzer.js';
21
+ import { LlmAnalyzer } from './analyzers/LlmAnalyzer.js';
22
+ import { SemanticAnalyzer } from './analyzers/SemanticAnalyzer.js';
23
+ import { ThreatIntelAnalyzer } from './analyzers/ThreatIntelAnalyzer.js';
24
24
  import logger from '../utils/logger.js';
25
25
  import ora from 'ora';
26
26
  function looksLikeDocumentationPath(filePath) {
@@ -242,14 +242,27 @@ async function loadCustomRulesForScan(config) {
242
242
  }
243
243
  return rules.filter(r => r.enabled && config.categories.includes(r.category) && config.severities.includes(r.severity));
244
244
  }
245
+ /**
246
+ * Build the ordered list of analyzers for a scan.
247
+ */
248
+ function buildAnalyzers(llmProvider, llmRuntime) {
249
+ return [
250
+ new EntropyAnalyzer(),
251
+ new McpAnalyzer(),
252
+ new DependencyAnalyzer(),
253
+ new CapabilityAnalyzer(),
254
+ new LlmAnalyzer(llmProvider, llmRuntime),
255
+ new SemanticAnalyzer(),
256
+ new ThreatIntelAnalyzer(),
257
+ ];
258
+ }
245
259
  /**
246
260
  * Scan a single file
247
261
  */
248
- async function scanFile(file, config, rules, llmProvider, llmRuntime) {
262
+ async function scanFile(file, config, rules, analyzers) {
249
263
  try {
250
264
  const content = await readFile(file.path, 'utf-8');
251
265
  const allFindings = [];
252
- const fileErrors = [];
253
266
  let ignoreState;
254
267
  if (config.ignoreComments && content.includes('ferret-')) {
255
268
  const parsed = parseIgnoreComments(content, file.type);
@@ -262,124 +275,20 @@ async function scanFile(file, config, rules, llmProvider, llmRuntime) {
262
275
  contextLines: config.contextLines,
263
276
  });
264
277
  allFindings.push(...patternFindings);
265
- // Entropy analysis (secret detection) if enabled
266
- if (config.entropyAnalysis) {
267
- try {
268
- const entropyFindings = analyzeEntropy(content, file);
269
- const converted = entropyFindingsToFindings(entropyFindings, file, content);
270
- allFindings.push(...converted);
271
- }
272
- catch (entropyError) {
273
- const entropyMessage = entropyError instanceof Error ? entropyError.message : String(entropyError);
274
- logger.warn(`Entropy analysis error for ${file.relativePath}: ${entropyMessage}`);
275
- }
276
- }
277
- // MCP configuration validation if enabled
278
- if (config.mcpValidation && file.component === 'mcp' && file.type === 'json') {
279
- const mcpResult = validateMcpConfigContent(content);
280
- if (mcpResult.valid && mcpResult.assessments.length > 0) {
281
- const mcpFindings = mcpAssessmentsToFindings(mcpResult.assessments, file.path);
282
- // Normalize relative path (feature module uses basename)
283
- for (const f of mcpFindings) {
284
- f.relativePath = file.relativePath;
285
- }
286
- allFindings.push(...mcpFindings);
287
- }
288
- }
289
- // Dependency analysis if enabled
290
- if (config.dependencyAnalysis && basename(file.path).toLowerCase() === 'package.json') {
291
- try {
292
- const depResult = analyzeDependencies(file.path, config.dependencyAudit);
293
- const depFindings = dependencyAssessmentsToFindings(depResult);
294
- for (const f of depFindings) {
295
- f.relativePath = file.relativePath;
296
- }
297
- allFindings.push(...depFindings);
298
- }
299
- catch (depError) {
300
- const depMessage = depError instanceof Error ? depError.message : String(depError);
301
- logger.warn(`Dependency analysis error for ${file.relativePath}: ${depMessage}`);
302
- }
303
- }
304
- // Capability mapping if enabled (best-effort, JSON-only)
305
- if (config.capabilityMapping && file.type === 'json') {
306
- try {
307
- const profile = analyzeCapabilitiesContent(file.path, content);
308
- if (profile) {
309
- const capFindings = capabilityProfileToFindings(profile);
310
- for (const f of capFindings) {
311
- f.relativePath = file.relativePath;
312
- }
313
- allFindings.push(...capFindings);
314
- }
315
- }
316
- catch (capError) {
317
- const capMessage = capError instanceof Error ? capError.message : String(capError);
318
- logger.warn(`Capability mapping error for ${file.relativePath}: ${capMessage}`);
319
- }
320
- }
321
- // LLM-assisted analysis (optional; networked)
322
- if (config.llmAnalysis && llmProvider && !llmRuntime.disabled && llmRuntime.analyzed < config.llm.maxFiles) {
323
- const llmResult = await analyzeWithLlm(llmProvider, config.llm, file, content, allFindings);
324
- if (llmResult.ran) {
325
- llmRuntime.analyzed += 1;
326
- }
327
- allFindings.push(...llmResult.findings);
328
- if (llmResult.error) {
329
- fileErrors.push(`LLM analysis: ${llmResult.error}`);
330
- if (!llmRuntime.disabled && /\bHTTP 429\b/i.test(llmResult.error)) {
331
- llmRuntime.disabled = true;
332
- llmRuntime.disabledReason = 'rate limited (HTTP 429)';
333
- logger.warn('LLM disabled for remainder of scan due to rate limiting (HTTP 429)');
334
- }
335
- }
336
- }
337
- // Semantic analysis if enabled and applicable
338
- if (config.semanticAnalysis && shouldAnalyzeSemantics(file, config)) {
339
- // Monitor memory usage
340
- const memBefore = getMemoryUsage();
341
- if (memBefore.used > 1000) { // More than 1GB used
342
- logger.warn(`High memory usage (${memBefore.used}MB) - skipping semantic analysis for ${file.relativePath}`);
343
- }
344
- else {
345
- try {
346
- logger.debug(`Running semantic analysis on ${file.relativePath}`);
347
- const semanticFindings = await analyzeFileSemantics(file, content, rules);
348
- // Convert SemanticFinding to Finding for compatibility
349
- allFindings.push(...semanticFindings);
350
- const memAfter = getMemoryUsage();
351
- logger.debug(`Semantic analysis memory: ${memAfter.used - memBefore.used}MB delta`);
352
- }
353
- catch (semanticError) {
354
- const semanticMessage = semanticError instanceof Error ? semanticError.message : String(semanticError);
355
- logger.warn(`Semantic analysis error for ${file.relativePath}: ${semanticMessage}`);
356
- }
357
- }
358
- }
359
- // Threat intelligence matching if enabled
360
- if (config.threatIntel && shouldMatchIndicators(file, config)) {
278
+ // Run each analyzer in order via the registry
279
+ const ctx = { file, content, config, rules, existingFindings: allFindings };
280
+ for (const analyzer of analyzers) {
281
+ if (!analyzer.shouldRun(ctx))
282
+ continue;
361
283
  try {
362
- const threatDB = loadThreatDatabase();
363
- logger.debug(`Running threat intelligence matching on ${file.relativePath}`);
364
- const threatFindings = matchIndicators(threatDB, file, content, {
365
- minConfidence: 50,
366
- enablePatternMatching: true,
367
- maxMatchesPerFile: 50
368
- });
369
- allFindings.push(...threatFindings);
370
- logger.debug(`Found ${threatFindings.length} threat intelligence matches`);
284
+ const found = await analyzer.analyze(ctx);
285
+ allFindings.push(...found);
286
+ ctx.existingFindings = allFindings;
371
287
  }
372
- catch (threatError) {
373
- const threatMessage = threatError instanceof Error ? threatError.message : String(threatError);
374
- logger.warn(`Threat intelligence error for ${file.relativePath}: ${threatMessage}`);
288
+ catch (err) {
289
+ logger.warn(`${analyzer.name} error for ${file.relativePath}: ${err instanceof Error ? err.message : String(err)}`);
375
290
  }
376
291
  }
377
- if (fileErrors.length > 0 && ignoreState) {
378
- return { findings: allFindings, errors: fileErrors, ignoreState };
379
- }
380
- if (fileErrors.length > 0) {
381
- return { findings: allFindings, errors: fileErrors };
382
- }
383
292
  if (ignoreState) {
384
293
  return { findings: allFindings, ignoreState };
385
294
  }
@@ -406,6 +315,30 @@ function isLocalEndpoint(urlStr) {
406
315
  return false;
407
316
  }
408
317
  }
318
+ function buildMcpTrustSummary(trustFindings) {
319
+ const summary = { total: 0, high: 0, medium: 0, low: 0, critical: 0, lowestScore: 100 };
320
+ const seen = new Set();
321
+ for (const f of trustFindings) {
322
+ const server = String(f.metadata?.['serverName'] ?? f.file);
323
+ if (seen.has(server))
324
+ continue;
325
+ seen.add(server);
326
+ summary.total++;
327
+ const score = typeof f.metadata?.['trustScore'] === 'number'
328
+ ? f.metadata['trustScore']
329
+ : (f.severity === 'CRITICAL' ? 20 : 45);
330
+ summary.lowestScore = Math.min(summary.lowestScore, score);
331
+ if (score >= 80)
332
+ summary.high++;
333
+ else if (score >= 60)
334
+ summary.medium++;
335
+ else if (score >= 40)
336
+ summary.low++;
337
+ else
338
+ summary.critical++;
339
+ }
340
+ return summary;
341
+ }
409
342
  /**
410
343
  * Main scan function
411
344
  */
@@ -459,6 +392,8 @@ export async function scan(config) {
459
392
  'Review privacy/compliance requirements before enabling this feature.');
460
393
  }
461
394
  }
395
+ // Build analyzer registry (uses llmProvider + llmRuntime by reference)
396
+ const analyzers = buildAnalyzers(llmProvider, llmRuntime);
462
397
  // Discover files with spinner
463
398
  let spinner = null;
464
399
  if (showProgress) {
@@ -520,7 +455,7 @@ export async function scan(config) {
520
455
  lastYield = Date.now();
521
456
  }
522
457
  }
523
- const result = await scanFile(file, config, rulesForScan, llmProvider, llmRuntime);
458
+ const result = await scanFile(file, config, rulesForScan, analyzers);
524
459
  if (result.errors && result.errors.length > 0) {
525
460
  for (const err of result.errors) {
526
461
  errors.push({
@@ -588,6 +523,9 @@ export async function scan(config) {
588
523
  const sortedFindings = sortFindings(filteredFindings);
589
524
  const endTime = new Date();
590
525
  const duration = endTime.getTime() - startTime.getTime();
526
+ // Build MCP trust summary from trust-score findings emitted by mcpValidator
527
+ const mcpTrustFindings = sortedFindings.filter(f => f.metadata?.['issueType'] === 'trust-score');
528
+ const mcpTrustSummary = mcpTrustFindings.length > 0 ? buildMcpTrustSummary(mcpTrustFindings) : undefined;
591
529
  const result = {
592
530
  success: true,
593
531
  startTime,
@@ -604,6 +542,7 @@ export async function scan(config) {
604
542
  summary: calculateSummary(sortedFindings),
605
543
  errors,
606
544
  ignoredFindings,
545
+ ...(mcpTrustSummary !== undefined ? { mcpTrustSummary } : {}),
607
546
  };
608
547
  logger.info(`Scan complete: ${result.summary.total} findings in ${result.analyzedFiles} files (${duration}ms)`);
609
548
  return result;
@@ -0,0 +1,8 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ export declare class CapabilityAnalyzer implements IAnalyzer {
4
+ readonly name = "CapabilityAnalyzer";
5
+ shouldRun(ctx: AnalyzerContext): boolean;
6
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
7
+ }
8
+ //# sourceMappingURL=CapabilityAnalyzer.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { analyzeCapabilitiesContent, capabilityProfileToFindings } from '../../features/capabilityMapping.js';
2
+ export class CapabilityAnalyzer {
3
+ name = 'CapabilityAnalyzer';
4
+ shouldRun(ctx) {
5
+ return ctx.config.capabilityMapping && ctx.file.type === 'json';
6
+ }
7
+ async analyze(ctx) {
8
+ const profile = analyzeCapabilitiesContent(ctx.file.path, ctx.content);
9
+ if (!profile) {
10
+ return [];
11
+ }
12
+ const capFindings = capabilityProfileToFindings(profile);
13
+ for (const f of capFindings) {
14
+ f.relativePath = ctx.file.relativePath;
15
+ }
16
+ return capFindings;
17
+ }
18
+ }
19
+ //# sourceMappingURL=CapabilityAnalyzer.js.map
@@ -0,0 +1,8 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ export declare class DependencyAnalyzer implements IAnalyzer {
4
+ readonly name = "DependencyAnalyzer";
5
+ shouldRun(ctx: AnalyzerContext): boolean;
6
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
7
+ }
8
+ //# sourceMappingURL=DependencyAnalyzer.d.ts.map
@@ -0,0 +1,18 @@
1
+ import { basename } from 'node:path';
2
+ import { analyzeDependencies, dependencyAssessmentsToFindings } from '../../features/dependencyRisk.js';
3
+ export class DependencyAnalyzer {
4
+ name = 'DependencyAnalyzer';
5
+ shouldRun(ctx) {
6
+ return (ctx.config.dependencyAnalysis &&
7
+ basename(ctx.file.path).toLowerCase() === 'package.json');
8
+ }
9
+ async analyze(ctx) {
10
+ const depResult = analyzeDependencies(ctx.file.path, ctx.config.dependencyAudit);
11
+ const depFindings = dependencyAssessmentsToFindings(depResult);
12
+ for (const f of depFindings) {
13
+ f.relativePath = ctx.file.relativePath;
14
+ }
15
+ return depFindings;
16
+ }
17
+ }
18
+ //# sourceMappingURL=DependencyAnalyzer.js.map
@@ -0,0 +1,8 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ export declare class EntropyAnalyzer implements IAnalyzer {
4
+ readonly name = "EntropyAnalyzer";
5
+ shouldRun(ctx: AnalyzerContext): boolean;
6
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
7
+ }
8
+ //# sourceMappingURL=EntropyAnalyzer.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { analyzeEntropy, entropyFindingsToFindings } from '../../features/entropyAnalysis.js';
2
+ export class EntropyAnalyzer {
3
+ name = 'EntropyAnalyzer';
4
+ shouldRun(ctx) {
5
+ return ctx.config.entropyAnalysis;
6
+ }
7
+ async analyze(ctx) {
8
+ const entropyFindings = analyzeEntropy(ctx.content, ctx.file);
9
+ return entropyFindingsToFindings(entropyFindings, ctx.file, ctx.content);
10
+ }
11
+ }
12
+ //# sourceMappingURL=EntropyAnalyzer.js.map
@@ -0,0 +1,17 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ import { type LlmProvider } from '../../features/llmAnalysis.js';
4
+ export interface LlmRuntime {
5
+ analyzed: number;
6
+ disabled: boolean;
7
+ disabledReason?: string;
8
+ }
9
+ export declare class LlmAnalyzer implements IAnalyzer {
10
+ private readonly llmProvider;
11
+ private readonly llmRuntime;
12
+ readonly name = "LlmAnalyzer";
13
+ constructor(llmProvider: LlmProvider | null, llmRuntime: LlmRuntime);
14
+ shouldRun(ctx: AnalyzerContext): boolean;
15
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
16
+ }
17
+ //# sourceMappingURL=LlmAnalyzer.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { analyzeWithLlm } from '../../features/llmAnalysis.js';
2
+ import logger from '../../utils/logger.js';
3
+ export class LlmAnalyzer {
4
+ llmProvider;
5
+ llmRuntime;
6
+ name = 'LlmAnalyzer';
7
+ constructor(llmProvider, llmRuntime) {
8
+ this.llmProvider = llmProvider;
9
+ this.llmRuntime = llmRuntime;
10
+ }
11
+ shouldRun(ctx) {
12
+ return (ctx.config.llmAnalysis &&
13
+ this.llmProvider !== null &&
14
+ !this.llmRuntime.disabled &&
15
+ this.llmRuntime.analyzed < ctx.config.llm.maxFiles);
16
+ }
17
+ async analyze(ctx) {
18
+ // shouldRun already guards llmProvider non-null, but TypeScript needs the cast
19
+ const provider = this.llmProvider;
20
+ const llmResult = await analyzeWithLlm(provider, ctx.config.llm, ctx.file, ctx.content, ctx.existingFindings);
21
+ if (llmResult.ran) {
22
+ this.llmRuntime.analyzed += 1;
23
+ }
24
+ if (llmResult.error) {
25
+ if (!this.llmRuntime.disabled && /\bHTTP 429\b/i.test(llmResult.error)) {
26
+ this.llmRuntime.disabled = true;
27
+ this.llmRuntime.disabledReason = 'rate limited (HTTP 429)';
28
+ logger.warn('LLM disabled for remainder of scan due to rate limiting (HTTP 429)');
29
+ }
30
+ // Re-throw so the caller's catch block records it as a file error
31
+ throw new Error(`LLM analysis: ${llmResult.error}`);
32
+ }
33
+ return llmResult.findings;
34
+ }
35
+ }
36
+ //# sourceMappingURL=LlmAnalyzer.js.map
@@ -0,0 +1,8 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ export declare class McpAnalyzer implements IAnalyzer {
4
+ readonly name = "McpAnalyzer";
5
+ shouldRun(ctx: AnalyzerContext): boolean;
6
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
7
+ }
8
+ //# sourceMappingURL=McpAnalyzer.d.ts.map
@@ -0,0 +1,19 @@
1
+ import { validateMcpConfigContent, mcpAssessmentsToFindings } from '../../features/mcpValidator.js';
2
+ export class McpAnalyzer {
3
+ name = 'McpAnalyzer';
4
+ shouldRun(ctx) {
5
+ return ctx.config.mcpValidation && ctx.file.component === 'mcp' && ctx.file.type === 'json';
6
+ }
7
+ async analyze(ctx) {
8
+ const mcpResult = validateMcpConfigContent(ctx.content);
9
+ if (!mcpResult.valid || mcpResult.assessments.length === 0) {
10
+ return [];
11
+ }
12
+ const mcpFindings = mcpAssessmentsToFindings(mcpResult.assessments, ctx.file.path);
13
+ for (const f of mcpFindings) {
14
+ f.relativePath = ctx.file.relativePath;
15
+ }
16
+ return mcpFindings;
17
+ }
18
+ }
19
+ //# sourceMappingURL=McpAnalyzer.js.map
@@ -0,0 +1,8 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ export declare class SemanticAnalyzer implements IAnalyzer {
4
+ readonly name = "SemanticAnalyzer";
5
+ shouldRun(ctx: AnalyzerContext): boolean;
6
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
7
+ }
8
+ //# sourceMappingURL=SemanticAnalyzer.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { analyzeFile as analyzeFileSemantics, shouldAnalyze as shouldAnalyzeSemantics, getMemoryUsage, } from '../../analyzers/AstAnalyzer.js';
2
+ import logger from '../../utils/logger.js';
3
+ export class SemanticAnalyzer {
4
+ name = 'SemanticAnalyzer';
5
+ shouldRun(ctx) {
6
+ return ctx.config.semanticAnalysis && shouldAnalyzeSemantics(ctx.file, ctx.config);
7
+ }
8
+ async analyze(ctx) {
9
+ const memBefore = getMemoryUsage();
10
+ if (memBefore.used > 1000) {
11
+ logger.warn(`High memory usage (${memBefore.used}MB) - skipping semantic analysis for ${ctx.file.relativePath}`);
12
+ return [];
13
+ }
14
+ logger.debug(`Running semantic analysis on ${ctx.file.relativePath}`);
15
+ const semanticFindings = await analyzeFileSemantics(ctx.file, ctx.content, ctx.rules);
16
+ const memAfter = getMemoryUsage();
17
+ logger.debug(`Semantic analysis memory: ${memAfter.used - memBefore.used}MB delta`);
18
+ return semanticFindings;
19
+ }
20
+ }
21
+ //# sourceMappingURL=SemanticAnalyzer.js.map
@@ -0,0 +1,8 @@
1
+ import type { Finding } from '../../types.js';
2
+ import type { IAnalyzer, AnalyzerContext } from '../IAnalyzer.js';
3
+ export declare class ThreatIntelAnalyzer implements IAnalyzer {
4
+ readonly name = "ThreatIntelAnalyzer";
5
+ shouldRun(ctx: AnalyzerContext): boolean;
6
+ analyze(ctx: AnalyzerContext): Promise<Finding[]>;
7
+ }
8
+ //# sourceMappingURL=ThreatIntelAnalyzer.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { loadThreatDatabase } from '../../intelligence/ThreatFeed.js';
2
+ import { matchIndicators, shouldMatchIndicators } from '../../intelligence/IndicatorMatcher.js';
3
+ import logger from '../../utils/logger.js';
4
+ export class ThreatIntelAnalyzer {
5
+ name = 'ThreatIntelAnalyzer';
6
+ shouldRun(ctx) {
7
+ return ctx.config.threatIntel && shouldMatchIndicators(ctx.file, ctx.config);
8
+ }
9
+ async analyze(ctx) {
10
+ const threatDB = loadThreatDatabase();
11
+ logger.debug(`Running threat intelligence matching on ${ctx.file.relativePath}`);
12
+ const threatFindings = matchIndicators(threatDB, ctx.file, ctx.content, {
13
+ minConfidence: 50,
14
+ enablePatternMatching: true,
15
+ maxMatchesPerFile: 50,
16
+ });
17
+ logger.debug(`Found ${threatFindings.length} threat intelligence matches`);
18
+ return threatFindings;
19
+ }
20
+ }
21
+ //# sourceMappingURL=ThreatIntelAnalyzer.js.map
package/dist/types.d.ts CHANGED
@@ -217,6 +217,23 @@ export interface ScanResult {
217
217
  errors: ScanError[];
218
218
  /** Findings that were suppressed via inline ignore directives */
219
219
  ignoredFindings?: number;
220
+ /** Aggregate MCP server trust score summary (present when MCP configs were scanned) */
221
+ mcpTrustSummary?: McpTrustSummary;
222
+ }
223
+ /** Aggregated MCP server trust scores across a scan */
224
+ export interface McpTrustSummary {
225
+ /** Total number of MCP servers evaluated */
226
+ total: number;
227
+ /** Servers with HIGH trust (score 80–100) */
228
+ high: number;
229
+ /** Servers with MEDIUM trust (score 60–79) */
230
+ medium: number;
231
+ /** Servers with LOW trust (score 40–59) */
232
+ low: number;
233
+ /** Servers with CRITICAL trust (score 0–39) */
234
+ critical: number;
235
+ /** Lowest individual trust score seen */
236
+ lowestScore: number;
220
237
  }
221
238
  /** Summary statistics for a scan */
222
239
  export interface ScanSummary {
package/dist/types.js CHANGED
@@ -8,7 +8,7 @@ export const DEFAULT_CONFIG = {
8
8
  configOnly: false,
9
9
  marketplaceMode: 'configs',
10
10
  docDampening: true,
11
- redact: false,
11
+ redact: true,
12
12
  severities: ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'],
13
13
  categories: [
14
14
  'exfiltration',
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Safe regex runtime utilities with bounded runtime and match limits
2
+ * Safe regex runtime utilities with bounded runtime and match limits.
3
3
  *
4
+ * Uses Google RE2 (linear-time engine) when available for categorically
5
+ * safe pattern execution. Falls back to the screened native JS engine.
4
6
  * Prevents ReDoS attacks and runaway regex matching in user-controlled patterns.
5
7
  */
6
8
  export interface BoundedOptions {
@@ -16,19 +18,19 @@ export interface BoundedResult {
16
18
  truncated: boolean;
17
19
  }
18
20
  /**
19
- * Compile a pattern string into a RegExp, rejecting obviously dangerous patterns.
21
+ * Compile a pattern string into a RegExp (or RE2 instance when available).
20
22
  *
21
- * This function screens for common ReDoS patterns and syntax errors before
22
- * compilation, returning null for unsafe inputs.
23
+ * Tries RE2 first it is a linear-time engine that categorically eliminates
24
+ * ReDoS. If RE2 is unavailable or rejects the pattern (e.g. lookaheads), falls
25
+ * back to the static ReDoS screener + native RegExp.
23
26
  *
24
27
  * @param raw The raw pattern string
25
28
  * @param flags Regex flags (default: 'gi')
26
- * @returns Compiled RegExp or null if pattern is unsafe
29
+ * @returns Compiled RegExp/RE2 or null if pattern is unsafe/invalid
27
30
  *
28
31
  * @example
29
32
  * ```typescript
30
33
  * const safe = compileSafePattern('test\\d+'); // OK
31
- * const unsafe = compileSafePattern('(a+)+b'); // null - ReDoS risk
32
34
  * const invalid = compileSafePattern('[unclosed'); // null - syntax error
33
35
  * ```
34
36
  */
@@ -36,59 +38,18 @@ export declare function compileSafePattern(raw: string, flags?: string): RegExp
36
38
  /**
37
39
  * Run a regex against content with bounded runtime and match limits.
38
40
  *
39
- * This function wraps RegExp for each step with timeout and match count protection
40
- * to prevent runaway regex operations from hanging the application.
41
- *
42
- * @param pattern The compiled RegExp to run
43
- * @param content The content to search
44
- * @param options Runtime limits
45
- * @returns Result containing matches and truncation status
46
- *
47
- * @example
48
- * ```typescript
49
- * const pattern = /test\d+/g;
50
- * const { matches, truncated } = runBounded(pattern, content, { maxMs: 500 });
51
- * if (truncated) {
52
- * console.warn('Regex operation was truncated');
53
- * }
54
- * ```
41
+ * When RE2 is active the time budget is largely redundant (RE2 is linear),
42
+ * but the match-count ceiling still prevents unbounded result arrays.
55
43
  */
56
44
  export declare function runBounded(pattern: RegExp, content: string, options?: BoundedOptions): BoundedResult;
57
45
  /**
58
46
  * Safe pattern matching that combines compilation and bounded runtime.
59
- *
60
- * This is a convenience wrapper that safely compiles a pattern and runs
61
- * it with bounds, handling both compilation failures and runtime limits.
62
- *
63
- * @param rawPattern The raw pattern string
64
- * @param content The content to search
65
- * @param flags Regex flags (default: 'gi')
66
- * @param options Runtime limits
67
- * @returns Match result or null if pattern is unsafe
68
- *
69
- * @example
70
- * ```typescript
71
- * const result = safeMatch('test\\d+', content);
72
- * if (result === null) {
73
- * console.warn('Unsafe or invalid pattern');
74
- * } else if (result.truncated) {
75
- * console.warn('Pattern operation was bounded');
76
- * } else {
77
- * console.log(`Found ${result.matches.length} matches`);
78
- * }
79
- * ```
80
47
  */
81
48
  export declare function safeMatch(rawPattern: string, content: string, flags?: string, options?: BoundedOptions): BoundedResult | null;
82
49
  /**
83
50
  * Test if a pattern matches content safely, returning boolean result.
84
- *
85
- * This is equivalent to RegExp.test() but with safety checks and bounds.
86
- * Returns false for unsafe patterns or bounded operations.
87
- *
88
- * @param rawPattern The raw pattern string
89
- * @param content The content to test
90
- * @param flags Regex flags (default: 'i')
91
- * @returns True if pattern matches safely, false otherwise
92
51
  */
93
52
  export declare function safeTest(rawPattern: string, content: string, flags?: string): boolean;
53
+ /** Returns true when RE2 is active (linear-time engine). */
54
+ export declare function isRE2Active(): boolean;
94
55
  //# sourceMappingURL=safeRegex.d.ts.map