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 +24 -0
- package/README.md +72 -118
- package/bin/ferret.js +24 -4
- package/dist/__tests__/config.test.d.ts +6 -0
- package/dist/__tests__/config.test.js +270 -0
- package/dist/__tests__/exitCodes.test.d.ts +6 -0
- package/dist/__tests__/exitCodes.test.js +266 -0
- package/dist/__tests__/reporters.test.d.ts +6 -0
- package/dist/__tests__/reporters.test.js +341 -0
- package/dist/__tests__/rules.test.d.ts +6 -0
- package/dist/__tests__/rules.test.js +766 -0
- package/dist/features/customRules.js +1 -1
- package/dist/features/entropyAnalysis.js +2 -2
- package/dist/features/llmAnalysis.js +5 -1
- package/dist/marketplace/MarketplaceScanner.d.ts +0 -1
- package/dist/marketplace/MarketplaceScanner.js +3 -3
- package/dist/mitre/atlasCatalog.js +3 -3
- package/dist/monitoring/AgentMonitor.js +4 -0
- package/dist/reporters/ConsoleReporter.js +41 -56
- package/dist/rules/credentials.js +12 -5
- package/dist/rules/exfiltration.js +12 -0
- package/dist/sandbox/SandboxValidator.js +1 -0
- package/dist/scanner/Scanner.js +9 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.js +1 -0
- package/dist/utils/config.js +25 -4
- package/package.json +9 -3
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
|
-
##
|
|
98
|
+
## What's New in v2.1.0
|
|
99
99
|
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
- **
|
|
107
|
-
- **
|
|
108
|
-
- **
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
-
|
|
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 (
|
|
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**
|
|
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
|
|
653
|
+
- npx -p ferret-scan ferret scan . --ci --format sarif -o ferret-results.sarif
|
|
701
654
|
artifacts:
|
|
702
655
|
reports:
|
|
703
|
-
sast: ferret-results.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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,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
|