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
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Security
11
+ - **`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
12
+ - **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
13
+ - **Dependency vulnerabilities patched**: `npm audit fix` resolves all 7 vulnerabilities (1 critical Handlebars JS injection, 3 high ReDoS in minimatch/picomatch/flatted, 3 moderate)
14
+
15
+ ### Changed
16
+ - **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`
17
+
18
+ ### Fixed
19
+ - **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
20
+ - **Dockerfile version label**: Updated `org.opencontainers.image.version` from `2.1.0` to `2.2.0` for consistency
21
+
10
22
  ### Planned Features
11
23
  - Complete LSP server implementation
12
24
  - Complete IntelliJ plugin implementation
@@ -16,6 +28,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
28
  - REST API for third-party integrations
17
29
  - SIEM/SOAR integrations
18
30
 
31
+ ## [2.2.0] - 2026-04-23
32
+
33
+ ### Security
34
+ - **Bounded content cache**: Replaced unbounded `Map` with `BoundedContentCache` (256 MB aggregate cap, 10,000 entry limit, 1 MB per-file cap with LRU eviction) to prevent OOM on large repos
35
+ - **Quarantine hardening**: Quarantine directory created with mode `0700` (owner-only) on POSIX; permissions verified after creation with a warning if loose; disk-space pre-checked via `statfsSync` before any quarantine operation
36
+ - **BUILTIN_FIXES startup validation**: All 9 built-in remediation patterns validated by `compileSafePattern` at module load time — a bad pattern fails fast at startup rather than at first use
37
+ - **Hybrid AST deadline**: `analyzeFile` now enforces both a per-code-block cap (default 500 ms, `maxBlockMs`) and a file-scoped total cap (default 2 s, `maxMs`). A single hostile markdown block can no longer starve all subsequent blocks of their analysis budget
38
+ - **ReDoS prevention hardened**: `compileSafePattern` updated to screen alternation-inside-quantified-groups patterns; `globToRegex` escapes all regex metacharacters and anchors patterns; all correlation and AST pattern execution runs through `runBounded`
39
+ - **`statfsSync` bigint safety**: Explicit `Number()` coercion in `hasSufficientDiskSpace` guards against future `{ bigint: true }` call-sites
40
+ - **`ignoreComments` regex fix**: Alternation order corrected (longest-first: `ignore-next-line`, `ignore-line`, `ignore`) so `ferret-ignore-next-line` is no longer mis-parsed as `ferret-ignore`
41
+
42
+ ### Added
43
+ - **JSON schema sync**: `src/schemas/ferret-config.schema.json` now generated from the runtime zod schema via `npm run schema:generate`; CI enforces drift detection with `npm run schema:check`
44
+ - **Coverage thresholds**: Per-module Jest coverage thresholds for `safeRegex`, `glob`, `contentCache`, `Fixer`, `Quarantine`, `AstAnalyzer`, all four reporters, `WatchMode`, and `policyEnforcement` — silent regressions now fail CI
45
+ - **CI benchmark regression detection**: `scripts/bench-compare.mjs` compares benchmark results against the cached main-branch baseline and fails PRs that regress by >20%
46
+
47
+ ### Tests
48
+ - **673 tests** across 39 test suites (was 244 tests)
49
+ - New unit tests: `AstAnalyzer`, `ConsoleReporter`, `HtmlReporter`, `SarifReporter`, `WatchMode`, `contentCache`, `safeRegex`, `glob`, `Fixer`, `Quarantine`, `ignoreComments`, `mcpValidator`, `policyEnforcement`, `cliOptions`
50
+ - New integration tests: `remediation` (scan→fix→rescan, quarantine→restore, dry-run, backup round-trip) and `cli` (subprocess exit-code contract for `--version`, `--help`, scan, SARIF output)
51
+ - HtmlReporter XSS escape verified: `<script>` in finding values renders as `&lt;script&gt;`
52
+ - SarifReporter validates SARIF 2.1.0 shape, severity mapping, rule deduplication, and location encoding
53
+
19
54
  ## [2.1.0] - 2026-02-16
20
55
 
21
56
  ### Added
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.');
@@ -267,7 +279,7 @@ program
267
279
  // Apply baseline filtering if enabled
268
280
  if (!options.ignoreBaseline) {
269
281
  const baselinePath = options.baseline || getDefaultBaselinePath(config.paths);
270
- const baseline = loadBaseline(baselinePath);
282
+ const baseline = await loadBaseline(baselinePath);
271
283
  if (baseline) {
272
284
  console.log(`📋 Applying baseline from: ${baselinePath}`);
273
285
  result = filterAgainstBaseline(result, baseline);
@@ -514,7 +526,7 @@ baselineCmd
514
526
  const baselinePath = options.output || getDefaultBaselinePath(config.paths);
515
527
  const baseline = createBaseline(result, options.description);
516
528
 
517
- saveBaseline(baseline, baselinePath);
529
+ await saveBaseline(baseline, baselinePath);
518
530
  console.log(`✅ Created baseline with ${baseline.findings.length} findings`);
519
531
  console.log(`📋 Baseline saved to: ${baselinePath}`);
520
532
 
@@ -528,10 +540,10 @@ baselineCmd
528
540
  .command('show')
529
541
  .description('Show baseline information')
530
542
  .argument('[file]', 'Baseline file path (defaults to .ferret-baseline.json)')
531
- .action((file) => {
543
+ .action(async (file) => {
532
544
  try {
533
545
  const baselinePath = file || getDefaultBaselinePath([process.cwd()]);
534
- const baseline = loadBaseline(baselinePath);
546
+ const baseline = await loadBaseline(baselinePath);
535
547
 
536
548
  if (!baseline) {
537
549
  console.error(`No baseline found at: ${baselinePath}`);
@@ -578,7 +590,7 @@ baselineCmd
578
590
  .action(async (file, options) => {
579
591
  try {
580
592
  const baselinePath = file || getDefaultBaselinePath([process.cwd()]);
581
- const baseline = loadBaseline(baselinePath);
593
+ const baseline = await loadBaseline(baselinePath);
582
594
 
583
595
  if (!baseline) {
584
596
  console.error(`No baseline found at: ${baselinePath}`);
@@ -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