ferret-scan 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +15 -11
- package/bin/ferret.js +104 -8
- package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
- package/dist/__tests__/AgentMonitor.test.js +235 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
- package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
- package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
- package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
- package/dist/__tests__/IndicatorMatcher.test.js +245 -0
- package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
- package/dist/__tests__/MarketplaceScanner.test.js +212 -0
- package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
- package/dist/__tests__/RuleGenerator.test.js +207 -0
- package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
- package/dist/__tests__/ThreatFeed.test.js +359 -0
- package/dist/__tests__/WatchMode.test.d.ts +6 -0
- package/dist/__tests__/WatchMode.test.js +104 -0
- package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
- package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerFull.test.js +138 -0
- package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
- package/dist/__tests__/atlas.test.d.ts +6 -0
- package/dist/__tests__/atlas.test.js +319 -0
- package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalog.test.js +200 -0
- package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
- package/dist/__tests__/baseline.test.d.ts +6 -0
- package/dist/__tests__/baseline.test.js +321 -0
- package/dist/__tests__/baselineExtra.test.d.ts +6 -0
- package/dist/__tests__/baselineExtra.test.js +317 -0
- package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
- package/dist/__tests__/capabilityMapping.test.js +49 -0
- package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
- package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
- package/dist/__tests__/complianceExtra.test.d.ts +6 -0
- package/dist/__tests__/complianceExtra.test.js +121 -0
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/configLoader.test.d.ts +6 -0
- package/dist/__tests__/configLoader.test.js +225 -0
- package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
- package/dist/__tests__/configLoaderExtra.test.js +186 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
- package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
- package/dist/__tests__/customRules.extra.test.d.ts +6 -0
- package/dist/__tests__/customRules.extra.test.js +245 -0
- package/dist/__tests__/customRules.test.d.ts +7 -0
- package/dist/__tests__/customRules.test.js +347 -0
- package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
- package/dist/__tests__/dependencyRisk.test.js +248 -0
- package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
- package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
- package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
- package/dist/__tests__/featureExitCodes.test.js +332 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
- package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
- package/dist/__tests__/fixer.extra.test.d.ts +6 -0
- package/dist/__tests__/fixer.extra.test.js +135 -0
- package/dist/__tests__/fixerApply.test.d.ts +6 -0
- package/dist/__tests__/fixerApply.test.js +132 -0
- package/dist/__tests__/gitHooks.test.d.ts +7 -0
- package/dist/__tests__/gitHooks.test.js +188 -0
- package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
- package/dist/__tests__/htmlReporter.extra.test.js +126 -0
- package/dist/__tests__/interactiveTui.test.d.ts +6 -0
- package/dist/__tests__/interactiveTui.test.js +180 -0
- package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
- package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiMore.test.js +194 -0
- package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiSession.test.js +173 -0
- package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysis.test.js +229 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
- package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
- package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
- package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
- package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
- package/dist/__tests__/llmGroqTPM.test.js +89 -0
- package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
- package/dist/__tests__/llmProviderRetry.test.js +172 -0
- package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
- package/dist/__tests__/mcpValidator.extra.test.js +270 -0
- package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
- package/dist/__tests__/patternMatcherExtra.test.js +198 -0
- package/dist/__tests__/patternsCommon.test.d.ts +6 -0
- package/dist/__tests__/patternsCommon.test.js +107 -0
- package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
- package/dist/__tests__/policyEnforcement.test.js +510 -0
- package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
- package/dist/__tests__/quarantineExtra.test.js +214 -0
- package/dist/__tests__/redactionExtra.test.d.ts +6 -0
- package/dist/__tests__/redactionExtra.test.js +228 -0
- package/dist/__tests__/scanDiff.test.d.ts +7 -0
- package/dist/__tests__/scanDiff.test.js +266 -0
- package/dist/__tests__/scanFull.test.d.ts +6 -0
- package/dist/__tests__/scanFull.test.js +158 -0
- package/dist/__tests__/scannerDampening.test.d.ts +6 -0
- package/dist/__tests__/scannerDampening.test.js +160 -0
- package/dist/__tests__/scannerExtra.test.d.ts +6 -0
- package/dist/__tests__/scannerExtra.test.js +194 -0
- package/dist/__tests__/scannerMitre.test.d.ts +5 -0
- package/dist/__tests__/scannerMitre.test.js +141 -0
- package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
- package/dist/__tests__/scannerSSRF.test.js +149 -0
- package/dist/__tests__/schemas.test.d.ts +6 -0
- package/dist/__tests__/schemas.test.js +125 -0
- package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
- package/dist/__tests__/webhooks.extra.test.js +144 -0
- package/dist/__tests__/webhooks.test.d.ts +6 -0
- package/dist/__tests__/webhooks.test.js +154 -0
- package/dist/features/customRules.js +22 -29
- package/dist/features/mcpTrustScore.d.ts +17 -0
- package/dist/features/mcpTrustScore.js +74 -0
- package/dist/features/mcpValidator.d.ts +2 -0
- package/dist/features/mcpValidator.js +13 -0
- package/dist/features/policyEnforcement.d.ts +22 -22
- package/dist/intelligence/ThreatFeed.js +207 -62
- package/dist/remediation/Quarantine.js +24 -6
- package/dist/reporters/ConsoleReporter.js +10 -0
- package/dist/reporters/HtmlReporter.js +5 -0
- package/dist/reporters/SarifReporter.d.ts +1 -0
- package/dist/reporters/SarifReporter.js +1 -0
- package/dist/scanner/IAnalyzer.d.ts +19 -0
- package/dist/scanner/IAnalyzer.js +5 -0
- package/dist/scanner/Scanner.js +64 -125
- package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
- package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
- package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
- package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -1
- package/dist/utils/safeRegex.d.ts +12 -51
- package/dist/utils/safeRegex.js +45 -62
- package/dist/utils/schemas.d.ts +64 -64
- package/package.json +24 -18
|
@@ -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,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,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
|