ferret-scan 2.2.0 → 2.4.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 +17 -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 +25 -19
package/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.4.0] - 2026-04-27
11
+
12
+ ### Changed
13
+ - **Dropped Node 18 support**: `engines.node` bumped from `>=18.0.0` to `>=20.0.0`. Node 18 reached end-of-life April 2025 and the bundled `re2` native module no longer builds on it (CI Test (18) was failing on main). CI matrix updated to `['20', '22']`; ancillary workflows (`ferret.yml`) bumped from Node 18 → 20.
14
+
15
+ ### Security
16
+ - **`redact: true` by default**: `DEFAULT_CONFIG.redact` flipped from `false` to `true` — secrets found during a scan are now redacted in all output formats (console, CI logs, SARIF, HTML, CSV) without requiring any opt-in
17
+ - **ReDoS prevention enforced in custom rules**: `customRules.ts` now compiles all user-supplied patterns via `compileSafePattern` (previously used raw `new RegExp()`), closing the path by which a malicious `.ferret/rules.yml` could hang a CI build; validation step likewise uses `compileSafePattern` to reject unsafe patterns before load
18
+ - **Dependency vulnerabilities patched**: `npm audit fix` resolves all 7 vulnerabilities (1 critical Handlebars JS injection, 3 high ReDoS in minimatch/picomatch/flatted, 3 moderate)
19
+
20
+ ### Changed
21
+ - **Windows platform support**: Removed `"os": ["!win32"]` exclusion; `package.json` now lists `linux`, `darwin`, and `win32`. Platform guards were already in place in `Quarantine.ts` and `gitHooks.ts`
22
+
23
+ ### Fixed
24
+ - **SECURITY.md accuracy**: Supported version table updated to `2.x` (was incorrectly showing `1.x`); custom rules threat mitigation description now accurately describes the `compileSafePattern` enforcement path
25
+ - **Dockerfile version label**: Updated `org.opencontainers.image.version` from `2.1.0` to `2.2.0` for consistency
26
+
10
27
  ### Planned Features
11
28
  - Complete LSP server implementation
12
29
  - Complete IntelliJ plugin implementation
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢻⣿⣟⣿⣿⠿⣿⡿⠟⠁
20
20
  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⠯⠝⠋⠀⠀⠀⠀
21
21
  </pre>
22
- <strong>Security Scanner for AI CLI Configurations</strong>
22
+ <strong>Static Security Scanner for AI CLI and MCP Configurations</strong>
23
23
  </p>
24
24
 
25
25
  <p align="center">
@@ -42,9 +42,9 @@
42
42
 
43
43
  ---
44
44
 
45
- **Ferret** is a security scanner purpose-built for AI assistant configurations. It detects prompt injections, credential leaks, jailbreak attempts, and malicious patterns in your AI CLI setup before they become problems.
45
+ **Ferret** is a static security scanner purpose-built for AI assistant configurations. It detects prompt injections, credential leaks, jailbreak attempts, and malicious patterns in your AI CLI and MCP server configs before they become problems.
46
46
 
47
- Threat intelligence uses a local indicator database by default (no external feeds unless you add indicators).
47
+ Scanning is **local and offline by default** — no data leaves your machine. Threat intelligence uses a local indicator database (no external feeds unless you opt in).
48
48
 
49
49
  ```
50
50
  $ ferret scan .
@@ -106,21 +106,23 @@ Ferret understands AI CLI structures and catches **AI-specific threats** that ge
106
106
 
107
107
  ## Advanced Features
108
108
 
109
- **IDE Integration**
110
- - **VS Code Extension**: Real-time security scanning with inline diagnostics and quick fixes
111
-
112
- **Analysis Engines**
109
+ **Analysis Engines** (all implemented, local/offline)
113
110
  - **MITRE ATLAS mapping**: Every finding mapped to ATLAS adversary techniques
114
- - **LLM-assisted analysis**: Optional AI-powered threat detection (OpenAI-compatible APIs)
111
+ - **LLM-assisted analysis**: Optional AI-powered threat detection via OpenAI-compatible APIs (opt-in, networked)
115
112
  - **Semantic analysis**: TypeScript AST-based code analysis
116
113
  - **Cross-file correlation**: Detect multi-file attack chains
117
114
  - **Entropy analysis**: Secret detection via Shannon entropy
118
115
  - **Threat intelligence**: Local indicator database matching
119
116
 
117
+ **IDE Integration**
118
+ - **VS Code Extension**: Real-time security scanning with inline diagnostics and quick fixes (build from source)
119
+
120
120
  **Planned Features**
121
121
  - Language Server Protocol (LSP) for universal IDE support
122
122
  - IntelliJ plugin
123
- - Runtime behavior monitoring
123
+ - MCP server trust scoring and provenance verification
124
+ - SBOM/AIBOM generation for AI configurations
125
+ - Runtime behavior monitoring and anomaly detection (currently static analysis only)
124
126
  - Compliance framework assessments (SOC2, ISO 27001, GDPR)
125
127
  - Community rule sharing platform
126
128
 
@@ -850,14 +852,16 @@ ferret scan . --thorough --format atlas -o atlas-layer.json
850
852
 
851
853
  ## Planned Features
852
854
 
855
+ - MCP server trust scoring and package provenance verification
856
+ - SBOM/AIBOM generation for AI configurations
853
857
  - Language Server Protocol (LSP) for Neovim, Emacs, Sublime Text
854
858
  - IntelliJ plugin for JetBrains IDEs
855
- - Runtime behavior monitoring and anomaly detection
859
+ - Runtime behavior monitoring and anomaly detection (tool is currently static analysis only)
856
860
  - Compliance framework assessments (SOC2, ISO 27001, GDPR)
857
861
  - Community rule sharing platform
858
862
  - CI/CD plugins for Jenkins, Azure DevOps
859
863
  - REST API for third-party integrations
860
- - Threat intel updates from external sources
864
+ - Threat intel feeds from external sources (currently local DB only)
861
865
  - More LLM providers and local-first presets
862
866
 
863
867
  ## IDE Integration
package/bin/ferret.js CHANGED
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import { Command } from 'commander';
8
- import { readFileSync } from 'node:fs';
9
- import { resolve, dirname } from 'node:path';
8
+ import { readFileSync, existsSync } from 'node:fs';
9
+ import { resolve, dirname, basename } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { scan, getExitCode } from '../dist/scanner/Scanner.js';
12
12
  import { loadConfig, getAIConfigPaths } from '../dist/utils/config.js';
@@ -52,6 +52,7 @@ import { installHooks, uninstallHooks, getHookStatus } from '../dist/features/gi
52
52
  import { loadCustomRules, validateCustomRulesFile } from '../dist/features/customRules.js';
53
53
  import { analyzeEntropy, entropyFindingsToFindings } from '../dist/features/entropyAnalysis.js';
54
54
  import { validateMcpConfig, findAndValidateMcpConfigs, mcpAssessmentsToFindings } from '../dist/features/mcpValidator.js';
55
+ import { scoreMcpServer } from '../dist/features/mcpTrustScore.js';
55
56
  import { compareScanResults, formatComparisonReport, saveScanResult, loadScanResult } from '../dist/features/scanDiff.js';
56
57
  import { sendWebhook, detectWebhookType } from '../dist/features/webhooks.js';
57
58
  import { analyzeDependencies, dependencyAssessmentsToFindings, findAndAnalyzeDependencies } from '../dist/features/dependencyRisk.js';
@@ -89,14 +90,14 @@ program
89
90
  .option('-w, --watch', 'Watch mode - rescan on file changes')
90
91
  .option('--ci', 'CI mode - minimal output, suitable for pipelines')
91
92
  .option('-v, --verbose', 'Verbose output with context')
92
- .option('--threat-intel', 'Enable threat intelligence feeds (experimental)')
93
- .option('--semantic-analysis', 'Enable AST-based semantic analysis')
94
- .option('--correlation-analysis', 'Enable cross-file correlation analysis')
95
- .option('--entropy-analysis', 'Enable entropy-based secret detection')
93
+ .option('--threat-intel', '[EXPERIMENTAL] Enable local threat intelligence matching (built-in IoC database; add custom indicators via `ferret intel add`)')
94
+ .option('--semantic-analysis', 'Enable AST-based semantic analysis of code blocks in config files')
95
+ .option('--correlation-analysis', '[EXPERIMENTAL] Enable cross-file correlation analysis (detects multi-file attack chains; higher false-positive rate on large repos)')
96
+ .option('--entropy-analysis', 'Enable entropy-based secret detection (Shannon entropy scoring)')
96
97
  .option('--mcp-validation', 'Enable MCP server configuration validation')
97
98
  .option('--dependency-analysis', 'Enable dependency risk analysis (package.json)')
98
- .option('--dependency-audit', 'Run npm audit as part of dependency analysis (slow, may require network)')
99
- .option('--capability-mapping', 'Enable AI agent capability mapping')
99
+ .option('--dependency-audit', 'Run npm audit as part of dependency analysis (slow, requires network)')
100
+ .option('--capability-mapping', '[EXPERIMENTAL] Enable AI agent capability mapping (heuristic-based; expect false positives on complex configs)')
100
101
  .option('--config-only', 'Restrict scanning to high-signal AI config files (reduces noise)')
101
102
  .option('--marketplace <mode>', 'Marketplace scan mode: off, configs, all')
102
103
  .option('--no-doc-dampening', 'Disable documentation severity dampening (reduces false positives in docs)')
@@ -238,6 +239,17 @@ program
238
239
  // Apply auto-fix if enabled
239
240
  const shouldAutoFix = options.autoFix || options.autoRemediation;
240
241
 
242
+ // Warn when experimental features are enabled
243
+ const experimentalEnabled = [
244
+ options.threatIntel && 'threat-intel',
245
+ options.correlationAnalysis && 'correlation-analysis',
246
+ options.capabilityMapping && 'capability-mapping',
247
+ ].filter(Boolean);
248
+ if (experimentalEnabled.length > 0 && !options.ci) {
249
+ const names = experimentalEnabled.join(', ');
250
+ console.warn(`\n⚠ EXPERIMENTAL: ${names} — findings from these analyzers are heuristic-based and may have a higher false-positive rate. Review results carefully.\n`);
251
+ }
252
+
241
253
  // If no paths specified and no AI CLI configs found, show helpful message
242
254
  if (config.paths.length === 0) {
243
255
  console.error('No AI CLI configuration directories found.');
@@ -1120,6 +1132,90 @@ mcpCmd
1120
1132
  }
1121
1133
  });
1122
1134
 
1135
+ mcpCmd
1136
+ .command('audit')
1137
+ .description('Score every MCP server in a config for trust and security posture')
1138
+ .argument('[path]', 'Path to .mcp.json or directory to search (default: current directory)')
1139
+ .option('-f, --format <format>', 'Output format: table, json', 'table')
1140
+ .option('--fail-on <level>', 'Exit non-zero when any server is at or below this trust level: critical, low, medium', 'critical')
1141
+ .action((path, options) => {
1142
+ try {
1143
+ const targetPath = path ? resolve(path) : process.cwd();
1144
+
1145
+ // Locate .mcp.json files
1146
+ const { configs } = findAndValidateMcpConfigs(targetPath);
1147
+ const mcpFiles = configs.map(c => c.path);
1148
+
1149
+ // Also try direct file if it looks like an mcp config
1150
+ if (mcpFiles.length === 0 && existsSync(targetPath) && targetPath.endsWith('.json')) {
1151
+ mcpFiles.push(targetPath);
1152
+ }
1153
+
1154
+ if (mcpFiles.length === 0) {
1155
+ console.log('No MCP configuration files found');
1156
+ process.exit(0);
1157
+ }
1158
+
1159
+ const trustLevelOrder = { CRITICAL: 0, LOW: 1, MEDIUM: 2, HIGH: 3 };
1160
+ const failOrder = { critical: 0, low: 1, medium: 2, high: 3 };
1161
+ const failThreshold = failOrder[options.failOn] ?? 0;
1162
+
1163
+ const allResults = [];
1164
+ let worstTrust = 'HIGH';
1165
+
1166
+ for (const mcpFile of mcpFiles) {
1167
+ let parsed;
1168
+ try {
1169
+ parsed = JSON.parse(readFileSync(mcpFile, 'utf-8'));
1170
+ } catch {
1171
+ console.error(` Failed to parse ${mcpFile}`);
1172
+ continue;
1173
+ }
1174
+
1175
+ const servers = parsed.mcpServers ?? parsed.servers ?? {};
1176
+ for (const [name, cfg] of Object.entries(servers)) {
1177
+ const trust = scoreMcpServer({ ...cfg, name });
1178
+ allResults.push({ file: basename(mcpFile), name, ...trust });
1179
+ if (trustLevelOrder[trust.trustLevel] < trustLevelOrder[worstTrust]) {
1180
+ worstTrust = trust.trustLevel;
1181
+ }
1182
+ }
1183
+ }
1184
+
1185
+ if (options.format === 'json') {
1186
+ console.log(JSON.stringify({ servers: allResults, worstTrust }, null, 2));
1187
+ } else {
1188
+ const levelIcon = { HIGH: '✅', MEDIUM: '⚠️ ', LOW: '🟠', CRITICAL: '🔴' };
1189
+ console.log('\n MCP SERVER TRUST AUDIT\n');
1190
+ console.log(` ${'Server'.padEnd(28)} ${'Score'.padEnd(7)} ${'Trust'.padEnd(10)} Flags`);
1191
+ console.log(' ' + '─'.repeat(80));
1192
+ for (const r of allResults) {
1193
+ const icon = levelIcon[r.trustLevel] ?? '?';
1194
+ const flags = r.flags.length > 0 ? r.flags[0].slice(0, 48) + (r.flags[0].length > 48 ? '…' : '') : '';
1195
+ console.log(` ${icon} ${r.name.padEnd(26)} ${String(r.score).padEnd(7)} ${r.trustLevel.padEnd(10)} ${flags}`);
1196
+ if (r.flags.length > 1) {
1197
+ for (const f of r.flags.slice(1)) {
1198
+ console.log(` ${''.padEnd(26)} ${''.padEnd(7)} ${''.padEnd(10)} ${f.slice(0, 48)}`);
1199
+ }
1200
+ }
1201
+ }
1202
+ console.log('');
1203
+ console.log(` ${allResults.length} server(s) audited | Worst trust level: ${worstTrust}`);
1204
+ console.log('');
1205
+ }
1206
+
1207
+ const exitLevel = failOrder[worstTrust.toLowerCase()] ?? 3;
1208
+ if (exitLevel <= failThreshold) {
1209
+ process.exit(1);
1210
+ }
1211
+ process.exit(0);
1212
+
1213
+ } catch (error) {
1214
+ console.error('Error auditing MCP configs:', error.message);
1215
+ process.exit(1);
1216
+ }
1217
+ });
1218
+
1123
1219
  // Dependency analysis commands
1124
1220
  const depsCmd = program
1125
1221
  .command('deps')
@@ -0,0 +1,6 @@
1
+ /**
2
+ * AgentMonitor Tests
3
+ * Tests for the AgentMonitor class in src/monitoring/AgentMonitor.ts
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=AgentMonitor.test.d.ts.map
@@ -0,0 +1,235 @@
1
+ /**
2
+ * AgentMonitor Tests
3
+ * Tests for the AgentMonitor class in src/monitoring/AgentMonitor.ts
4
+ */
5
+ import { AgentMonitor, agentMonitor } from '../monitoring/AgentMonitor.js';
6
+ function makeConfig(overrides = {}) {
7
+ return {
8
+ enabled: true,
9
+ watchPaths: [],
10
+ trackNetwork: false,
11
+ trackFileSystem: false,
12
+ trackResources: false,
13
+ anomalyDetection: true,
14
+ ...overrides,
15
+ };
16
+ }
17
+ describe('AgentMonitor', () => {
18
+ let monitor;
19
+ beforeEach(() => {
20
+ monitor = new AgentMonitor();
21
+ });
22
+ afterEach(async () => {
23
+ await monitor.stopMonitoring();
24
+ });
25
+ // -------------------------------------------------------------------------
26
+ // startMonitoring / stopMonitoring
27
+ // -------------------------------------------------------------------------
28
+ describe('startMonitoring', () => {
29
+ it('starts monitoring without error', async () => {
30
+ await expect(monitor.startMonitoring(makeConfig())).resolves.toBeUndefined();
31
+ });
32
+ it('is idempotent – calling twice does not throw', async () => {
33
+ await monitor.startMonitoring(makeConfig());
34
+ await expect(monitor.startMonitoring(makeConfig())).resolves.toBeUndefined();
35
+ });
36
+ it('starts with trackResources=true', async () => {
37
+ await expect(monitor.startMonitoring(makeConfig({ trackResources: true }))).resolves.toBeUndefined();
38
+ });
39
+ it('starts with trackNetwork=true', async () => {
40
+ await expect(monitor.startMonitoring(makeConfig({ trackNetwork: true }))).resolves.toBeUndefined();
41
+ });
42
+ it('starts with trackFileSystem=true and watchPaths', async () => {
43
+ await expect(monitor.startMonitoring(makeConfig({ trackFileSystem: true, watchPaths: ['/tmp'] }))).resolves.toBeUndefined();
44
+ });
45
+ });
46
+ describe('stopMonitoring', () => {
47
+ it('stops without error even when not started', async () => {
48
+ await expect(monitor.stopMonitoring()).resolves.toBeUndefined();
49
+ });
50
+ it('clears execution history on stop', async () => {
51
+ await monitor.startMonitoring(makeConfig());
52
+ monitor.trackExecution({ command: 'test-cmd', agentType: 'test' });
53
+ expect(monitor.getExecutionHistory()).toHaveLength(1);
54
+ await monitor.stopMonitoring();
55
+ expect(monitor.getExecutionHistory()).toHaveLength(0);
56
+ });
57
+ });
58
+ // -------------------------------------------------------------------------
59
+ // trackExecution
60
+ // -------------------------------------------------------------------------
61
+ describe('trackExecution', () => {
62
+ it('returns an execution id string', () => {
63
+ const id = monitor.trackExecution({ command: 'ls', agentType: 'test' });
64
+ expect(typeof id).toBe('string');
65
+ expect(id).toMatch(/^exec_/);
66
+ });
67
+ it('stores execution in history', () => {
68
+ monitor.trackExecution({ command: 'ls', agentType: 'test' });
69
+ expect(monitor.getExecutionHistory()).toHaveLength(1);
70
+ });
71
+ it('fills in defaults for missing fields', () => {
72
+ const id = monitor.trackExecution({});
73
+ const history = monitor.getExecutionHistory();
74
+ const exec = history.find(e => e.id === id);
75
+ expect(exec).toBeDefined();
76
+ expect(exec?.agentType).toBe('unknown');
77
+ expect(exec?.command).toBe('');
78
+ expect(exec?.args).toEqual([]);
79
+ expect(exec?.networkActivity).toEqual([]);
80
+ expect(exec?.fileSystemActivity).toEqual([]);
81
+ });
82
+ it('emits execution-started event', () => {
83
+ const listener = jest.fn();
84
+ monitor.on('execution-started', listener);
85
+ monitor.trackExecution({ command: 'echo', agentType: 'test' });
86
+ expect(listener).toHaveBeenCalledTimes(1);
87
+ expect(listener.mock.calls[0][0]).toMatchObject({ command: 'echo' });
88
+ });
89
+ it('supports multiple executions', () => {
90
+ monitor.trackExecution({ command: 'cmd1' });
91
+ monitor.trackExecution({ command: 'cmd2' });
92
+ monitor.trackExecution({ command: 'cmd3' });
93
+ expect(monitor.getExecutionHistory()).toHaveLength(3);
94
+ });
95
+ });
96
+ // -------------------------------------------------------------------------
97
+ // completeExecution
98
+ // -------------------------------------------------------------------------
99
+ describe('completeExecution', () => {
100
+ it('does nothing if id is unknown', () => {
101
+ expect(() => monitor.completeExecution('nonexistent-id', 0)).not.toThrow();
102
+ });
103
+ it('sets endTime, exitCode, and duration on known execution', () => {
104
+ const id = monitor.trackExecution({ command: 'sleep', agentType: 'test' });
105
+ monitor.completeExecution(id, 0);
106
+ const exec = monitor.getExecutionHistory().find(e => e.id === id);
107
+ expect(exec?.exitCode).toBe(0);
108
+ expect(exec?.endTime).toBeInstanceOf(Date);
109
+ expect(typeof exec?.duration).toBe('number');
110
+ expect(exec.duration).toBeGreaterThanOrEqual(0);
111
+ });
112
+ it('emits execution-completed event', () => {
113
+ const listener = jest.fn();
114
+ monitor.on('execution-completed', listener);
115
+ const id = monitor.trackExecution({ command: 'test', agentType: 'test' });
116
+ monitor.completeExecution(id, 1);
117
+ expect(listener).toHaveBeenCalledTimes(1);
118
+ expect(listener.mock.calls[0][0]).toMatchObject({ exitCode: 1 });
119
+ });
120
+ });
121
+ // -------------------------------------------------------------------------
122
+ // Baseline & anomaly detection
123
+ // -------------------------------------------------------------------------
124
+ describe('baseline updates', () => {
125
+ it('starts with empty baselines', () => {
126
+ expect(monitor.getBaselines().size).toBe(0);
127
+ });
128
+ it('builds baseline after completing an execution', () => {
129
+ const id = monitor.trackExecution({ command: 'my-cmd', agentType: 'test' });
130
+ monitor.completeExecution(id, 0);
131
+ expect(monitor.getBaselines().has('my-cmd')).toBe(true);
132
+ });
133
+ it('updates baseline across multiple executions', () => {
134
+ for (let i = 0; i < 3; i++) {
135
+ const id = monitor.trackExecution({ command: 'repeat-cmd', agentType: 'test' });
136
+ monitor.completeExecution(id, 0);
137
+ }
138
+ const baseline = monitor.getBaselines().get('repeat-cmd');
139
+ expect(baseline?.executions).toBe(3);
140
+ });
141
+ });
142
+ describe('anomaly detection', () => {
143
+ it('emits anomalies-detected on CPU spike', () => {
144
+ const anomalyListener = jest.fn();
145
+ monitor.on('anomalies-detected', anomalyListener);
146
+ // Establish baseline with normal CPU usage
147
+ const id1 = monitor.trackExecution({
148
+ command: 'spike-cmd',
149
+ agentType: 'test',
150
+ resources: { cpuPercent: 10, memoryMB: 100, diskReadMB: 0, diskWriteMB: 0 },
151
+ });
152
+ monitor.completeExecution(id1, 0);
153
+ // Now simulate a CPU spike (> 2.5x baseline)
154
+ const id2 = monitor.trackExecution({
155
+ command: 'spike-cmd',
156
+ agentType: 'test',
157
+ resources: { cpuPercent: 5000, memoryMB: 100, diskReadMB: 0, diskWriteMB: 0 },
158
+ });
159
+ monitor.completeExecution(id2, 0);
160
+ expect(anomalyListener).toHaveBeenCalled();
161
+ const call = anomalyListener.mock.calls[anomalyListener.mock.calls.length - 1][0];
162
+ expect(call.anomalies.some(a => a.type === 'resource_spike')).toBe(true);
163
+ });
164
+ it('emits anomalies-detected on memory spike', () => {
165
+ const anomalyListener = jest.fn();
166
+ monitor.on('anomalies-detected', anomalyListener);
167
+ const id1 = monitor.trackExecution({
168
+ command: 'mem-cmd',
169
+ agentType: 'test',
170
+ resources: { cpuPercent: 5, memoryMB: 50, diskReadMB: 0, diskWriteMB: 0 },
171
+ });
172
+ monitor.completeExecution(id1, 0);
173
+ // 2x spike
174
+ const id2 = monitor.trackExecution({
175
+ command: 'mem-cmd',
176
+ agentType: 'test',
177
+ resources: { cpuPercent: 5, memoryMB: 10000, diskReadMB: 0, diskWriteMB: 0 },
178
+ });
179
+ monitor.completeExecution(id2, 0);
180
+ expect(anomalyListener).toHaveBeenCalled();
181
+ });
182
+ it('emits anomalies-detected for suspicious file access', () => {
183
+ const anomalyListener = jest.fn();
184
+ monitor.on('anomalies-detected', anomalyListener);
185
+ // Establish baseline first
186
+ const id1 = monitor.trackExecution({ command: 'file-cmd', agentType: 'test' });
187
+ monitor.completeExecution(id1, 0);
188
+ // Execution that accesses sensitive files
189
+ const id2 = monitor.trackExecution({
190
+ command: 'file-cmd',
191
+ agentType: 'test',
192
+ fileSystemActivity: [
193
+ { timestamp: new Date(), operation: 'read', path: '/home/user/.env', bytes: 100 },
194
+ { timestamp: new Date(), operation: 'read', path: '/home/user/.ssh/id_rsa', bytes: 200 },
195
+ ],
196
+ });
197
+ monitor.completeExecution(id2, 0);
198
+ expect(anomalyListener).toHaveBeenCalled();
199
+ const lastCall = anomalyListener.mock.calls[anomalyListener.mock.calls.length - 1][0];
200
+ expect(lastCall.anomalies.some(a => a.type === 'suspicious_files')).toBe(true);
201
+ });
202
+ it('emits anomalies-detected for unusual network activity', () => {
203
+ const anomalyListener = jest.fn();
204
+ monitor.on('anomalies-detected', anomalyListener);
205
+ // Establish baseline with minimal network
206
+ const id1 = monitor.trackExecution({
207
+ command: 'net-cmd',
208
+ agentType: 'test',
209
+ networkActivity: [{ timestamp: new Date(), direction: 'outbound', protocol: 'tcp', host: 'example.com', port: 80, bytes: 100 }],
210
+ });
211
+ monitor.completeExecution(id1, 0);
212
+ // Big spike (> 3x)
213
+ const id2 = monitor.trackExecution({
214
+ command: 'net-cmd',
215
+ agentType: 'test',
216
+ networkActivity: [
217
+ { timestamp: new Date(), direction: 'outbound', protocol: 'tcp', host: 'evil.com', port: 443, bytes: 1_000_000_000 },
218
+ ],
219
+ });
220
+ monitor.completeExecution(id2, 0);
221
+ expect(anomalyListener).toHaveBeenCalled();
222
+ const lastCall = anomalyListener.mock.calls[anomalyListener.mock.calls.length - 1][0];
223
+ expect(lastCall.anomalies.some(a => a.type === 'unusual_network')).toBe(true);
224
+ });
225
+ });
226
+ // -------------------------------------------------------------------------
227
+ // Exported singleton
228
+ // -------------------------------------------------------------------------
229
+ describe('agentMonitor singleton', () => {
230
+ it('is an instance of AgentMonitor', () => {
231
+ expect(agentMonitor).toBeInstanceOf(AgentMonitor);
232
+ });
233
+ });
234
+ });
235
+ //# sourceMappingURL=AgentMonitor.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * AtlasNavigatorReporter Tests
3
+ * Tests for MITRE ATLAS Navigator layer generation.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=AtlasNavigatorReporter.test.d.ts.map