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,138 @@
1
+ /**
2
+ * Full AstAnalyzer Tests
3
+ * Tests for analyzeFile function
4
+ */
5
+ import { analyzeFile } from '../analyzers/AstAnalyzer.js';
6
+ function makeFile(overrides = {}) {
7
+ return {
8
+ path: '/project/.claude/agents/test.md',
9
+ relativePath: 'agents/test.md',
10
+ type: 'md',
11
+ component: 'agent',
12
+ size: 100,
13
+ modified: new Date(),
14
+ ...overrides,
15
+ };
16
+ }
17
+ function makeSemanticRule(overrides = {}) {
18
+ return {
19
+ id: 'SEM-001',
20
+ name: 'Semantic Test Rule',
21
+ category: 'injection',
22
+ severity: 'HIGH',
23
+ description: 'Test semantic rule',
24
+ patterns: [],
25
+ fileTypes: ['md', 'ts', 'js'],
26
+ components: ['agent', 'skill', 'hook', 'plugin', 'mcp', 'settings', 'ai-config-md', 'rules-file'],
27
+ remediation: 'Fix it',
28
+ references: [],
29
+ enabled: true,
30
+ semanticPatterns: [
31
+ {
32
+ type: 'function-call',
33
+ pattern: 'dangerousCall($ARGS)',
34
+ },
35
+ ],
36
+ ...overrides,
37
+ };
38
+ }
39
+ describe('analyzeFile', () => {
40
+ it('returns empty array for rules with no semantic patterns', async () => {
41
+ const file = makeFile();
42
+ const rule = {
43
+ id: 'NO-SEM',
44
+ name: 'No Semantic Rule',
45
+ category: 'injection',
46
+ severity: 'HIGH',
47
+ description: 'Rule without semantic patterns',
48
+ patterns: [/test/gi],
49
+ fileTypes: ['md'],
50
+ components: ['agent'],
51
+ remediation: 'Fix',
52
+ references: [],
53
+ enabled: true,
54
+ };
55
+ const findings = await analyzeFile(file, 'const x = 1;', [rule]);
56
+ expect(findings).toHaveLength(0);
57
+ });
58
+ it('returns empty array for empty content', async () => {
59
+ const file = makeFile({ type: 'ts' });
60
+ const rule = makeSemanticRule();
61
+ const findings = await analyzeFile(file, '', [rule]);
62
+ expect(findings).toHaveLength(0);
63
+ });
64
+ it('analyzes TypeScript file for dangerous calls', async () => {
65
+ const file = makeFile({
66
+ path: '/project/.claude/agents/test.ts',
67
+ relativePath: 'agents/test.ts',
68
+ type: 'ts',
69
+ });
70
+ const rule = makeSemanticRule();
71
+ const content = 'const result = dangerousCall("argument");';
72
+ const findings = await analyzeFile(file, content, [rule]);
73
+ expect(Array.isArray(findings)).toBe(true);
74
+ });
75
+ it('analyzes markdown file with TypeScript code blocks', async () => {
76
+ const file = makeFile();
77
+ const rule = makeSemanticRule();
78
+ const content = '# Agent Config\n\n```typescript\nconst result = dangerousCall("arg");\n```\n';
79
+ const findings = await analyzeFile(file, content, [rule]);
80
+ expect(Array.isArray(findings)).toBe(true);
81
+ });
82
+ it('handles markdown without code blocks', async () => {
83
+ const file = makeFile();
84
+ const rule = makeSemanticRule();
85
+ const content = '# Simple markdown\nNo code blocks here.';
86
+ const findings = await analyzeFile(file, content, [rule]);
87
+ expect(findings).toHaveLength(0);
88
+ });
89
+ it('respects maxMs timeout option', async () => {
90
+ const file = makeFile({ type: 'ts' });
91
+ const rule = makeSemanticRule();
92
+ const content = 'const x = 1; dangerousCall("test"); const y = 2;';
93
+ const findings = await analyzeFile(file, content, [rule], { maxMs: 10 });
94
+ expect(Array.isArray(findings)).toBe(true);
95
+ });
96
+ it('handles JavaScript files', async () => {
97
+ const file = makeFile({
98
+ path: '/project/.claude/agents/test.js',
99
+ relativePath: 'agents/test.js',
100
+ type: 'js',
101
+ });
102
+ const rule = makeSemanticRule({
103
+ semanticPatterns: [{
104
+ type: 'function-call',
105
+ pattern: 'dangerousCall($ARGS)',
106
+ }],
107
+ });
108
+ const content = 'dangerousCall("some code");';
109
+ const findings = await analyzeFile(file, content, [rule]);
110
+ expect(Array.isArray(findings)).toBe(true);
111
+ });
112
+ it('returns empty array for non-code file types', async () => {
113
+ const file = makeFile({ type: 'json' });
114
+ const rule = makeSemanticRule();
115
+ const content = '{"key": "value"}';
116
+ const findings = await analyzeFile(file, content, [rule]);
117
+ expect(findings).toHaveLength(0);
118
+ });
119
+ it('handles multiple semantic patterns in one rule', async () => {
120
+ const file = makeFile({ type: 'ts' });
121
+ const rule = makeSemanticRule({
122
+ semanticPatterns: [
123
+ {
124
+ type: 'function-call',
125
+ pattern: 'dangerousCall($ARGS)',
126
+ },
127
+ {
128
+ type: 'function-call',
129
+ pattern: 'suspiciousOperation($ARGS)',
130
+ },
131
+ ],
132
+ });
133
+ const content = 'dangerousCall("bad"); suspiciousOperation("test");';
134
+ const findings = await analyzeFile(file, content, [rule]);
135
+ expect(Array.isArray(findings)).toBe(true);
136
+ });
137
+ });
138
+ //# sourceMappingURL=astAnalyzerFull.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * AstAnalyzer Pattern Tests
3
+ * Tests for different SemanticPattern types (property-access, dynamic-import, eval-chain, object-structure)
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=astAnalyzerPatterns.test.d.ts.map
@@ -0,0 +1,143 @@
1
+ /**
2
+ * AstAnalyzer Pattern Tests
3
+ * Tests for different SemanticPattern types (property-access, dynamic-import, eval-chain, object-structure)
4
+ */
5
+ import { analyzeFile } from '../analyzers/AstAnalyzer.js';
6
+ function makeFile(type = 'ts') {
7
+ return {
8
+ path: '/project/.claude/agents/test.' + type,
9
+ relativePath: 'agents/test.' + type,
10
+ type,
11
+ component: 'agent',
12
+ size: 100,
13
+ modified: new Date(),
14
+ };
15
+ }
16
+ function makeRule(patterns) {
17
+ return {
18
+ id: 'AST-TEST-001',
19
+ name: 'AST Test Rule',
20
+ category: 'injection',
21
+ severity: 'HIGH',
22
+ description: 'Test AST patterns',
23
+ patterns: [],
24
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
25
+ components: ['agent', 'skill', 'hook', 'plugin', 'mcp', 'settings', 'ai-config-md', 'rules-file'],
26
+ remediation: 'Fix it',
27
+ references: [],
28
+ enabled: true,
29
+ semanticPatterns: patterns,
30
+ };
31
+ }
32
+ describe('analyzeFile - SemanticPattern types', () => {
33
+ it('detects property-access pattern', async () => {
34
+ const file = makeFile('ts');
35
+ const rule = makeRule([{
36
+ type: 'property-access',
37
+ pattern: 'process.env',
38
+ }]);
39
+ const content = 'const key = process.env.API_KEY;';
40
+ const findings = await analyzeFile(file, content, [rule]);
41
+ expect(Array.isArray(findings)).toBe(true);
42
+ });
43
+ it('detects dynamic-import pattern', async () => {
44
+ const file = makeFile('ts');
45
+ const rule = makeRule([{
46
+ type: 'dynamic-import',
47
+ pattern: 'malicious-module',
48
+ }]);
49
+ const content = "const mod = await import('malicious-module');";
50
+ const findings = await analyzeFile(file, content, [rule]);
51
+ expect(Array.isArray(findings)).toBe(true);
52
+ });
53
+ it('detects object-structure pattern', async () => {
54
+ const file = makeFile('ts');
55
+ const rule = makeRule([{
56
+ type: 'object-structure',
57
+ pattern: 'dangerous',
58
+ }]);
59
+ const content = 'const obj = { dangerous: true };';
60
+ const findings = await analyzeFile(file, content, [rule]);
61
+ expect(Array.isArray(findings)).toBe(true);
62
+ });
63
+ it('handles multiple pattern types in one rule', async () => {
64
+ const file = makeFile('ts');
65
+ const rule = makeRule([
66
+ { type: 'function-call', pattern: 'suspiciousFunc' },
67
+ { type: 'property-access', pattern: 'sensitive.data' },
68
+ ]);
69
+ const content = 'suspiciousFunc(); const x = sensitive.data;';
70
+ const findings = await analyzeFile(file, content, [rule]);
71
+ expect(Array.isArray(findings)).toBe(true);
72
+ });
73
+ it('handles tsx files', async () => {
74
+ const file = makeFile('tsx');
75
+ const rule = makeRule([{
76
+ type: 'function-call',
77
+ pattern: 'unsafeFunc',
78
+ }]);
79
+ const content = 'const Component = () => { unsafeFunc(); return <div/>; }';
80
+ const findings = await analyzeFile(file, content, [rule]);
81
+ expect(Array.isArray(findings)).toBe(true);
82
+ });
83
+ it('handles jsx files', async () => {
84
+ const file = makeFile('jsx');
85
+ const rule = makeRule([{
86
+ type: 'function-call',
87
+ pattern: 'unsafeFunc',
88
+ }]);
89
+ const content = 'function Comp() { unsafeFunc(); return null; }';
90
+ const findings = await analyzeFile(file, content, [rule]);
91
+ expect(Array.isArray(findings)).toBe(true);
92
+ });
93
+ it('handles eval-chain pattern type', async () => {
94
+ const file = makeFile('ts');
95
+ const rule = makeRule([{
96
+ type: 'eval-chain',
97
+ pattern: 'JSON.parse',
98
+ }]);
99
+ const content = 'JSON.parse(userInput);';
100
+ const findings = await analyzeFile(file, content, [rule]);
101
+ expect(Array.isArray(findings)).toBe(true);
102
+ });
103
+ it('analyzes markdown with multiple code blocks', async () => {
104
+ const file = makeFile('md');
105
+ const rule = makeRule([{
106
+ type: 'function-call',
107
+ pattern: 'suspiciousAction',
108
+ }]);
109
+ const content = `
110
+ # Instructions
111
+
112
+ \`\`\`typescript
113
+ suspiciousAction("first");
114
+ \`\`\`
115
+
116
+ Some text.
117
+
118
+ \`\`\`javascript
119
+ suspiciousAction("second");
120
+ \`\`\`
121
+ `;
122
+ const findings = await analyzeFile(file, content, [rule]);
123
+ expect(Array.isArray(findings)).toBe(true);
124
+ });
125
+ it('handles syntax errors in TypeScript gracefully', async () => {
126
+ const file = makeFile('ts');
127
+ const rule = makeRule([{
128
+ type: 'function-call',
129
+ pattern: 'someFunc',
130
+ }]);
131
+ // Intentionally invalid TypeScript syntax
132
+ const content = 'const x = { missing colon;';
133
+ const findings = await analyzeFile(file, content, [rule]);
134
+ expect(Array.isArray(findings)).toBe(true);
135
+ });
136
+ it('handles empty patterns array', async () => {
137
+ const file = makeFile('ts');
138
+ const rule = makeRule([]);
139
+ const findings = await analyzeFile(file, 'const x = 1;', [rule]);
140
+ expect(findings).toHaveLength(0);
141
+ });
142
+ });
143
+ //# sourceMappingURL=astAnalyzerPatterns.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MITRE Atlas Tests
3
+ * Tests for technique mapping, annotation, and catalog summary functions.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=atlas.test.d.ts.map
@@ -0,0 +1,319 @@
1
+ /**
2
+ * MITRE Atlas Tests
3
+ * Tests for technique mapping, annotation, and catalog summary functions.
4
+ */
5
+ import { getMitreAtlasTechnique, getMitreAtlasTechniqueIdsForFinding, getMitreAtlasTechniquesForFinding, getMitreAtlasTechniqueCatalogSummary, getRelevantMitreAtlasTechniqueCatalogSummary, annotateFindingsWithMitreAtlas, severityToAtlasScore, setMitreAtlasTechniqueCatalog, MITRE_ATLAS_TECHNIQUES, } from '../mitre/atlas.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Helpers
8
+ // ---------------------------------------------------------------------------
9
+ function makeFinding(overrides = {}) {
10
+ return {
11
+ ruleId: 'INJ-001',
12
+ ruleName: 'Test Rule',
13
+ severity: 'HIGH',
14
+ category: 'injection',
15
+ file: '/test.md',
16
+ relativePath: 'test.md',
17
+ line: 1,
18
+ match: 'bad content',
19
+ context: [],
20
+ remediation: 'Fix it.',
21
+ timestamp: new Date(),
22
+ riskScore: 50,
23
+ ...overrides,
24
+ };
25
+ }
26
+ // ---------------------------------------------------------------------------
27
+ // getMitreAtlasTechnique
28
+ // ---------------------------------------------------------------------------
29
+ describe('getMitreAtlasTechnique', () => {
30
+ beforeEach(() => {
31
+ // Reset dynamic catalog
32
+ setMitreAtlasTechniqueCatalog(null);
33
+ });
34
+ it('returns known technique from built-in catalog', () => {
35
+ const tech = getMitreAtlasTechnique('AML.T0051');
36
+ expect(tech).toBeDefined();
37
+ expect(tech.name).toBe('LLM Prompt Injection');
38
+ });
39
+ it('returns undefined for unknown technique id', () => {
40
+ const tech = getMitreAtlasTechnique('AML.TXXXX');
41
+ expect(tech).toBeUndefined();
42
+ });
43
+ it('prefers dynamic catalog over built-in', () => {
44
+ const dynamic = {
45
+ 'AML.T0051': {
46
+ id: 'AML.T0051',
47
+ name: 'Custom Override Name',
48
+ url: 'https://example.com',
49
+ tactics: ['execution'],
50
+ },
51
+ };
52
+ setMitreAtlasTechniqueCatalog(dynamic);
53
+ const tech = getMitreAtlasTechnique('AML.T0051');
54
+ expect(tech.name).toBe('Custom Override Name');
55
+ });
56
+ });
57
+ // ---------------------------------------------------------------------------
58
+ // severityToAtlasScore
59
+ // ---------------------------------------------------------------------------
60
+ describe('severityToAtlasScore', () => {
61
+ it('maps CRITICAL to 5', () => expect(severityToAtlasScore('CRITICAL')).toBe(5));
62
+ it('maps HIGH to 4', () => expect(severityToAtlasScore('HIGH')).toBe(4));
63
+ it('maps MEDIUM to 3', () => expect(severityToAtlasScore('MEDIUM')).toBe(3));
64
+ it('maps LOW to 2', () => expect(severityToAtlasScore('LOW')).toBe(2));
65
+ it('maps INFO to 1', () => expect(severityToAtlasScore('INFO')).toBe(1));
66
+ });
67
+ // ---------------------------------------------------------------------------
68
+ // getMitreAtlasTechniqueIdsForFinding — rule-specific mappings
69
+ // ---------------------------------------------------------------------------
70
+ describe('getMitreAtlasTechniqueIdsForFinding — rule-specific', () => {
71
+ beforeEach(() => { setMitreAtlasTechniqueCatalog(null); });
72
+ const RULE_CASES = [
73
+ ['AI-001', ['AML.T0056']],
74
+ ['AI-004', ['AML.T0080']],
75
+ ['AI-005', ['AML.T0094']],
76
+ ['AI-006', ['AML.T0067']],
77
+ ['AI-008', ['AML.T0051', 'AML.T0093']],
78
+ ['AI-009', ['AML.T0053']],
79
+ ['AI-010', ['AML.T0054']],
80
+ ['AI-011', ['AML.T0081']],
81
+ ['INJ-002', ['AML.T0054']],
82
+ ['INJ-003', ['AML.T0054']],
83
+ ['INJ-004', ['AML.T0054']],
84
+ ['INJ-001', ['AML.T0051']],
85
+ ['INJ-006', ['AML.T0051']],
86
+ ['INJ-007', ['AML.T0051']],
87
+ ];
88
+ for (const [ruleId, expectedIds] of RULE_CASES) {
89
+ it(`maps ${ruleId} to ${expectedIds.join(', ')}`, () => {
90
+ const finding = makeFinding({ ruleId });
91
+ const ids = getMitreAtlasTechniqueIdsForFinding(finding);
92
+ for (const expected of expectedIds) {
93
+ expect(ids).toContain(expected);
94
+ }
95
+ });
96
+ }
97
+ });
98
+ // ---------------------------------------------------------------------------
99
+ // getMitreAtlasTechniqueIdsForFinding — category fallbacks
100
+ // ---------------------------------------------------------------------------
101
+ describe('getMitreAtlasTechniqueIdsForFinding — category fallbacks', () => {
102
+ beforeEach(() => { setMitreAtlasTechniqueCatalog(null); });
103
+ const CATEGORY_CASES = [
104
+ ['injection', ['AML.T0051']],
105
+ ['ai-specific', ['AML.T0051']],
106
+ ['credentials', ['AML.T0083', 'AML.T0098']],
107
+ ['exfiltration', ['AML.T0086', 'AML.T0057']],
108
+ ['obfuscation', ['AML.T0068']],
109
+ ['persistence', ['AML.T0081']],
110
+ ['supply-chain', ['AML.T0011.002', 'AML.T0104']],
111
+ ];
112
+ for (const [category, expectedIds] of CATEGORY_CASES) {
113
+ it(`category '${category}' maps to ${expectedIds.join(', ')}`, () => {
114
+ const finding = makeFinding({ ruleId: 'UNKNOWN-999', category: category });
115
+ const ids = getMitreAtlasTechniqueIdsForFinding(finding);
116
+ for (const expected of expectedIds) {
117
+ expect(ids).toContain(expected);
118
+ }
119
+ });
120
+ }
121
+ it('unknown category and unknown ruleId returns empty or specific default', () => {
122
+ const finding = makeFinding({ ruleId: 'UNKNOWN-999', category: 'backdoors' });
123
+ const ids = getMitreAtlasTechniqueIdsForFinding(finding);
124
+ // backdoors has no explicit mapping, may return empty
125
+ expect(Array.isArray(ids)).toBe(true);
126
+ });
127
+ });
128
+ // ---------------------------------------------------------------------------
129
+ // getMitreAtlasTechniqueIdsForFinding — explicit metadata
130
+ // ---------------------------------------------------------------------------
131
+ describe('getMitreAtlasTechniqueIdsForFinding — explicit metadata', () => {
132
+ beforeEach(() => { setMitreAtlasTechniqueCatalog(null); });
133
+ it('includes explicit IDs from metadata.mitre.atlas', () => {
134
+ const finding = makeFinding({
135
+ ruleId: 'UNKNOWN-999',
136
+ category: 'backdoors',
137
+ metadata: {
138
+ mitre: {
139
+ atlas: [{ id: 'AML.T0054' }, { id: 'AML.T0068' }],
140
+ },
141
+ },
142
+ });
143
+ const ids = getMitreAtlasTechniqueIdsForFinding(finding);
144
+ expect(ids).toContain('AML.T0054');
145
+ expect(ids).toContain('AML.T0068');
146
+ });
147
+ it('deduplicates IDs when both explicit and category map to same technique', () => {
148
+ const finding = makeFinding({
149
+ ruleId: 'UNKNOWN-999',
150
+ category: 'injection',
151
+ metadata: {
152
+ mitre: {
153
+ atlas: [{ id: 'AML.T0051' }],
154
+ },
155
+ },
156
+ });
157
+ const ids = getMitreAtlasTechniqueIdsForFinding(finding);
158
+ expect(ids.filter(id => id === 'AML.T0051')).toHaveLength(1);
159
+ });
160
+ });
161
+ // ---------------------------------------------------------------------------
162
+ // getMitreAtlasTechniquesForFinding
163
+ // ---------------------------------------------------------------------------
164
+ describe('getMitreAtlasTechniquesForFinding', () => {
165
+ beforeEach(() => {
166
+ setMitreAtlasTechniqueCatalog(null);
167
+ });
168
+ it('returns technique objects for known IDs', () => {
169
+ const finding = makeFinding({ ruleId: 'INJ-001' });
170
+ const techniques = getMitreAtlasTechniquesForFinding(finding);
171
+ expect(techniques.length).toBeGreaterThan(0);
172
+ expect(techniques[0].id).toBe('AML.T0051');
173
+ expect(techniques[0].url).toContain('atlas.mitre.org');
174
+ });
175
+ it('creates placeholder technique for unknown IDs in explicit metadata', () => {
176
+ const finding = makeFinding({
177
+ ruleId: 'UNKNOWN-999',
178
+ category: 'backdoors',
179
+ metadata: {
180
+ mitre: {
181
+ atlas: [{ id: 'AML.T9999' }],
182
+ },
183
+ },
184
+ });
185
+ const techniques = getMitreAtlasTechniquesForFinding(finding);
186
+ const placeholder = techniques.find(t => t.id === 'AML.T9999');
187
+ expect(placeholder).toBeDefined();
188
+ expect(placeholder.name).toBe('AML.T9999');
189
+ });
190
+ });
191
+ // ---------------------------------------------------------------------------
192
+ // annotateFindingsWithMitreAtlas
193
+ // ---------------------------------------------------------------------------
194
+ describe('annotateFindingsWithMitreAtlas', () => {
195
+ beforeEach(() => {
196
+ setMitreAtlasTechniqueCatalog(null);
197
+ });
198
+ it('adds mitre.atlas metadata to findings', () => {
199
+ const findings = [makeFinding({ ruleId: 'INJ-001' })];
200
+ const annotated = annotateFindingsWithMitreAtlas(findings);
201
+ const meta = annotated[0].metadata;
202
+ expect(meta.mitre.atlas.length).toBeGreaterThan(0);
203
+ });
204
+ it('does not duplicate techniques when re-annotating', () => {
205
+ const findings = [makeFinding({ ruleId: 'INJ-001' })];
206
+ annotateFindingsWithMitreAtlas(findings);
207
+ annotateFindingsWithMitreAtlas(findings); // re-annotate
208
+ const meta = findings[0].metadata;
209
+ const ids = meta.mitre.atlas.map(t => t.id);
210
+ const uniqueIds = new Set(ids);
211
+ expect(ids.length).toBe(uniqueIds.size);
212
+ });
213
+ it('skips findings with no matching techniques', () => {
214
+ const findings = [makeFinding({ ruleId: 'UNKNOWN-999', category: 'backdoors' })];
215
+ const original = { ...findings[0] };
216
+ annotateFindingsWithMitreAtlas(findings);
217
+ // If no techniques mapped, metadata might not be set
218
+ // At minimum, should not throw
219
+ expect(findings[0]).toBeDefined();
220
+ void original; // silence unused warning
221
+ });
222
+ it('returns the same array reference', () => {
223
+ const findings = [makeFinding()];
224
+ const result = annotateFindingsWithMitreAtlas(findings);
225
+ expect(result).toBe(findings);
226
+ });
227
+ });
228
+ // ---------------------------------------------------------------------------
229
+ // getMitreAtlasTechniqueCatalogSummary
230
+ // ---------------------------------------------------------------------------
231
+ describe('getMitreAtlasTechniqueCatalogSummary', () => {
232
+ beforeEach(() => {
233
+ setMitreAtlasTechniqueCatalog(null);
234
+ });
235
+ it('returns a non-empty summary string', () => {
236
+ const summary = getMitreAtlasTechniqueCatalogSummary();
237
+ expect(typeof summary).toBe('string');
238
+ expect(summary.length).toBeGreaterThan(0);
239
+ });
240
+ it('each line follows "AML.T####: Name" format', () => {
241
+ const lines = getMitreAtlasTechniqueCatalogSummary().split('\n');
242
+ for (const line of lines) {
243
+ expect(line).toMatch(/^AML\.T\d{4}/);
244
+ }
245
+ });
246
+ it('respects maxTechniques limit', () => {
247
+ const lines = getMitreAtlasTechniqueCatalogSummary(3).split('\n');
248
+ expect(lines.length).toBeLessThanOrEqual(3);
249
+ });
250
+ it('returns empty string when maxTechniques is 0', () => {
251
+ const summary = getMitreAtlasTechniqueCatalogSummary(0);
252
+ expect(summary).toBe('');
253
+ });
254
+ it('uses dynamic catalog when set', () => {
255
+ const dynamic = {
256
+ 'AML.T9999': {
257
+ id: 'AML.T9999',
258
+ name: 'My Custom Technique',
259
+ url: 'https://example.com',
260
+ tactics: ['test'],
261
+ },
262
+ };
263
+ setMitreAtlasTechniqueCatalog(dynamic);
264
+ const summary = getMitreAtlasTechniqueCatalogSummary();
265
+ expect(summary).toContain('My Custom Technique');
266
+ });
267
+ });
268
+ // ---------------------------------------------------------------------------
269
+ // getRelevantMitreAtlasTechniqueCatalogSummary
270
+ // ---------------------------------------------------------------------------
271
+ describe('getRelevantMitreAtlasTechniqueCatalogSummary', () => {
272
+ beforeEach(() => {
273
+ setMitreAtlasTechniqueCatalog(null);
274
+ });
275
+ it('returns techniques relevant to query text', () => {
276
+ const summary = getRelevantMitreAtlasTechniqueCatalogSummary('prompt injection jailbreak');
277
+ expect(summary.length).toBeGreaterThan(0);
278
+ // Should contain injection-related techniques
279
+ expect(summary).toContain('AML.T0051');
280
+ });
281
+ it('falls back to catalog summary when query is empty', () => {
282
+ const withQuery = getRelevantMitreAtlasTechniqueCatalogSummary('', 5);
283
+ const full = getMitreAtlasTechniqueCatalogSummary(5);
284
+ expect(withQuery).toBe(full);
285
+ });
286
+ it('respects maxTechniques limit', () => {
287
+ const summary = getRelevantMitreAtlasTechniqueCatalogSummary('injection', 3);
288
+ const lines = summary.split('\n');
289
+ expect(lines.length).toBeLessThanOrEqual(3);
290
+ });
291
+ it('returns empty when maxTechniques is 0', () => {
292
+ const summary = getRelevantMitreAtlasTechniqueCatalogSummary('anything', 0);
293
+ expect(summary).toBe('');
294
+ });
295
+ it('uses full catalog summary when no techniques score positively', () => {
296
+ const summary = getRelevantMitreAtlasTechniqueCatalogSummary('xyznonmatchingtext', 5);
297
+ // Falls back to catalog summary since no tokens match
298
+ expect(summary.length).toBeGreaterThan(0);
299
+ });
300
+ });
301
+ // ---------------------------------------------------------------------------
302
+ // MITRE_ATLAS_TECHNIQUES constant
303
+ // ---------------------------------------------------------------------------
304
+ describe('MITRE_ATLAS_TECHNIQUES', () => {
305
+ it('contains expected techniques', () => {
306
+ expect(MITRE_ATLAS_TECHNIQUES['AML.T0051']).toBeDefined();
307
+ expect(MITRE_ATLAS_TECHNIQUES['AML.T0054']).toBeDefined();
308
+ expect(MITRE_ATLAS_TECHNIQUES['AML.T0083']).toBeDefined();
309
+ });
310
+ it('all entries have required fields', () => {
311
+ for (const [id, tech] of Object.entries(MITRE_ATLAS_TECHNIQUES)) {
312
+ expect(tech.id).toBe(id);
313
+ expect(typeof tech.name).toBe('string');
314
+ expect(typeof tech.url).toBe('string');
315
+ expect(Array.isArray(tech.tactics)).toBe(true);
316
+ }
317
+ });
318
+ });
319
+ //# sourceMappingURL=atlas.test.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MITRE ATLAS Catalog Loader Tests
3
+ * Tests for parsing the STIX bundle and loadMitreAtlasTechniqueCatalog.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=atlasCatalog.test.d.ts.map