ferret-scan 2.1.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +109 -13
  4. package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
  5. package/dist/__tests__/AgentMonitor.test.js +235 -0
  6. package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
  7. package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
  8. package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
  9. package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
  10. package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
  11. package/dist/__tests__/IndicatorMatcher.test.js +245 -0
  12. package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
  13. package/dist/__tests__/MarketplaceScanner.test.js +212 -0
  14. package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
  15. package/dist/__tests__/RuleGenerator.test.js +207 -0
  16. package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
  17. package/dist/__tests__/ThreatFeed.test.js +359 -0
  18. package/dist/__tests__/WatchMode.test.d.ts +6 -0
  19. package/dist/__tests__/WatchMode.test.js +104 -0
  20. package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
  21. package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
  22. package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
  23. package/dist/__tests__/astAnalyzerFull.test.js +138 -0
  24. package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
  25. package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
  26. package/dist/__tests__/atlas.test.d.ts +6 -0
  27. package/dist/__tests__/atlas.test.js +319 -0
  28. package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
  29. package/dist/__tests__/atlasCatalog.test.js +200 -0
  30. package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
  31. package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
  32. package/dist/__tests__/baseline.test.d.ts +6 -0
  33. package/dist/__tests__/baseline.test.js +321 -0
  34. package/dist/__tests__/baselineExtra.test.d.ts +6 -0
  35. package/dist/__tests__/baselineExtra.test.js +317 -0
  36. package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
  37. package/dist/__tests__/capabilityMapping.test.js +49 -0
  38. package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
  39. package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
  40. package/dist/__tests__/complianceExtra.test.d.ts +6 -0
  41. package/dist/__tests__/complianceExtra.test.js +121 -0
  42. package/dist/__tests__/config.test.js +1 -1
  43. package/dist/__tests__/configLoader.test.d.ts +6 -0
  44. package/dist/__tests__/configLoader.test.js +225 -0
  45. package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
  46. package/dist/__tests__/configLoaderExtra.test.js +186 -0
  47. package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
  48. package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
  49. package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
  50. package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
  51. package/dist/__tests__/customRules.extra.test.d.ts +6 -0
  52. package/dist/__tests__/customRules.extra.test.js +245 -0
  53. package/dist/__tests__/customRules.test.d.ts +7 -0
  54. package/dist/__tests__/customRules.test.js +347 -0
  55. package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
  56. package/dist/__tests__/dependencyRisk.test.js +248 -0
  57. package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
  58. package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
  59. package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
  60. package/dist/__tests__/featureExitCodes.test.js +332 -0
  61. package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
  62. package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
  63. package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
  64. package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
  65. package/dist/__tests__/fixer.extra.test.d.ts +6 -0
  66. package/dist/__tests__/fixer.extra.test.js +135 -0
  67. package/dist/__tests__/fixerApply.test.d.ts +6 -0
  68. package/dist/__tests__/fixerApply.test.js +132 -0
  69. package/dist/__tests__/gitHooks.test.d.ts +7 -0
  70. package/dist/__tests__/gitHooks.test.js +188 -0
  71. package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
  72. package/dist/__tests__/htmlReporter.extra.test.js +126 -0
  73. package/dist/__tests__/interactiveTui.test.d.ts +6 -0
  74. package/dist/__tests__/interactiveTui.test.js +180 -0
  75. package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
  76. package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
  77. package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
  78. package/dist/__tests__/interactiveTuiMore.test.js +194 -0
  79. package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
  80. package/dist/__tests__/interactiveTuiSession.test.js +173 -0
  81. package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
  82. package/dist/__tests__/llmAnalysis.test.js +229 -0
  83. package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
  84. package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
  85. package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
  86. package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
  87. package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
  88. package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
  89. package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
  90. package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
  91. package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
  92. package/dist/__tests__/llmGroqTPM.test.js +89 -0
  93. package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
  94. package/dist/__tests__/llmProviderRetry.test.js +172 -0
  95. package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
  96. package/dist/__tests__/mcpValidator.extra.test.js +270 -0
  97. package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
  98. package/dist/__tests__/patternMatcherExtra.test.js +198 -0
  99. package/dist/__tests__/patternsCommon.test.d.ts +6 -0
  100. package/dist/__tests__/patternsCommon.test.js +107 -0
  101. package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
  102. package/dist/__tests__/policyEnforcement.test.js +510 -0
  103. package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
  104. package/dist/__tests__/quarantineExtra.test.js +214 -0
  105. package/dist/__tests__/redactionExtra.test.d.ts +6 -0
  106. package/dist/__tests__/redactionExtra.test.js +228 -0
  107. package/dist/__tests__/scanDiff.test.d.ts +7 -0
  108. package/dist/__tests__/scanDiff.test.js +266 -0
  109. package/dist/__tests__/scanFull.test.d.ts +6 -0
  110. package/dist/__tests__/scanFull.test.js +158 -0
  111. package/dist/__tests__/scannerDampening.test.d.ts +6 -0
  112. package/dist/__tests__/scannerDampening.test.js +160 -0
  113. package/dist/__tests__/scannerExtra.test.d.ts +6 -0
  114. package/dist/__tests__/scannerExtra.test.js +194 -0
  115. package/dist/__tests__/scannerMitre.test.d.ts +5 -0
  116. package/dist/__tests__/scannerMitre.test.js +141 -0
  117. package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
  118. package/dist/__tests__/scannerSSRF.test.js +149 -0
  119. package/dist/__tests__/schemas.test.d.ts +6 -0
  120. package/dist/__tests__/schemas.test.js +125 -0
  121. package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
  122. package/dist/__tests__/webhooks.extra.test.js +144 -0
  123. package/dist/__tests__/webhooks.test.d.ts +6 -0
  124. package/dist/__tests__/webhooks.test.js +154 -0
  125. package/dist/analyzers/AstAnalyzer.d.ts +5 -1
  126. package/dist/analyzers/AstAnalyzer.js +25 -4
  127. package/dist/features/customRules.js +22 -29
  128. package/dist/features/ignoreComments.js +5 -5
  129. package/dist/features/mcpTrustScore.d.ts +17 -0
  130. package/dist/features/mcpTrustScore.js +74 -0
  131. package/dist/features/mcpValidator.d.ts +2 -0
  132. package/dist/features/mcpValidator.js +13 -0
  133. package/dist/features/policyEnforcement.d.ts +22 -22
  134. package/dist/features/policyEnforcement.js +3 -2
  135. package/dist/intelligence/ThreatFeed.js +207 -62
  136. package/dist/remediation/Fixer.js +56 -30
  137. package/dist/remediation/Quarantine.js +79 -11
  138. package/dist/reporters/ConsoleReporter.js +10 -0
  139. package/dist/reporters/HtmlReporter.js +5 -0
  140. package/dist/reporters/SarifReporter.d.ts +1 -0
  141. package/dist/reporters/SarifReporter.js +1 -0
  142. package/dist/rules/ai-specific.js +8 -8
  143. package/dist/rules/backdoors.js +12 -12
  144. package/dist/rules/correlationRules.js +6 -6
  145. package/dist/rules/index.d.ts +1 -0
  146. package/dist/rules/index.js +10 -1
  147. package/dist/rules/injection.js +8 -8
  148. package/dist/rules/patterns/common.d.ts +34 -0
  149. package/dist/rules/patterns/common.js +48 -0
  150. package/dist/scanner/IAnalyzer.d.ts +19 -0
  151. package/dist/scanner/IAnalyzer.js +5 -0
  152. package/dist/scanner/PatternMatcher.js +19 -2
  153. package/dist/scanner/Scanner.js +64 -125
  154. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  155. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  156. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  157. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  158. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  159. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  160. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  161. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  162. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  163. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  164. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  165. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  166. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  167. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  168. package/dist/types.d.ts +23 -0
  169. package/dist/types.js +1 -1
  170. package/dist/utils/baseline.d.ts +15 -2
  171. package/dist/utils/baseline.js +50 -19
  172. package/dist/utils/contentCache.d.ts +39 -0
  173. package/dist/utils/contentCache.js +77 -0
  174. package/dist/utils/glob.d.ts +50 -0
  175. package/dist/utils/glob.js +84 -0
  176. package/dist/utils/pathSecurity.js +1 -0
  177. package/dist/utils/safeRegex.d.ts +55 -0
  178. package/dist/utils/safeRegex.js +130 -0
  179. package/dist/utils/schemas.d.ts +70 -64
  180. package/dist/utils/schemas.js +13 -0
  181. package/package.json +34 -19
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Dependency Risk Analysis Tests
3
+ */
4
+ import { parsePackageJson, dependencyAssessmentsToFindings } from '../features/dependencyRisk.js';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ describe('parsePackageJson', () => {
9
+ let tmpDir;
10
+ let pkgPath;
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-dep-test-'));
13
+ pkgPath = path.join(tmpDir, 'package.json');
14
+ });
15
+ afterEach(() => {
16
+ fs.rmSync(tmpDir, { recursive: true, force: true });
17
+ });
18
+ it('returns error for non-existent file', () => {
19
+ const { packages, errors } = parsePackageJson('/nonexistent/package.json');
20
+ expect(packages).toHaveLength(0);
21
+ expect(errors.length).toBeGreaterThan(0);
22
+ expect(errors[0]).toContain('not found');
23
+ });
24
+ it('parses a simple package.json with dependencies', () => {
25
+ fs.writeFileSync(pkgPath, JSON.stringify({
26
+ name: 'test-app',
27
+ dependencies: {
28
+ 'express': '^4.18.0',
29
+ 'lodash': '4.17.21',
30
+ },
31
+ }));
32
+ const { packages, errors } = parsePackageJson(pkgPath);
33
+ expect(errors).toHaveLength(0);
34
+ expect(packages).toHaveLength(2);
35
+ expect(packages.map(p => p.name)).toContain('express');
36
+ expect(packages.map(p => p.name)).toContain('lodash');
37
+ expect(packages.every(p => p.type === 'dependency')).toBe(true);
38
+ });
39
+ it('parses devDependencies', () => {
40
+ fs.writeFileSync(pkgPath, JSON.stringify({
41
+ name: 'test-app',
42
+ devDependencies: {
43
+ 'jest': '^29.0.0',
44
+ },
45
+ }));
46
+ const { packages } = parsePackageJson(pkgPath);
47
+ expect(packages[0]?.type).toBe('devDependency');
48
+ expect(packages[0]?.name).toBe('jest');
49
+ });
50
+ it('parses peerDependencies', () => {
51
+ fs.writeFileSync(pkgPath, JSON.stringify({
52
+ name: 'test-lib',
53
+ peerDependencies: {
54
+ 'react': '>=17.0.0',
55
+ },
56
+ }));
57
+ const { packages } = parsePackageJson(pkgPath);
58
+ expect(packages[0]?.type).toBe('peerDependency');
59
+ });
60
+ it('parses optionalDependencies', () => {
61
+ fs.writeFileSync(pkgPath, JSON.stringify({
62
+ name: 'test-app',
63
+ optionalDependencies: {
64
+ 'fsevents': '^2.3.0',
65
+ },
66
+ }));
67
+ const { packages } = parsePackageJson(pkgPath);
68
+ expect(packages[0]?.type).toBe('optionalDependency');
69
+ });
70
+ it('detects local file dependencies', () => {
71
+ fs.writeFileSync(pkgPath, JSON.stringify({
72
+ name: 'test-app',
73
+ dependencies: {
74
+ 'my-local-lib': 'file:../local-lib',
75
+ },
76
+ }));
77
+ const { packages } = parsePackageJson(pkgPath);
78
+ expect(packages[0]?.isLocal).toBe(true);
79
+ });
80
+ it('detects git dependencies', () => {
81
+ fs.writeFileSync(pkgPath, JSON.stringify({
82
+ name: 'test-app',
83
+ dependencies: {
84
+ 'my-git-dep': 'github:user/repo#main',
85
+ },
86
+ }));
87
+ const { packages } = parsePackageJson(pkgPath);
88
+ expect(packages[0]?.isGit).toBe(true);
89
+ });
90
+ it('detects URL dependencies', () => {
91
+ fs.writeFileSync(pkgPath, JSON.stringify({
92
+ name: 'test-app',
93
+ dependencies: {
94
+ 'url-dep': 'https://example.com/package.tgz',
95
+ },
96
+ }));
97
+ const { packages } = parsePackageJson(pkgPath);
98
+ expect(packages[0]?.isUrl).toBe(true);
99
+ });
100
+ it('returns error for invalid JSON', () => {
101
+ fs.writeFileSync(pkgPath, 'not valid json {{{');
102
+ const { packages, errors } = parsePackageJson(pkgPath);
103
+ expect(packages).toHaveLength(0);
104
+ expect(errors.length).toBeGreaterThan(0);
105
+ });
106
+ it('handles empty dependencies section', () => {
107
+ fs.writeFileSync(pkgPath, JSON.stringify({
108
+ name: 'empty-app',
109
+ dependencies: {},
110
+ }));
111
+ const { packages, errors } = parsePackageJson(pkgPath);
112
+ expect(errors).toHaveLength(0);
113
+ expect(packages).toHaveLength(0);
114
+ });
115
+ it('handles package.json with no dependencies', () => {
116
+ fs.writeFileSync(pkgPath, JSON.stringify({
117
+ name: 'no-deps',
118
+ version: '1.0.0',
119
+ }));
120
+ const { packages, errors } = parsePackageJson(pkgPath);
121
+ expect(errors).toHaveLength(0);
122
+ expect(packages).toHaveLength(0);
123
+ });
124
+ });
125
+ describe('dependencyAssessmentsToFindings', () => {
126
+ function makeResult(overrides = {}) {
127
+ return {
128
+ packageJsonPath: '/project/package.json',
129
+ totalPackages: 0,
130
+ assessments: [],
131
+ summary: { critical: 0, high: 0, medium: 0, low: 0, vulnerable: 0 },
132
+ ...overrides,
133
+ };
134
+ }
135
+ it('returns empty array for no assessments', () => {
136
+ const findings = dependencyAssessmentsToFindings(makeResult());
137
+ expect(findings).toEqual([]);
138
+ });
139
+ it('converts issues to findings', () => {
140
+ const result = makeResult({
141
+ assessments: [
142
+ {
143
+ package: { name: 'event-stream', version: '3.3.6', type: 'dependency' },
144
+ riskLevel: 'critical',
145
+ issues: [
146
+ {
147
+ type: 'known-malicious',
148
+ severity: 'CRITICAL',
149
+ description: 'Known malicious package',
150
+ remediation: 'Remove immediately',
151
+ },
152
+ ],
153
+ vulnerabilities: [],
154
+ },
155
+ ],
156
+ });
157
+ const findings = dependencyAssessmentsToFindings(result);
158
+ expect(findings).toHaveLength(1);
159
+ expect(findings[0]?.severity).toBe('CRITICAL');
160
+ expect(findings[0]?.match).toBe('event-stream@3.3.6');
161
+ expect(findings[0]?.ruleId).toMatch(/^DEP-/);
162
+ });
163
+ it('converts vulnerabilities to findings', () => {
164
+ const result = makeResult({
165
+ assessments: [
166
+ {
167
+ package: { name: 'lodash', version: '4.17.0', type: 'dependency' },
168
+ riskLevel: 'high',
169
+ issues: [],
170
+ vulnerabilities: [
171
+ {
172
+ id: 'CVE-2021-1234',
173
+ severity: 'high',
174
+ title: 'Prototype Pollution',
175
+ url: 'https://nvd.nist.gov/CVE-2021-1234',
176
+ fixAvailable: true,
177
+ affectedVersions: '<4.17.21',
178
+ },
179
+ ],
180
+ },
181
+ ],
182
+ });
183
+ const findings = dependencyAssessmentsToFindings(result);
184
+ expect(findings).toHaveLength(1);
185
+ expect(findings[0]?.severity).toBe('HIGH');
186
+ expect(findings[0]?.ruleId).toBe('DEP-VULN-CVE-2021-1234');
187
+ expect(findings[0]?.remediation).toContain('Update lodash');
188
+ });
189
+ it('maps vulnerability severity correctly', () => {
190
+ const result = makeResult({
191
+ assessments: [
192
+ {
193
+ package: { name: 'pkg', version: '1.0.0', type: 'dependency' },
194
+ riskLevel: 'medium',
195
+ issues: [],
196
+ vulnerabilities: [
197
+ { id: 'CVE-001', severity: 'critical', title: 'Critical Vuln', fixAvailable: false },
198
+ { id: 'CVE-002', severity: 'moderate', title: 'Moderate Vuln', fixAvailable: false },
199
+ { id: 'CVE-003', severity: 'low', title: 'Low Vuln', fixAvailable: false },
200
+ ],
201
+ },
202
+ ],
203
+ });
204
+ const findings = dependencyAssessmentsToFindings(result);
205
+ expect(findings[0]?.severity).toBe('CRITICAL');
206
+ expect(findings[1]?.severity).toBe('MEDIUM');
207
+ expect(findings[2]?.severity).toBe('LOW');
208
+ });
209
+ it('includes no-fix message when fixAvailable is false', () => {
210
+ const result = makeResult({
211
+ assessments: [
212
+ {
213
+ package: { name: 'vulnerable-pkg', version: '1.0.0', type: 'dependency' },
214
+ riskLevel: 'high',
215
+ issues: [],
216
+ vulnerabilities: [
217
+ { id: 'CVE-001', severity: 'high', title: 'Bad Vuln', fixAvailable: false },
218
+ ],
219
+ },
220
+ ],
221
+ });
222
+ const findings = dependencyAssessmentsToFindings(result);
223
+ expect(findings[0]?.remediation).toContain('No fix available');
224
+ });
225
+ it('assigns correct risk scores', () => {
226
+ const result = makeResult({
227
+ assessments: [
228
+ {
229
+ package: { name: 'pkg', version: '1.0.0', type: 'dependency' },
230
+ riskLevel: 'critical',
231
+ issues: [
232
+ { type: 'known-malicious', severity: 'CRITICAL', description: 'X', remediation: 'Y' },
233
+ { type: 'git-dep', severity: 'HIGH', description: 'X', remediation: 'Y' },
234
+ { type: 'unpinned', severity: 'MEDIUM', description: 'X', remediation: 'Y' },
235
+ { type: 'local-dep', severity: 'LOW', description: 'X', remediation: 'Y' },
236
+ ],
237
+ vulnerabilities: [],
238
+ },
239
+ ],
240
+ });
241
+ const findings = dependencyAssessmentsToFindings(result);
242
+ expect(findings[0]?.riskScore).toBe(95); // CRITICAL
243
+ expect(findings[1]?.riskScore).toBe(80); // HIGH
244
+ expect(findings[2]?.riskScore).toBe(60); // MEDIUM
245
+ expect(findings[3]?.riskScore).toBe(40); // LOW
246
+ });
247
+ });
248
+ //# sourceMappingURL=dependencyRisk.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Additional Dependency Risk Tests
3
+ * Covers analyzePackage, analyzeDependencies, findAndAnalyzeDependencies
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=dependencyRiskExtra.test.d.ts.map
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Additional Dependency Risk Tests
3
+ * Covers analyzePackage, analyzeDependencies, findAndAnalyzeDependencies
4
+ */
5
+ import { analyzeDependencies, } from '../features/dependencyRisk.js';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as os from 'node:os';
9
+ // Mock execSync to prevent real npm audit calls
10
+ jest.mock('node:child_process', () => ({
11
+ execSync: jest.fn().mockReturnValue(JSON.stringify({
12
+ vulnerabilities: {},
13
+ })),
14
+ }));
15
+ describe('analyzeDependencies', () => {
16
+ let tmpDir;
17
+ beforeEach(() => {
18
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-deps-analyze-'));
19
+ });
20
+ afterEach(() => {
21
+ fs.rmSync(tmpDir, { recursive: true, force: true });
22
+ });
23
+ it('handles non-existent package.json', () => {
24
+ const result = analyzeDependencies('/nonexistent/package.json');
25
+ expect(result.totalPackages).toBe(0);
26
+ expect(result.assessments).toHaveLength(0);
27
+ });
28
+ it('analyzes package with no risky deps', () => {
29
+ const pkgPath = path.join(tmpDir, 'package.json');
30
+ fs.writeFileSync(pkgPath, JSON.stringify({
31
+ name: 'safe-app',
32
+ dependencies: {
33
+ 'express': '^4.18.0',
34
+ 'lodash': '4.17.21',
35
+ },
36
+ }));
37
+ const result = analyzeDependencies(pkgPath, false);
38
+ expect(result.totalPackages).toBe(2);
39
+ expect(result.summary.critical).toBe(0);
40
+ });
41
+ it('detects known high-risk package (event-stream)', () => {
42
+ const pkgPath = path.join(tmpDir, 'package.json');
43
+ fs.writeFileSync(pkgPath, JSON.stringify({
44
+ name: 'risky-app',
45
+ dependencies: {
46
+ 'event-stream': '3.3.6',
47
+ },
48
+ }));
49
+ const result = analyzeDependencies(pkgPath, false);
50
+ expect(result.summary.critical).toBeGreaterThan(0);
51
+ const criticalAssessments = result.assessments.filter(a => a.riskLevel === 'critical');
52
+ expect(criticalAssessments.length).toBeGreaterThan(0);
53
+ expect(criticalAssessments[0]?.issues.some(i => i.type === 'known-malicious')).toBe(true);
54
+ });
55
+ it('detects flatmap-stream as high-risk', () => {
56
+ const pkgPath = path.join(tmpDir, 'package.json');
57
+ fs.writeFileSync(pkgPath, JSON.stringify({
58
+ dependencies: { 'flatmap-stream': '0.1.1' },
59
+ }));
60
+ const result = analyzeDependencies(pkgPath, false);
61
+ const assessment = result.assessments.find(a => a.package.name === 'flatmap-stream');
62
+ expect(assessment?.riskLevel).toBe('critical');
63
+ });
64
+ it('detects security concern packages (node-serialize)', () => {
65
+ const pkgPath = path.join(tmpDir, 'package.json');
66
+ fs.writeFileSync(pkgPath, JSON.stringify({
67
+ dependencies: { 'node-serialize': '0.0.4' },
68
+ }));
69
+ const result = analyzeDependencies(pkgPath, false);
70
+ const assessment = result.assessments.find(a => a.package.name === 'node-serialize');
71
+ expect(assessment?.issues.some(i => i.type === 'security-concern')).toBe(true);
72
+ });
73
+ it('detects vm2 as medium security concern', () => {
74
+ const pkgPath = path.join(tmpDir, 'package.json');
75
+ fs.writeFileSync(pkgPath, JSON.stringify({
76
+ dependencies: { 'vm2': '3.9.19' },
77
+ }));
78
+ const result = analyzeDependencies(pkgPath, false);
79
+ const assessment = result.assessments.find(a => a.package.name === 'vm2');
80
+ expect(assessment?.issues.some(i => i.type === 'security-concern')).toBe(true);
81
+ });
82
+ it('detects git dependency', () => {
83
+ const pkgPath = path.join(tmpDir, 'package.json');
84
+ fs.writeFileSync(pkgPath, JSON.stringify({
85
+ dependencies: { 'my-lib': 'github:user/repo#main' },
86
+ }));
87
+ const result = analyzeDependencies(pkgPath, false);
88
+ const assessment = result.assessments.find(a => a.package.name === 'my-lib');
89
+ expect(assessment?.issues.some(i => i.type === 'git-dependency')).toBe(true);
90
+ });
91
+ it('detects URL dependency', () => {
92
+ const pkgPath = path.join(tmpDir, 'package.json');
93
+ fs.writeFileSync(pkgPath, JSON.stringify({
94
+ dependencies: { 'url-dep': 'https://example.com/pkg.tgz' },
95
+ }));
96
+ const result = analyzeDependencies(pkgPath, false);
97
+ const assessment = result.assessments.find(a => a.package.name === 'url-dep');
98
+ expect(assessment?.issues.some(i => i.type === 'url-dependency')).toBe(true);
99
+ });
100
+ it('detects insecure HTTP URL', () => {
101
+ const pkgPath = path.join(tmpDir, 'package.json');
102
+ fs.writeFileSync(pkgPath, JSON.stringify({
103
+ dependencies: { 'http-dep': 'http://example.com/pkg.tgz' },
104
+ }));
105
+ const result = analyzeDependencies(pkgPath, false);
106
+ const assessment = result.assessments.find(a => a.package.name === 'http-dep');
107
+ expect(assessment?.issues.some(i => i.type === 'insecure-url')).toBe(true);
108
+ });
109
+ it('detects wildcard version', () => {
110
+ const pkgPath = path.join(tmpDir, 'package.json');
111
+ fs.writeFileSync(pkgPath, JSON.stringify({
112
+ dependencies: { 'wild-dep': '*' },
113
+ }));
114
+ const result = analyzeDependencies(pkgPath, false);
115
+ const assessment = result.assessments.find(a => a.package.name === 'wild-dep');
116
+ expect(assessment?.issues.some(i => i.type === 'unpinned-version')).toBe(true);
117
+ });
118
+ it('detects "latest" as unpinned version', () => {
119
+ const pkgPath = path.join(tmpDir, 'package.json');
120
+ fs.writeFileSync(pkgPath, JSON.stringify({
121
+ dependencies: { 'latest-dep': 'latest' },
122
+ }));
123
+ const result = analyzeDependencies(pkgPath, false);
124
+ const assessment = result.assessments.find(a => a.package.name === 'latest-dep');
125
+ expect(assessment?.issues.some(i => i.type === 'unpinned-version')).toBe(true);
126
+ });
127
+ it('detects very early version (0.0.x) as possibly-abandoned', () => {
128
+ const pkgPath = path.join(tmpDir, 'package.json');
129
+ fs.writeFileSync(pkgPath, JSON.stringify({
130
+ dependencies: { 'old-dep': '0.0.1' },
131
+ }));
132
+ const result = analyzeDependencies(pkgPath, false);
133
+ const assessment = result.assessments.find(a => a.package.name === 'old-dep');
134
+ expect(assessment?.issues.some(i => i.type === 'possibly-abandoned')).toBe(true);
135
+ });
136
+ it('calculates correct summary counts', () => {
137
+ const pkgPath = path.join(tmpDir, 'package.json');
138
+ fs.writeFileSync(pkgPath, JSON.stringify({
139
+ dependencies: {
140
+ 'event-stream': '3.3.6', // critical
141
+ 'express': '^4.18.0', // none
142
+ 'node-serialize': '0.0.4', // high
143
+ },
144
+ }));
145
+ const result = analyzeDependencies(pkgPath, false);
146
+ expect(result.summary.critical).toBeGreaterThan(0);
147
+ expect(result.totalPackages).toBe(3);
148
+ });
149
+ it('runs with audit by default (mocked)', () => {
150
+ const pkgPath = path.join(tmpDir, 'package.json');
151
+ fs.writeFileSync(pkgPath, JSON.stringify({
152
+ dependencies: { 'lodash': '4.17.21' },
153
+ }));
154
+ // Should not throw even with audit=true
155
+ expect(() => analyzeDependencies(pkgPath, true)).not.toThrow();
156
+ });
157
+ });
158
+ describe('local and file dependencies', () => {
159
+ let tmpDir;
160
+ beforeEach(() => {
161
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ferret-local-deps-'));
162
+ });
163
+ afterEach(() => {
164
+ fs.rmSync(tmpDir, { recursive: true, force: true });
165
+ });
166
+ it('detects local file dependency', () => {
167
+ const pkgPath = path.join(tmpDir, 'package.json');
168
+ fs.writeFileSync(pkgPath, JSON.stringify({
169
+ dependencies: { 'local-lib': 'file:../local-lib' },
170
+ }));
171
+ const result = analyzeDependencies(pkgPath, false);
172
+ const assessment = result.assessments.find(a => a.package.name === 'local-lib');
173
+ expect(assessment?.issues.some(i => i.type === 'local-dependency')).toBe(true);
174
+ expect(assessment?.riskLevel).toBe('low');
175
+ });
176
+ });
177
+ //# sourceMappingURL=dependencyRiskExtra.test.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Feature ExitCodes Tests
3
+ * Tests for features/exitCodes.ts: determineExitCode, generateExitCodeSummary,
4
+ * formatExitCodeForCI, parseExitCodesFromEnv, validateExitCodes.
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=featureExitCodes.test.d.ts.map