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.
- package/CHANGELOG.md +35 -0
- package/README.md +15 -11
- package/bin/ferret.js +109 -13
- package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
- package/dist/__tests__/AgentMonitor.test.js +235 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
- package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
- package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
- package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
- package/dist/__tests__/IndicatorMatcher.test.js +245 -0
- package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
- package/dist/__tests__/MarketplaceScanner.test.js +212 -0
- package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
- package/dist/__tests__/RuleGenerator.test.js +207 -0
- package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
- package/dist/__tests__/ThreatFeed.test.js +359 -0
- package/dist/__tests__/WatchMode.test.d.ts +6 -0
- package/dist/__tests__/WatchMode.test.js +104 -0
- package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
- package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerFull.test.js +138 -0
- package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
- package/dist/__tests__/atlas.test.d.ts +6 -0
- package/dist/__tests__/atlas.test.js +319 -0
- package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalog.test.js +200 -0
- package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
- package/dist/__tests__/baseline.test.d.ts +6 -0
- package/dist/__tests__/baseline.test.js +321 -0
- package/dist/__tests__/baselineExtra.test.d.ts +6 -0
- package/dist/__tests__/baselineExtra.test.js +317 -0
- package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
- package/dist/__tests__/capabilityMapping.test.js +49 -0
- package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
- package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
- package/dist/__tests__/complianceExtra.test.d.ts +6 -0
- package/dist/__tests__/complianceExtra.test.js +121 -0
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/configLoader.test.d.ts +6 -0
- package/dist/__tests__/configLoader.test.js +225 -0
- package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
- package/dist/__tests__/configLoaderExtra.test.js +186 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
- package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
- package/dist/__tests__/customRules.extra.test.d.ts +6 -0
- package/dist/__tests__/customRules.extra.test.js +245 -0
- package/dist/__tests__/customRules.test.d.ts +7 -0
- package/dist/__tests__/customRules.test.js +347 -0
- package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
- package/dist/__tests__/dependencyRisk.test.js +248 -0
- package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
- package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
- package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
- package/dist/__tests__/featureExitCodes.test.js +332 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
- package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
- package/dist/__tests__/fixer.extra.test.d.ts +6 -0
- package/dist/__tests__/fixer.extra.test.js +135 -0
- package/dist/__tests__/fixerApply.test.d.ts +6 -0
- package/dist/__tests__/fixerApply.test.js +132 -0
- package/dist/__tests__/gitHooks.test.d.ts +7 -0
- package/dist/__tests__/gitHooks.test.js +188 -0
- package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
- package/dist/__tests__/htmlReporter.extra.test.js +126 -0
- package/dist/__tests__/interactiveTui.test.d.ts +6 -0
- package/dist/__tests__/interactiveTui.test.js +180 -0
- package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
- package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiMore.test.js +194 -0
- package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiSession.test.js +173 -0
- package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysis.test.js +229 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
- package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
- package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
- package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
- package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
- package/dist/__tests__/llmGroqTPM.test.js +89 -0
- package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
- package/dist/__tests__/llmProviderRetry.test.js +172 -0
- package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
- package/dist/__tests__/mcpValidator.extra.test.js +270 -0
- package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
- package/dist/__tests__/patternMatcherExtra.test.js +198 -0
- package/dist/__tests__/patternsCommon.test.d.ts +6 -0
- package/dist/__tests__/patternsCommon.test.js +107 -0
- package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
- package/dist/__tests__/policyEnforcement.test.js +510 -0
- package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
- package/dist/__tests__/quarantineExtra.test.js +214 -0
- package/dist/__tests__/redactionExtra.test.d.ts +6 -0
- package/dist/__tests__/redactionExtra.test.js +228 -0
- package/dist/__tests__/scanDiff.test.d.ts +7 -0
- package/dist/__tests__/scanDiff.test.js +266 -0
- package/dist/__tests__/scanFull.test.d.ts +6 -0
- package/dist/__tests__/scanFull.test.js +158 -0
- package/dist/__tests__/scannerDampening.test.d.ts +6 -0
- package/dist/__tests__/scannerDampening.test.js +160 -0
- package/dist/__tests__/scannerExtra.test.d.ts +6 -0
- package/dist/__tests__/scannerExtra.test.js +194 -0
- package/dist/__tests__/scannerMitre.test.d.ts +5 -0
- package/dist/__tests__/scannerMitre.test.js +141 -0
- package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
- package/dist/__tests__/scannerSSRF.test.js +149 -0
- package/dist/__tests__/schemas.test.d.ts +6 -0
- package/dist/__tests__/schemas.test.js +125 -0
- package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
- package/dist/__tests__/webhooks.extra.test.js +144 -0
- package/dist/__tests__/webhooks.test.d.ts +6 -0
- package/dist/__tests__/webhooks.test.js +154 -0
- package/dist/analyzers/AstAnalyzer.d.ts +5 -1
- package/dist/analyzers/AstAnalyzer.js +25 -4
- package/dist/features/customRules.js +22 -29
- package/dist/features/ignoreComments.js +5 -5
- package/dist/features/mcpTrustScore.d.ts +17 -0
- package/dist/features/mcpTrustScore.js +74 -0
- package/dist/features/mcpValidator.d.ts +2 -0
- package/dist/features/mcpValidator.js +13 -0
- package/dist/features/policyEnforcement.d.ts +22 -22
- package/dist/features/policyEnforcement.js +3 -2
- package/dist/intelligence/ThreatFeed.js +207 -62
- package/dist/remediation/Fixer.js +56 -30
- package/dist/remediation/Quarantine.js +79 -11
- package/dist/reporters/ConsoleReporter.js +10 -0
- package/dist/reporters/HtmlReporter.js +5 -0
- package/dist/reporters/SarifReporter.d.ts +1 -0
- package/dist/reporters/SarifReporter.js +1 -0
- package/dist/rules/ai-specific.js +8 -8
- package/dist/rules/backdoors.js +12 -12
- package/dist/rules/correlationRules.js +6 -6
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +10 -1
- package/dist/rules/injection.js +8 -8
- package/dist/rules/patterns/common.d.ts +34 -0
- package/dist/rules/patterns/common.js +48 -0
- package/dist/scanner/IAnalyzer.d.ts +19 -0
- package/dist/scanner/IAnalyzer.js +5 -0
- package/dist/scanner/PatternMatcher.js +19 -2
- package/dist/scanner/Scanner.js +64 -125
- package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
- package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
- package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
- package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +1 -1
- package/dist/utils/baseline.d.ts +15 -2
- package/dist/utils/baseline.js +50 -19
- package/dist/utils/contentCache.d.ts +39 -0
- package/dist/utils/contentCache.js +77 -0
- package/dist/utils/glob.d.ts +50 -0
- package/dist/utils/glob.js +84 -0
- package/dist/utils/pathSecurity.js +1 -0
- package/dist/utils/safeRegex.d.ts +55 -0
- package/dist/utils/safeRegex.js +130 -0
- package/dist/utils/schemas.d.ts +70 -64
- package/dist/utils/schemas.js +13 -0
- 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 `<script>`
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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,
|
|
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,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
|