ferret-scan 2.1.2 → 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 (181) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +109 -13
  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/analyzers/AstAnalyzer.d.ts +5 -1
  126. package/dist/analyzers/AstAnalyzer.js +25 -4
  127. package/dist/features/customRules.js +22 -29
  128. package/dist/features/ignoreComments.js +5 -5
  129. package/dist/features/mcpTrustScore.d.ts +17 -0
  130. package/dist/features/mcpTrustScore.js +74 -0
  131. package/dist/features/mcpValidator.d.ts +2 -0
  132. package/dist/features/mcpValidator.js +13 -0
  133. package/dist/features/policyEnforcement.d.ts +22 -22
  134. package/dist/features/policyEnforcement.js +3 -2
  135. package/dist/intelligence/ThreatFeed.js +207 -62
  136. package/dist/remediation/Fixer.js +56 -30
  137. package/dist/remediation/Quarantine.js +79 -11
  138. package/dist/reporters/ConsoleReporter.js +10 -0
  139. package/dist/reporters/HtmlReporter.js +5 -0
  140. package/dist/reporters/SarifReporter.d.ts +1 -0
  141. package/dist/reporters/SarifReporter.js +1 -0
  142. package/dist/rules/ai-specific.js +8 -8
  143. package/dist/rules/backdoors.js +12 -12
  144. package/dist/rules/correlationRules.js +6 -6
  145. package/dist/rules/index.d.ts +1 -0
  146. package/dist/rules/index.js +10 -1
  147. package/dist/rules/injection.js +8 -8
  148. package/dist/rules/patterns/common.d.ts +34 -0
  149. package/dist/rules/patterns/common.js +48 -0
  150. package/dist/scanner/IAnalyzer.d.ts +19 -0
  151. package/dist/scanner/IAnalyzer.js +5 -0
  152. package/dist/scanner/PatternMatcher.js +19 -2
  153. package/dist/scanner/Scanner.js +64 -125
  154. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  155. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  156. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  157. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  158. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  159. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  160. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  161. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  162. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  163. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  164. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  165. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  166. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  167. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  168. package/dist/types.d.ts +23 -0
  169. package/dist/types.js +1 -1
  170. package/dist/utils/baseline.d.ts +15 -2
  171. package/dist/utils/baseline.js +50 -19
  172. package/dist/utils/contentCache.d.ts +39 -0
  173. package/dist/utils/contentCache.js +77 -0
  174. package/dist/utils/glob.d.ts +50 -0
  175. package/dist/utils/glob.js +84 -0
  176. package/dist/utils/pathSecurity.js +1 -0
  177. package/dist/utils/safeRegex.d.ts +55 -0
  178. package/dist/utils/safeRegex.js +130 -0
  179. package/dist/utils/schemas.d.ts +70 -64
  180. package/dist/utils/schemas.js +13 -0
  181. package/package.json +34 -19
@@ -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 {
@@ -304,6 +321,12 @@ export interface ScannerConfig {
304
321
  verbose: boolean;
305
322
  /** CI mode (simplified output) */
306
323
  ci: boolean;
324
+ /** Maximum wall-clock ms for semantic AST analysis of a single code block (default: 2000) */
325
+ maxSemanticAnalysisMs?: number;
326
+ /** Maximum AST node count before aborting semantic analysis of a single code block (default: 50000) */
327
+ maxAstNodes?: number;
328
+ /** Per-code-block deadline in ms within the file-scoped budget (default: 500) */
329
+ maxBlockMs?: number;
307
330
  }
308
331
  /** Supported output formats */
309
332
  export type OutputFormat = 'console' | 'json' | 'sarif' | 'html' | 'csv' | 'atlas';
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',
@@ -14,21 +14,34 @@ export interface BaselineFinding {
14
14
  reason?: string;
15
15
  expiresDate?: string;
16
16
  }
17
+ export interface BaselineIntegrity {
18
+ algorithm: 'sha256';
19
+ hash: string;
20
+ }
17
21
  export interface Baseline {
18
22
  version: string;
19
23
  createdDate: string;
20
24
  lastUpdated: string;
21
25
  description?: string;
22
26
  findings: BaselineFinding[];
27
+ integrity?: BaselineIntegrity;
23
28
  }
29
+ /**
30
+ * Compute integrity hash of a baseline (excluding the integrity field itself)
31
+ */
32
+ export declare function computeBaselineIntegrity(baseline: Omit<Baseline, 'integrity'>): BaselineIntegrity;
33
+ /**
34
+ * Verify that a loaded baseline has not been tampered with
35
+ */
36
+ export declare function verifyBaselineIntegrity(baseline: Baseline): boolean;
24
37
  /**
25
38
  * Load baseline from file
26
39
  */
27
- export declare function loadBaseline(baselinePath: string): Baseline | null;
40
+ export declare function loadBaseline(baselinePath: string): Promise<Baseline | null>;
28
41
  /**
29
42
  * Save baseline to file
30
43
  */
31
- export declare function saveBaseline(baseline: Baseline, baselinePath: string): void;
44
+ export declare function saveBaseline(baseline: Baseline, baselinePath: string): Promise<void>;
32
45
  /**
33
46
  * Create a new baseline from scan results
34
47
  */