ferret-scan 2.0.0 → 2.1.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 CHANGED
@@ -16,6 +16,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
16
16
  - REST API for third-party integrations
17
17
  - SIEM/SOAR integrations
18
18
 
19
+ ## [2.1.0] - 2026-02-16
20
+
21
+ ### Added
22
+ - **NO_COLOR support**: Respects the `NO_COLOR` environment variable per no-color.org standard. Chalk auto-detects terminal capabilities and disables color output when `NO_COLOR` is set
23
+ - **SSRF protection for custom rules**: Remote URLs in `--custom-rules` are now blocked by default. Use the new `--allow-remote-rules` flag to opt in to loading rules from URLs
24
+ - **SIGINT handler**: Graceful shutdown on Ctrl+C during scan with cleanup message and exit code 130
25
+ - **Interactive baseline removal**: `ferret baseline remove` now prompts for confirmation interactively instead of requiring `--yes`
26
+ - **Dockerfile updated**: Multi-stage build with Node.js 20, non-root user, proper signal handling, and minimal image size
27
+ - **npm-shrinkwrap.json**: Deterministic dependency installs for reproducible builds
28
+ - **ESM exports map**: Added `"exports"` field to package.json for proper ESM module resolution
29
+ - **Version command changelog link**: `ferret version` now includes a link to the changelog
30
+ - **Comprehensive test suite**: 244 tests covering rule matching (positive/negative cases for all 9 categories), config loading, reporter output, exit codes, and SARIF validation
31
+
32
+ ### Changed
33
+ - **Chalk replaces raw ANSI codes**: ConsoleReporter now uses chalk consistently instead of raw ANSI escape sequences. This automatically supports `NO_COLOR`, `FORCE_COLOR`, and terminal capability detection
34
+ - **Invalid input warnings**: Unknown `--severity` and `--category` values now produce a warning instead of being silently dropped
35
+ - **typescript moved to devDependencies**: Saves ~60MB on production installs
36
+
37
+ ### Security
38
+ - **SSRF protection**: Custom rules from remote URLs require explicit opt-in via `--allow-remote-rules` to prevent server-side request forgery
39
+
40
+ ### Fixed
41
+ - Missing `allowRemoteRules` field in MarketplaceScanner config
42
+
19
43
  ## [2.0.0] - 2026-02-15
20
44
 
21
45
  ### Added
package/README.md CHANGED
@@ -95,37 +95,34 @@ AI CLI configurations are a **new attack surface**. Traditional security scanner
95
95
 
96
96
  Ferret understands AI CLI structures and catches **AI-specific threats** that generic scanners miss.
97
97
 
98
- ## New: Advanced Features (v2.0)
98
+ ## What's New in v2.1.0
99
99
 
100
- **🔌 IDE Integrations**
100
+ - **NO_COLOR support**: Respects the `NO_COLOR` environment variable per [no-color.org](https://no-color.org)
101
+ - **SSRF protection**: Remote custom rules URLs blocked by default; use `--allow-remote-rules` to opt in
102
+ - **SIGINT handler**: Graceful shutdown on Ctrl+C during scan
103
+ - **Interactive baseline removal**: `ferret baseline remove` prompts for confirmation
104
+ - **244 tests**: Comprehensive test suite covering rules, config, reporters, and exit codes
105
+ - **npm-shrinkwrap.json**: Deterministic dependency installs
106
+
107
+ ## Advanced Features
108
+
109
+ **IDE Integration**
101
110
  - **VS Code Extension**: Real-time security scanning with inline diagnostics and quick fixes
102
- - **Language Server Protocol**: IDE-agnostic security analysis for Neovim, Emacs, and more
103
- - **IntelliJ Plugin**: Enterprise-grade support for Java/Kotlin teams
104
-
105
- **📊 Behavior Analysis**
106
- - **Runtime Monitoring**: Track agent execution patterns and resource usage
107
- - **Anomaly Detection**: Identify unusual behavior and potential security incidents
108
- - **Cross-Agent Communication**: Monitor interactions between AI agents
109
-
110
- **🏪 Marketplace Security**
111
- - **Plugin Scanning**: Analyze Claude Skills, Cursor extensions, and community plugins
112
- - **Permission Analysis**: Detect dangerous capability combinations
113
- - **Risk Assessment**: Automated threat scoring and recommendations
114
-
115
- **🤖 AI-Powered Rules**
116
- - **Automated Rule Generation**: Create security rules from threat intelligence using LLM
117
- - **Community Platform**: Share and import security rules from the community
118
- - **Adaptive Detection**: Rules that evolve with the threat landscape
119
-
120
- **🔒 Sandboxing Integration**
121
- - **Pre-execution Validation**: Security checks before agent code runs
122
- - **Runtime Constraints**: Enforce resource limits and access controls
123
- - **Capability Boundaries**: Verify and restrict agent capabilities
124
-
125
- **✅ Compliance Frameworks**
126
- - **SOC2 Compliance**: Automated control assessment and reporting
127
- - **ISO 27001**: Security standard mapping and evidence collection
128
- - **GDPR**: Privacy impact assessment for AI agents
111
+
112
+ **Analysis Engines**
113
+ - **MITRE ATLAS mapping**: Every finding mapped to ATLAS adversary techniques
114
+ - **LLM-assisted analysis**: Optional AI-powered threat detection (OpenAI-compatible APIs)
115
+ - **Semantic analysis**: TypeScript AST-based code analysis
116
+ - **Cross-file correlation**: Detect multi-file attack chains
117
+ - **Entropy analysis**: Secret detection via Shannon entropy
118
+ - **Threat intelligence**: Local indicator database matching
119
+
120
+ **Planned Features**
121
+ - Language Server Protocol (LSP) for universal IDE support
122
+ - IntelliJ plugin
123
+ - Runtime behavior monitoring
124
+ - Compliance framework assessments (SOC2, ISO 27001, GDPR)
125
+ - Community rule sharing platform
129
126
 
130
127
  ---
131
128
 
@@ -404,6 +401,9 @@ npx -p ferret-scan ferret scan .
404
401
  # Or install locally
405
402
  npm install --save-dev ferret-scan
406
403
  npx ferret scan .
404
+
405
+ # Or run via Docker (no Node.js required)
406
+ docker run --rm -v $(pwd):/workspace:ro ghcr.io/fubak/ferret-scan scan /workspace
407
407
  ```
408
408
 
409
409
  ## Quick Start
@@ -464,13 +464,19 @@ GROQ_API_KEY="..." ferret scan . --thorough \
464
464
  --llm-model llama-3.1-8b-instant \
465
465
  --mitre-atlas-catalog
466
466
 
467
- # Load custom rules (file paths or URLs)
467
+ # Load custom rules (local files)
468
468
  ferret scan . --custom-rules ./.ferret/rules.yml
469
+
470
+ # Load custom rules from remote URLs (requires opt-in)
471
+ ferret scan . --custom-rules https://example.com/rules.yml --allow-remote-rules
472
+
473
+ # Disable color output
474
+ NO_COLOR=1 ferret scan .
469
475
  ```
470
476
 
471
477
  ## What It Detects
472
478
 
473
- Ferret includes **80 enabled rules** (as of `v1.0.10`) across these categories. Run `ferret rules stats` for the latest counts.
479
+ Ferret includes **80+ enabled rules** across these categories. Run `ferret rules stats` for the latest counts.
474
480
 
475
481
  | Category | Rules | What It Finds |
476
482
  |----------|-------|---------------|
@@ -612,59 +618,6 @@ ferret intel search "jailbreak" # Search indicators
612
618
  ferret intel add --type pattern --value "malicious" --severity high
613
619
  ```
614
620
 
615
- ### `ferret marketplace` (NEW)
616
-
617
- Scan AI agent marketplaces and plugins:
618
-
619
- ```bash
620
- ferret marketplace scan claude # Scan Claude Skills marketplace
621
- ferret marketplace scan cursor # Scan Cursor extensions
622
- ferret marketplace analyze <plugin-id> # Analyze specific plugin
623
- ferret marketplace list --risky # Show high-risk plugins
624
- ```
625
-
626
- ### `ferret monitor` (NEW)
627
-
628
- Runtime behavior monitoring:
629
-
630
- ```bash
631
- ferret monitor start # Start monitoring agent behavior
632
- ferret monitor status # Check monitoring status
633
- ferret monitor report # Generate behavior report
634
- ferret monitor stop # Stop monitoring
635
- ```
636
-
637
- ### `ferret sandbox` (NEW)
638
-
639
- Sandbox integration and validation:
640
-
641
- ```bash
642
- ferret sandbox validate <command> # Pre-execution security check
643
- ferret sandbox enforce --config <file> # Apply runtime constraints
644
- ferret sandbox test <agent-config> # Test agent in sandbox
645
- ```
646
-
647
- ### `ferret compliance` (NEW)
648
-
649
- Compliance framework assessment:
650
-
651
- ```bash
652
- ferret compliance assess soc2 # SOC2 compliance assessment
653
- ferret compliance assess iso27001 # ISO 27001 assessment
654
- ferret compliance assess gdpr # GDPR privacy impact assessment
655
- ferret compliance report --format pdf # Generate compliance report
656
- ```
657
-
658
- ### `ferret rules generate` (NEW)
659
-
660
- AI-powered rule generation:
661
-
662
- ```bash
663
- ferret rules generate --from-threat <report.json> # Generate from threat intel
664
- ferret rules generate --community # Browse community rules
665
- ferret rules validate <rule-file> # Validate custom rules
666
- ferret rules publish <rule-file> # Share with community
667
- ```
668
621
 
669
622
  ## CI/CD Integration
670
623
 
@@ -697,10 +650,10 @@ security_scan:
697
650
  stage: test
698
651
  image: node:20
699
652
  script:
700
- - npx -p ferret-scan ferret scan . --ci --format json -o ferret-results.json
653
+ - npx -p ferret-scan ferret scan . --ci --format sarif -o ferret-results.sarif
701
654
  artifacts:
702
655
  reports:
703
- sast: ferret-results.json
656
+ sast: ferret-results.sarif
704
657
  ```
705
658
 
706
659
  ### Pre-commit Hook
@@ -718,6 +671,15 @@ fi
718
671
  echo "✅ Security scan passed"
719
672
  ```
720
673
 
674
+ ## Environment Variables
675
+
676
+ | Variable | Description |
677
+ |----------|-------------|
678
+ | `NO_COLOR` | Disable all color output ([no-color.org](https://no-color.org)) |
679
+ | `FERRET_EXIT_SUCCESS` | Override success exit code (default: 0) |
680
+ | `FERRET_EXIT_FINDINGS` | Override findings exit code (default: 1) |
681
+ | `FERRET_EXIT_ERROR` | Override error exit code (default: 3) |
682
+
721
683
  ## Configuration
722
684
 
723
685
  Ferret will auto-load config from (first found walking up from CWD):
@@ -775,17 +737,26 @@ Optional: keep MITRE ATLAS technique metadata up to date (downloads STIX bundle
775
737
 
776
738
  ## Docker
777
739
 
740
+ No Node.js required. The image runs as a non-root user with minimal dependencies.
741
+
778
742
  ```bash
743
+ # Build the image
744
+ docker build -t ferret-scan .
745
+
779
746
  # Basic scan
780
747
  docker run --rm -v $(pwd):/workspace:ro \
781
- ghcr.io/fubak/ferret-scan scan /workspace
748
+ ferret-scan scan /workspace
782
749
 
783
750
  # With output file
784
751
  docker run --rm \
785
752
  -v $(pwd):/workspace:ro \
786
753
  -v $(pwd)/results:/output:rw \
787
- ghcr.io/fubak/ferret-scan scan /workspace \
754
+ ferret-scan scan /workspace \
788
755
  --format html -o /output/report.html
756
+
757
+ # CI mode
758
+ docker run --rm -v $(pwd):/workspace:ro \
759
+ ferret-scan scan /workspace --ci --fail-on high
789
760
  ```
790
761
 
791
762
  ## Advanced Features
@@ -858,8 +829,11 @@ rules:
858
829
  You can also pass sources explicitly (file paths or URLs):
859
830
 
860
831
  ```bash
832
+ # Local rules files
861
833
  ferret scan . --custom-rules ./.ferret/rules.yml
862
- ferret scan . --custom-rules https://example.com/ferret-rules.yml
834
+
835
+ # Remote rules require --allow-remote-rules (SSRF protection)
836
+ ferret scan . --custom-rules https://example.com/ferret-rules.yml --allow-remote-rules
863
837
  ```
864
838
 
865
839
  ### Thorough Mode
@@ -876,6 +850,13 @@ ferret scan . --thorough --format atlas -o atlas-layer.json
876
850
 
877
851
  ## Planned Features
878
852
 
853
+ - Language Server Protocol (LSP) for Neovim, Emacs, Sublime Text
854
+ - IntelliJ plugin for JetBrains IDEs
855
+ - Runtime behavior monitoring and anomaly detection
856
+ - Compliance framework assessments (SOC2, ISO 27001, GDPR)
857
+ - Community rule sharing platform
858
+ - CI/CD plugins for Jenkins, Azure DevOps
859
+ - REST API for third-party integrations
879
860
  - Threat intel updates from external sources
880
861
  - More LLM providers and local-first presets
881
862
 
@@ -883,7 +864,7 @@ ferret scan . --thorough --format atlas -o atlas-layer.json
883
864
 
884
865
  ### VS Code Extension
885
866
 
886
- Install from VS Code Marketplace or build from source:
867
+ Build from source:
887
868
 
888
869
  ```bash
889
870
  cd extensions/vscode
@@ -909,40 +890,13 @@ npm run compile
909
890
  }
910
891
  ```
911
892
 
912
- ### Language Server Protocol (LSP)
913
-
914
- Universal IDE support through LSP:
915
-
916
- ```bash
917
- cd lsp/server
918
- npm install
919
- npm run build
920
- node dist/server.js --stdio
921
- ```
922
-
923
- **Supported Editors:**
924
- - Neovim (via nvim-lspconfig)
925
- - Emacs (via lsp-mode)
926
- - Sublime Text (via LSP package)
927
- - Atom (via atom-languageclient)
928
-
929
- ### IntelliJ Plugin
930
-
931
- Enterprise-grade support for JetBrains IDEs:
932
-
933
- ```bash
934
- cd plugins/intellij
935
- ./gradlew build
936
- # Install: Settings -> Plugins -> Install from disk
937
- ```
938
-
939
893
  ## Performance
940
894
 
941
895
  | Metric | Value |
942
896
  |--------|-------|
943
897
  | **Speed** | Fast deterministic scanning; optional analyzers (semantic/correlation/deps/LLM) add cost |
944
898
  | **Memory** | Depends on enabled analyzers (semantic analysis uses the TypeScript compiler) |
945
- | **Rules** | 80 enabled rules (as of `v1.0.10`) + optional custom rules |
899
+ | **Rules** | 80+ enabled rules + optional custom rules |
946
900
 
947
901
  ## Documentation
948
902
 
package/bin/ferret.js CHANGED
@@ -122,10 +122,22 @@ program
122
122
  .option('--auto-fix', 'Automatically apply safe fixes after scanning')
123
123
  .option('--config <file>', 'Path to configuration file')
124
124
  .option('--custom-rules <sources>', 'Custom rule sources (comma-separated file paths or URLs)')
125
+ .option('--allow-remote-rules', 'Allow loading custom rules from remote URLs (required for URL sources)')
125
126
  .option('--baseline <file>', 'Path to baseline file for filtering known findings')
126
127
  .option('--ignore-baseline', 'Ignore baseline file and show all findings')
127
128
  .action(async (path, options, command) => {
128
129
  try {
130
+ // SIGINT handler for graceful shutdown
131
+ let aborted = false;
132
+ const sigintHandler = () => {
133
+ if (!aborted) {
134
+ aborted = true;
135
+ console.error('\nScan interrupted. Cleaning up...');
136
+ process.exit(130);
137
+ }
138
+ };
139
+ process.on('SIGINT', sigintHandler);
140
+
129
141
  // Configure logger
130
142
  logger.configure({
131
143
  verbose: options.verbose,
@@ -219,6 +231,7 @@ program
219
231
  llmMinConfidence: options.llmMinConfidence,
220
232
  thorough: options.thorough,
221
233
  autoRemediation: options.autoRemediation,
234
+ allowRemoteRules: options.allowRemoteRules,
222
235
  config: options.config,
223
236
  });
224
237
 
@@ -573,10 +586,16 @@ baselineCmd
573
586
  }
574
587
 
575
588
  if (!options.yes) {
576
- // Simple confirmation (in a real implementation, you'd use a proper prompt library)
577
- console.log(`This will delete the baseline at: ${baselinePath}`);
578
- console.log('Use --yes to confirm');
579
- process.exit(1);
589
+ const { createInterface } = await import('node:readline');
590
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
591
+ const answer = await new Promise((resolve) => {
592
+ rl.question(`Remove baseline at ${baselinePath}? [y/N] `, resolve);
593
+ });
594
+ rl.close();
595
+ if (String(answer).trim().toLowerCase() !== 'y') {
596
+ console.log('Cancelled.');
597
+ process.exit(0);
598
+ }
580
599
  }
581
600
 
582
601
  const { unlinkSync } = await import('node:fs');
@@ -949,6 +968,7 @@ program
949
968
  .action(() => {
950
969
  console.log(`Ferret v${packageJson.version}`);
951
970
  console.log('Security scanner for AI CLI configurations');
971
+ console.log(`Changelog: https://github.com/fubak/ferret-scan/blob/main/CHANGELOG.md`);
952
972
  });
953
973
 
954
974
  // Git hooks commands
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Configuration Loading Tests
3
+ * Tests parseSeverities, parseCategories, loadConfig, and default values
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=config.test.d.ts.map
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Configuration Loading Tests
3
+ * Tests parseSeverities, parseCategories, loadConfig, and default values
4
+ */
5
+ import { loadConfig } from '../utils/config.js';
6
+ import { DEFAULT_CONFIG } from '../types.js';
7
+ // ---------------------------------------------------------------------------
8
+ // Since parseSeverities and parseCategories are not exported directly,
9
+ // we test them indirectly through loadConfig, which calls them internally.
10
+ // We also test loadConfig behavior directly for defaults and overrides.
11
+ // ---------------------------------------------------------------------------
12
+ // Spy on logger.warn to verify warning behavior
13
+ let warnSpy;
14
+ beforeEach(() => {
15
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
16
+ const logger = require('../utils/logger.js').default;
17
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
18
+ warnSpy = jest.spyOn(logger, 'warn').mockImplementation(() => { });
19
+ });
20
+ afterEach(() => {
21
+ warnSpy?.mockRestore();
22
+ });
23
+ // ---------------------------------------------------------------------------
24
+ // parseSeverities (tested through loadConfig)
25
+ // ---------------------------------------------------------------------------
26
+ describe('parseSeverities behavior via loadConfig', () => {
27
+ it('should accept valid severity values', () => {
28
+ const config = loadConfig({ severity: 'HIGH,CRITICAL' });
29
+ expect(config.severities).toEqual(['HIGH', 'CRITICAL']);
30
+ });
31
+ it('should be case-insensitive for severity parsing', () => {
32
+ const config = loadConfig({ severity: 'high,medium' });
33
+ expect(config.severities).toEqual(['HIGH', 'MEDIUM']);
34
+ });
35
+ it('should warn and ignore unknown severity values', () => {
36
+ const config = loadConfig({ severity: 'HIGH,BOGUS,LOW' });
37
+ expect(config.severities).toEqual(['HIGH', 'LOW']);
38
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Ignoring unknown severity "BOGUS"'));
39
+ });
40
+ it('should keep defaults when all severities are invalid', () => {
41
+ const config = loadConfig({ severity: 'INVALID,NOPE' });
42
+ // When parseSeverities returns undefined (no valid values), defaults remain
43
+ expect(config.severities).toEqual(DEFAULT_CONFIG.severities);
44
+ });
45
+ it('should keep defaults when severity option is not provided', () => {
46
+ const config = loadConfig({});
47
+ expect(config.severities).toEqual(DEFAULT_CONFIG.severities);
48
+ });
49
+ });
50
+ // ---------------------------------------------------------------------------
51
+ // parseCategories (tested through loadConfig)
52
+ // ---------------------------------------------------------------------------
53
+ describe('parseCategories behavior via loadConfig', () => {
54
+ it('should accept valid category values', () => {
55
+ const config = loadConfig({ categories: 'injection,backdoors' });
56
+ expect(config.categories).toEqual(['injection', 'backdoors']);
57
+ });
58
+ it('should be case-insensitive for category parsing', () => {
59
+ const config = loadConfig({ categories: 'INJECTION,EXFILTRATION' });
60
+ expect(config.categories).toEqual(['injection', 'exfiltration']);
61
+ });
62
+ it('should warn and ignore unknown category values', () => {
63
+ const config = loadConfig({ categories: 'injection,fakecategory' });
64
+ expect(config.categories).toEqual(['injection']);
65
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Ignoring unknown category "fakecategory"'));
66
+ });
67
+ it('should keep defaults when all categories are invalid', () => {
68
+ const config = loadConfig({ categories: 'invalid,nope' });
69
+ expect(config.categories).toEqual(DEFAULT_CONFIG.categories);
70
+ });
71
+ it('should keep defaults when categories option is not provided', () => {
72
+ const config = loadConfig({});
73
+ expect(config.categories).toEqual(DEFAULT_CONFIG.categories);
74
+ });
75
+ it('should handle all valid categories', () => {
76
+ const all = 'exfiltration,credentials,injection,backdoors,supply-chain,permissions,persistence,obfuscation,ai-specific,advanced-hiding,behavioral';
77
+ const config = loadConfig({ categories: all });
78
+ expect(config.categories).toHaveLength(11);
79
+ });
80
+ });
81
+ // ---------------------------------------------------------------------------
82
+ // loadConfig defaults
83
+ // ---------------------------------------------------------------------------
84
+ describe('loadConfig defaults', () => {
85
+ it('should return sensible defaults with minimal options', () => {
86
+ const config = loadConfig({});
87
+ expect(config.severities).toEqual(DEFAULT_CONFIG.severities);
88
+ expect(config.categories).toEqual(DEFAULT_CONFIG.categories);
89
+ expect(config.failOn).toBe('HIGH');
90
+ expect(config.format).toBe('console');
91
+ expect(config.contextLines).toBe(3);
92
+ expect(config.maxFileSize).toBe(10 * 1024 * 1024);
93
+ expect(config.verbose).toBe(false);
94
+ expect(config.ci).toBe(false);
95
+ expect(config.watch).toBe(false);
96
+ expect(config.configOnly).toBe(false);
97
+ expect(config.docDampening).toBe(true);
98
+ expect(config.redact).toBe(false);
99
+ expect(config.ignoreComments).toBe(true);
100
+ expect(config.mitreAtlas).toBe(true);
101
+ });
102
+ it('should default allowRemoteRules to false', () => {
103
+ const config = loadConfig({});
104
+ expect(config.allowRemoteRules).toBe(false);
105
+ });
106
+ it('should default analysis features to false', () => {
107
+ const config = loadConfig({});
108
+ expect(config.threatIntel).toBe(false);
109
+ expect(config.semanticAnalysis).toBe(false);
110
+ expect(config.correlationAnalysis).toBe(false);
111
+ expect(config.entropyAnalysis).toBe(false);
112
+ expect(config.mcpValidation).toBe(false);
113
+ expect(config.dependencyAnalysis).toBe(false);
114
+ expect(config.dependencyAudit).toBe(false);
115
+ expect(config.capabilityMapping).toBe(false);
116
+ expect(config.llmAnalysis).toBe(false);
117
+ expect(config.autoRemediation).toBe(false);
118
+ });
119
+ it('should default ignore patterns to node_modules and .git', () => {
120
+ const config = loadConfig({});
121
+ expect(config.ignore).toContain('**/node_modules/**');
122
+ expect(config.ignore).toContain('**/.git/**');
123
+ });
124
+ it('should default customRules to an empty array', () => {
125
+ const config = loadConfig({});
126
+ expect(config.customRules).toEqual([]);
127
+ });
128
+ it('should default LLM config with sensible values', () => {
129
+ const config = loadConfig({});
130
+ expect(config.llm.provider).toBe('openai-compatible');
131
+ expect(config.llm.model).toBe('gpt-4o-mini');
132
+ expect(config.llm.temperature).toBe(0);
133
+ expect(config.llm.maxRetries).toBe(2);
134
+ expect(config.llm.onlyIfFindings).toBe(true);
135
+ expect(config.llm.minConfidence).toBe(0.6);
136
+ });
137
+ it('should default MITRE ATLAS catalog to disabled', () => {
138
+ const config = loadConfig({});
139
+ expect(config.mitreAtlasCatalog.enabled).toBe(false);
140
+ expect(config.mitreAtlasCatalog.autoUpdate).toBe(true);
141
+ });
142
+ });
143
+ // ---------------------------------------------------------------------------
144
+ // loadConfig CLI overrides
145
+ // ---------------------------------------------------------------------------
146
+ describe('loadConfig CLI overrides', () => {
147
+ it('should override format from CLI', () => {
148
+ const config = loadConfig({ format: 'json' });
149
+ expect(config.format).toBe('json');
150
+ });
151
+ it('should override failOn from CLI', () => {
152
+ const config = loadConfig({ failOn: 'CRITICAL' });
153
+ expect(config.failOn).toBe('CRITICAL');
154
+ });
155
+ it('should override failOn case-insensitively', () => {
156
+ const config = loadConfig({ failOn: 'medium' });
157
+ expect(config.failOn).toBe('MEDIUM');
158
+ });
159
+ it('should override verbose from CLI', () => {
160
+ const config = loadConfig({ verbose: true });
161
+ expect(config.verbose).toBe(true);
162
+ });
163
+ it('should override ci from CLI', () => {
164
+ const config = loadConfig({ ci: true });
165
+ expect(config.ci).toBe(true);
166
+ });
167
+ it('should override watch from CLI', () => {
168
+ const config = loadConfig({ watch: true });
169
+ expect(config.watch).toBe(true);
170
+ });
171
+ it('should override configOnly from CLI', () => {
172
+ const config = loadConfig({ configOnly: true });
173
+ expect(config.configOnly).toBe(true);
174
+ });
175
+ it('should override docDampening from CLI', () => {
176
+ const config = loadConfig({ docDampening: false });
177
+ expect(config.docDampening).toBe(false);
178
+ });
179
+ it('should override redact from CLI', () => {
180
+ const config = loadConfig({ redact: true });
181
+ expect(config.redact).toBe(true);
182
+ });
183
+ it('should override allowRemoteRules from CLI', () => {
184
+ const config = loadConfig({ allowRemoteRules: true });
185
+ expect(config.allowRemoteRules).toBe(true);
186
+ });
187
+ it('should override analysis features from CLI', () => {
188
+ const config = loadConfig({
189
+ threatIntel: true,
190
+ semanticAnalysis: true,
191
+ correlationAnalysis: true,
192
+ entropyAnalysis: true,
193
+ mcpValidation: true,
194
+ dependencyAnalysis: true,
195
+ capabilityMapping: true,
196
+ });
197
+ expect(config.threatIntel).toBe(true);
198
+ expect(config.semanticAnalysis).toBe(true);
199
+ expect(config.correlationAnalysis).toBe(true);
200
+ expect(config.entropyAnalysis).toBe(true);
201
+ expect(config.mcpValidation).toBe(true);
202
+ expect(config.dependencyAnalysis).toBe(true);
203
+ expect(config.capabilityMapping).toBe(true);
204
+ });
205
+ it('should override marketplace mode from CLI', () => {
206
+ const config = loadConfig({ marketplace: 'all' });
207
+ expect(config.marketplaceMode).toBe('all');
208
+ });
209
+ it('should warn on invalid marketplace mode', () => {
210
+ loadConfig({ marketplace: 'invalid' });
211
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid --marketplace mode'));
212
+ });
213
+ it('should override LLM settings from CLI', () => {
214
+ const config = loadConfig({
215
+ llmAnalysis: true,
216
+ llmProvider: 'custom-provider',
217
+ llmModel: 'custom-model',
218
+ llmBaseUrl: 'https://custom.com/api',
219
+ llmApiKeyEnv: 'MY_KEY',
220
+ llmTimeoutMs: 60000,
221
+ llmMaxInputChars: 5000,
222
+ llmCacheDir: '/tmp/cache',
223
+ llmOnlyIfFindings: false,
224
+ llmMaxFiles: 10,
225
+ llmMinConfidence: 0.8,
226
+ });
227
+ expect(config.llmAnalysis).toBe(true);
228
+ expect(config.llm.provider).toBe('custom-provider');
229
+ expect(config.llm.model).toBe('custom-model');
230
+ expect(config.llm.baseUrl).toBe('https://custom.com/api');
231
+ expect(config.llm.apiKeyEnv).toBe('MY_KEY');
232
+ expect(config.llm.timeoutMs).toBe(60000);
233
+ expect(config.llm.maxInputChars).toBe(5000);
234
+ expect(config.llm.cacheDir).toBe('/tmp/cache');
235
+ expect(config.llm.onlyIfFindings).toBe(false);
236
+ expect(config.llm.maxFiles).toBe(10);
237
+ expect(config.llm.minConfidence).toBe(0.8);
238
+ });
239
+ });
240
+ // ---------------------------------------------------------------------------
241
+ // Thorough mode
242
+ // ---------------------------------------------------------------------------
243
+ describe('loadConfig thorough mode', () => {
244
+ it('should enable all analysis features when thorough is set', () => {
245
+ const config = loadConfig({ thorough: true });
246
+ expect(config.threatIntel).toBe(true);
247
+ expect(config.semanticAnalysis).toBe(true);
248
+ expect(config.correlationAnalysis).toBe(true);
249
+ expect(config.entropyAnalysis).toBe(true);
250
+ expect(config.mcpValidation).toBe(true);
251
+ expect(config.dependencyAnalysis).toBe(true);
252
+ expect(config.capabilityMapping).toBe(true);
253
+ expect(config.ignoreComments).toBe(true);
254
+ expect(config.mitreAtlas).toBe(true);
255
+ });
256
+ });
257
+ // ---------------------------------------------------------------------------
258
+ // Output file
259
+ // ---------------------------------------------------------------------------
260
+ describe('loadConfig output file', () => {
261
+ it('should set outputFile from CLI', () => {
262
+ const config = loadConfig({ output: '/tmp/report.json' });
263
+ expect(config.outputFile).toBe('/tmp/report.json');
264
+ });
265
+ it('should not set outputFile by default', () => {
266
+ const config = loadConfig({});
267
+ expect(config.outputFile).toBeUndefined();
268
+ });
269
+ });
270
+ //# sourceMappingURL=config.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Exit Code Tests
3
+ * Tests getExitCode from Scanner.ts for various finding/config combinations
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=exitCodes.test.d.ts.map