ferret-scan 1.0.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 +51 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/bin/ferret.js +822 -0
- package/dist/__tests__/basic.test.d.ts +6 -0
- package/dist/__tests__/basic.test.js +80 -0
- package/dist/analyzers/AstAnalyzer.d.ts +30 -0
- package/dist/analyzers/AstAnalyzer.js +332 -0
- package/dist/analyzers/CorrelationAnalyzer.d.ts +21 -0
- package/dist/analyzers/CorrelationAnalyzer.js +288 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/intelligence/IndicatorMatcher.d.ts +50 -0
- package/dist/intelligence/IndicatorMatcher.js +285 -0
- package/dist/intelligence/ThreatFeed.d.ts +99 -0
- package/dist/intelligence/ThreatFeed.js +296 -0
- package/dist/remediation/Fixer.d.ts +71 -0
- package/dist/remediation/Fixer.js +391 -0
- package/dist/remediation/Quarantine.d.ts +102 -0
- package/dist/remediation/Quarantine.js +329 -0
- package/dist/reporters/ConsoleReporter.d.ts +13 -0
- package/dist/reporters/ConsoleReporter.js +185 -0
- package/dist/reporters/HtmlReporter.d.ts +25 -0
- package/dist/reporters/HtmlReporter.js +604 -0
- package/dist/reporters/SarifReporter.d.ts +86 -0
- package/dist/reporters/SarifReporter.js +117 -0
- package/dist/rules/ai-specific.d.ts +8 -0
- package/dist/rules/ai-specific.js +221 -0
- package/dist/rules/backdoors.d.ts +8 -0
- package/dist/rules/backdoors.js +134 -0
- package/dist/rules/correlationRules.d.ts +8 -0
- package/dist/rules/correlationRules.js +227 -0
- package/dist/rules/credentials.d.ts +8 -0
- package/dist/rules/credentials.js +194 -0
- package/dist/rules/exfiltration.d.ts +8 -0
- package/dist/rules/exfiltration.js +139 -0
- package/dist/rules/index.d.ts +51 -0
- package/dist/rules/index.js +97 -0
- package/dist/rules/injection.d.ts +8 -0
- package/dist/rules/injection.js +136 -0
- package/dist/rules/obfuscation.d.ts +8 -0
- package/dist/rules/obfuscation.js +159 -0
- package/dist/rules/permissions.d.ts +8 -0
- package/dist/rules/permissions.js +129 -0
- package/dist/rules/persistence.d.ts +8 -0
- package/dist/rules/persistence.js +117 -0
- package/dist/rules/semanticRules.d.ts +10 -0
- package/dist/rules/semanticRules.js +212 -0
- package/dist/rules/supply-chain.d.ts +8 -0
- package/dist/rules/supply-chain.js +148 -0
- package/dist/scanner/FileDiscovery.d.ts +24 -0
- package/dist/scanner/FileDiscovery.js +282 -0
- package/dist/scanner/PatternMatcher.d.ts +25 -0
- package/dist/scanner/PatternMatcher.js +206 -0
- package/dist/scanner/Scanner.d.ts +14 -0
- package/dist/scanner/Scanner.js +266 -0
- package/dist/scanner/WatchMode.d.ts +29 -0
- package/dist/scanner/WatchMode.js +195 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.js +53 -0
- package/dist/utils/baseline.d.ts +80 -0
- package/dist/utils/baseline.js +276 -0
- package/dist/utils/config.d.ts +21 -0
- package/dist/utils/config.js +247 -0
- package/dist/utils/ignore.d.ts +18 -0
- package/dist/utils/ignore.js +82 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +75 -0
- package/package.json +119 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation Analyzer - Cross-file attack pattern detection
|
|
3
|
+
* Detects sophisticated attack patterns that span multiple configuration files
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, relative } from 'node:path';
|
|
7
|
+
import logger from '../utils/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Build file relationship map based on directory proximity and naming patterns
|
|
10
|
+
*/
|
|
11
|
+
function buildFileRelationships(files) {
|
|
12
|
+
const relationships = [];
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const related = [];
|
|
15
|
+
const fileDir = dirname(file.path);
|
|
16
|
+
for (const otherFile of files) {
|
|
17
|
+
if (file.path === otherFile.path)
|
|
18
|
+
continue;
|
|
19
|
+
const otherDir = dirname(otherFile.path);
|
|
20
|
+
const distance = calculateDirectoryDistance(fileDir, otherDir);
|
|
21
|
+
// Consider files related if they're in same directory or close proximity
|
|
22
|
+
if (distance <= 2) {
|
|
23
|
+
related.push(otherFile);
|
|
24
|
+
}
|
|
25
|
+
// Also consider files related by naming patterns
|
|
26
|
+
if (areFilesRelatedByNaming(file, otherFile)) {
|
|
27
|
+
related.push(otherFile);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
relationships.push({
|
|
31
|
+
file,
|
|
32
|
+
relatedFiles: [...new Set(related)], // Remove duplicates
|
|
33
|
+
distance: 0
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return relationships;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculate directory distance between two paths
|
|
40
|
+
*/
|
|
41
|
+
function calculateDirectoryDistance(path1, path2) {
|
|
42
|
+
const rel = relative(path1, path2);
|
|
43
|
+
if (rel === '')
|
|
44
|
+
return 0;
|
|
45
|
+
const parts = rel.split('/').filter(p => p && p !== '.');
|
|
46
|
+
return parts.length;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if files are related by naming patterns
|
|
50
|
+
*/
|
|
51
|
+
function areFilesRelatedByNaming(file1, file2) {
|
|
52
|
+
const name1 = file1.relativePath.toLowerCase();
|
|
53
|
+
const name2 = file2.relativePath.toLowerCase();
|
|
54
|
+
// Claude-specific relationships
|
|
55
|
+
const patterns = [
|
|
56
|
+
// hooks and skills
|
|
57
|
+
{ pattern1: /hooks?\//, pattern2: /skills?\/|agents?\// },
|
|
58
|
+
// settings and configs
|
|
59
|
+
{ pattern1: /settings\.json/, pattern2: /config\./ },
|
|
60
|
+
// claude.md and related configs
|
|
61
|
+
{ pattern1: /claude\.md/, pattern2: /\.mcp\.json|settings\.json/ },
|
|
62
|
+
// agent and skill relationships
|
|
63
|
+
{ pattern1: /agent/, pattern2: /skill/ },
|
|
64
|
+
// security-related files
|
|
65
|
+
{ pattern1: /security|auth/, pattern2: /permission|access/ },
|
|
66
|
+
];
|
|
67
|
+
for (const { pattern1, pattern2 } of patterns) {
|
|
68
|
+
if ((pattern1.test(name1) && pattern2.test(name2)) ||
|
|
69
|
+
(pattern2.test(name1) && pattern1.test(name2))) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Find cross-file patterns
|
|
77
|
+
*/
|
|
78
|
+
function findCrossFilePatterns(relationships, correlationRules) {
|
|
79
|
+
const matches = [];
|
|
80
|
+
for (const rule of correlationRules) {
|
|
81
|
+
logger.debug(`Checking correlation rule: ${rule.id}`);
|
|
82
|
+
for (const relationship of relationships) {
|
|
83
|
+
const allFiles = [relationship.file, ...relationship.relatedFiles];
|
|
84
|
+
// Check if rule file patterns match
|
|
85
|
+
const matchingFiles = allFiles.filter(file => rule.filePatterns.some(pattern => file.relativePath.toLowerCase().includes(pattern.toLowerCase())));
|
|
86
|
+
if (matchingFiles.length < 2)
|
|
87
|
+
continue; // Need at least 2 files for correlation
|
|
88
|
+
// Find content patterns across files
|
|
89
|
+
const contentMatches = findContentPatternsAcrossFiles(matchingFiles, rule.contentPatterns);
|
|
90
|
+
if (contentMatches.length >= rule.contentPatterns.length) {
|
|
91
|
+
const strength = calculateCorrelationStrength(contentMatches, rule);
|
|
92
|
+
matches.push({
|
|
93
|
+
rule,
|
|
94
|
+
files: matchingFiles,
|
|
95
|
+
patterns: contentMatches,
|
|
96
|
+
strength
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return matches;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Find content patterns across multiple files
|
|
105
|
+
*/
|
|
106
|
+
function findContentPatternsAcrossFiles(files, patterns) {
|
|
107
|
+
const matches = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
try {
|
|
110
|
+
const content = readFileSync(file.path, 'utf-8');
|
|
111
|
+
const lines = content.split('\n');
|
|
112
|
+
for (const pattern of patterns) {
|
|
113
|
+
const regex = new RegExp(pattern, 'gi');
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const line = lines[i] ?? '';
|
|
116
|
+
const match = regex.exec(line);
|
|
117
|
+
if (match) {
|
|
118
|
+
matches.push({
|
|
119
|
+
file,
|
|
120
|
+
pattern,
|
|
121
|
+
line: i + 1,
|
|
122
|
+
match: match[0]
|
|
123
|
+
});
|
|
124
|
+
regex.lastIndex = 0; // Reset regex for next iteration
|
|
125
|
+
break; // One match per pattern per file is enough
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
logger.warn(`Error reading file ${file.relativePath} for correlation analysis: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return matches;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Calculate correlation strength based on pattern matches
|
|
138
|
+
*/
|
|
139
|
+
function calculateCorrelationStrength(matches, rule) {
|
|
140
|
+
// Base strength from pattern coverage
|
|
141
|
+
const patternCoverage = matches.length / rule.contentPatterns.length;
|
|
142
|
+
// Bonus for multiple files involvement
|
|
143
|
+
const uniqueFiles = new Set(matches.map(m => m.file.path)).size;
|
|
144
|
+
const fileBonus = Math.min(uniqueFiles / rule.filePatterns.length, 1) * 0.2;
|
|
145
|
+
// Bonus for proximity (if files are close together)
|
|
146
|
+
const proximityBonus = 0.1;
|
|
147
|
+
return Math.min(patternCoverage + fileBonus + proximityBonus, 1);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Create context lines for correlation finding
|
|
151
|
+
*/
|
|
152
|
+
function createCorrelationContext(file, lineNumber) {
|
|
153
|
+
try {
|
|
154
|
+
const content = readFileSync(file.path, 'utf-8');
|
|
155
|
+
const lines = content.split('\n');
|
|
156
|
+
const contextLines = 3;
|
|
157
|
+
const start = Math.max(0, lineNumber - contextLines - 1);
|
|
158
|
+
const end = Math.min(lines.length, lineNumber + contextLines);
|
|
159
|
+
const context = [];
|
|
160
|
+
for (let i = start; i < end; i++) {
|
|
161
|
+
context.push({
|
|
162
|
+
lineNumber: i + 1,
|
|
163
|
+
content: lines[i] ?? '',
|
|
164
|
+
isMatch: i === lineNumber - 1
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return context;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
logger.warn(`Error creating context for ${file.relativePath}:${lineNumber}`);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Convert cross-file matches to correlation findings
|
|
176
|
+
*/
|
|
177
|
+
function createCorrelationFindings(matches, parentRule) {
|
|
178
|
+
const findings = [];
|
|
179
|
+
for (const match of matches) {
|
|
180
|
+
// Create a finding for the primary pattern
|
|
181
|
+
const primaryPattern = match.patterns[0];
|
|
182
|
+
if (!primaryPattern)
|
|
183
|
+
continue;
|
|
184
|
+
const context = createCorrelationContext(primaryPattern.file, primaryPattern.line);
|
|
185
|
+
const riskVectors = generateRiskVectors(match);
|
|
186
|
+
const finding = {
|
|
187
|
+
ruleId: parentRule.id,
|
|
188
|
+
ruleName: parentRule.name,
|
|
189
|
+
severity: parentRule.severity,
|
|
190
|
+
category: parentRule.category,
|
|
191
|
+
file: primaryPattern.file.path,
|
|
192
|
+
relativePath: primaryPattern.file.relativePath,
|
|
193
|
+
line: primaryPattern.line,
|
|
194
|
+
match: primaryPattern.match,
|
|
195
|
+
context,
|
|
196
|
+
remediation: parentRule.remediation,
|
|
197
|
+
metadata: {
|
|
198
|
+
correlationRule: match.rule,
|
|
199
|
+
relatedPatterns: match.patterns,
|
|
200
|
+
totalFiles: match.files.length
|
|
201
|
+
},
|
|
202
|
+
timestamp: new Date(),
|
|
203
|
+
riskScore: Math.round(match.strength * 100),
|
|
204
|
+
// Correlation-specific fields
|
|
205
|
+
relatedFiles: match.files.map(f => f.relativePath),
|
|
206
|
+
attackPattern: match.rule.description,
|
|
207
|
+
riskVectors,
|
|
208
|
+
correlationStrength: match.strength
|
|
209
|
+
};
|
|
210
|
+
findings.push(finding);
|
|
211
|
+
}
|
|
212
|
+
return findings;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Generate risk vectors for a cross-file match
|
|
216
|
+
*/
|
|
217
|
+
function generateRiskVectors(match) {
|
|
218
|
+
const vectors = [];
|
|
219
|
+
// Analyze the attack pattern
|
|
220
|
+
const description = match.rule.description.toLowerCase();
|
|
221
|
+
if (description.includes('credential') || description.includes('secret')) {
|
|
222
|
+
vectors.push('Credential Exposure');
|
|
223
|
+
}
|
|
224
|
+
if (description.includes('network') || description.includes('transmission')) {
|
|
225
|
+
vectors.push('Data Exfiltration');
|
|
226
|
+
}
|
|
227
|
+
if (description.includes('permission') || description.includes('escalation')) {
|
|
228
|
+
vectors.push('Privilege Escalation');
|
|
229
|
+
}
|
|
230
|
+
if (description.includes('backdoor') || description.includes('persistence')) {
|
|
231
|
+
vectors.push('Persistence Mechanism');
|
|
232
|
+
}
|
|
233
|
+
if (description.includes('obfuscation') || description.includes('hiding')) {
|
|
234
|
+
vectors.push('Steganography/Hiding');
|
|
235
|
+
}
|
|
236
|
+
// Add file-type specific vectors
|
|
237
|
+
const fileTypes = match.files.map(f => f.component);
|
|
238
|
+
if (fileTypes.includes('hook') && fileTypes.includes('skill')) {
|
|
239
|
+
vectors.push('Hook-Skill Chain');
|
|
240
|
+
}
|
|
241
|
+
if (fileTypes.includes('settings') && fileTypes.includes('ai-config-md')) {
|
|
242
|
+
vectors.push('Configuration Tampering');
|
|
243
|
+
}
|
|
244
|
+
return vectors.length > 0 ? vectors : ['Cross-File Coordination'];
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Analyze files for cross-file correlation patterns
|
|
248
|
+
*/
|
|
249
|
+
export function analyzeCorrelations(files, rules) {
|
|
250
|
+
const findings = [];
|
|
251
|
+
try {
|
|
252
|
+
// Get rules with correlation patterns
|
|
253
|
+
const correlationRules = rules
|
|
254
|
+
.filter(rule => rule.correlationRules && rule.correlationRules.length > 0)
|
|
255
|
+
.flatMap(rule => rule.correlationRules.map(corrRule => ({ ...corrRule, parentRule: rule })));
|
|
256
|
+
if (correlationRules.length === 0 || files.length < 2) {
|
|
257
|
+
return findings;
|
|
258
|
+
}
|
|
259
|
+
logger.debug(`Cross-file correlation analysis with ${correlationRules.length} rules across ${files.length} files`);
|
|
260
|
+
// Build file relationships
|
|
261
|
+
const relationships = buildFileRelationships(files);
|
|
262
|
+
// Find cross-file patterns
|
|
263
|
+
const matches = findCrossFilePatterns(relationships, correlationRules.map(r => ({ ...r, parentRule: undefined })));
|
|
264
|
+
// Convert to findings
|
|
265
|
+
for (const match of matches) {
|
|
266
|
+
const parentRule = correlationRules.find(r => r.id === match.rule.id)?.parentRule;
|
|
267
|
+
if (parentRule) {
|
|
268
|
+
const correlationFindings = createCorrelationFindings([match], parentRule);
|
|
269
|
+
findings.push(...correlationFindings);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
logger.error(`Error in cross-file correlation analysis: ${error instanceof Error ? error.message : String(error)}`);
|
|
275
|
+
}
|
|
276
|
+
return findings;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Check if correlation analysis should be performed
|
|
280
|
+
*/
|
|
281
|
+
export function shouldAnalyzeCorrelations(files, config) {
|
|
282
|
+
return config.correlationAnalysis && files.length >= 2;
|
|
283
|
+
}
|
|
284
|
+
export default {
|
|
285
|
+
analyzeCorrelations,
|
|
286
|
+
shouldAnalyzeCorrelations
|
|
287
|
+
};
|
|
288
|
+
//# sourceMappingURL=CorrelationAnalyzer.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ferret-Scan - Security scanner for AI CLI configurations
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
export type { Severity, ThreatCategory, ComponentType, FileType, Rule, Finding, ContextLine, DiscoveredFile, ScanResult, ScanSummary, ScanError, ScannerConfig, OutputFormat, CliOptions, ConfigFile, } from './types.js';
|
|
7
|
+
export { DEFAULT_CONFIG, SEVERITY_WEIGHTS, SEVERITY_ORDER } from './types.js';
|
|
8
|
+
export { scan, getExitCode } from './scanner/Scanner.js';
|
|
9
|
+
export { discoverFiles } from './scanner/FileDiscovery.js';
|
|
10
|
+
export { matchRules, matchRule, createPatternMatcher } from './scanner/PatternMatcher.js';
|
|
11
|
+
export { getAllRules, getRulesByCategories, getRulesBySeverity, getRuleById, getEnabledRules, getRulesForScan, getRuleStats, } from './rules/index.js';
|
|
12
|
+
export { generateConsoleReport } from './reporters/ConsoleReporter.js';
|
|
13
|
+
export { loadConfig, getAIConfigPaths } from './utils/config.js';
|
|
14
|
+
export { getClaudeConfigPaths } from './utils/config.js';
|
|
15
|
+
export { createIgnoreFilter, shouldIgnore } from './utils/ignore.js';
|
|
16
|
+
export { logger } from './utils/logger.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ferret-Scan - Security scanner for AI CLI configurations
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
export { DEFAULT_CONFIG, SEVERITY_WEIGHTS, SEVERITY_ORDER } from './types.js';
|
|
7
|
+
// Scanner
|
|
8
|
+
export { scan, getExitCode } from './scanner/Scanner.js';
|
|
9
|
+
export { discoverFiles } from './scanner/FileDiscovery.js';
|
|
10
|
+
export { matchRules, matchRule, createPatternMatcher } from './scanner/PatternMatcher.js';
|
|
11
|
+
// Rules
|
|
12
|
+
export { getAllRules, getRulesByCategories, getRulesBySeverity, getRuleById, getEnabledRules, getRulesForScan, getRuleStats, } from './rules/index.js';
|
|
13
|
+
// Reporters
|
|
14
|
+
export { generateConsoleReport } from './reporters/ConsoleReporter.js';
|
|
15
|
+
// Utils
|
|
16
|
+
export { loadConfig, getAIConfigPaths } from './utils/config.js';
|
|
17
|
+
// Re-export deprecated for backwards compatibility
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
19
|
+
export { getClaudeConfigPaths } from './utils/config.js';
|
|
20
|
+
export { createIgnoreFilter, shouldIgnore } from './utils/ignore.js';
|
|
21
|
+
export { logger } from './utils/logger.js';
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indicator Matcher - Matches content against threat intelligence indicators
|
|
3
|
+
* Provides fast lookup and matching of IoCs against scanned content
|
|
4
|
+
*/
|
|
5
|
+
import type { ThreatDatabase, ThreatIndicator, IndicatorType } from './ThreatFeed.js';
|
|
6
|
+
import type { Finding, DiscoveredFile } from '../types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Threat intelligence finding
|
|
9
|
+
*/
|
|
10
|
+
export interface ThreatIntelFinding extends Finding {
|
|
11
|
+
/** Matched threat indicator */
|
|
12
|
+
threatIndicator: ThreatIndicator;
|
|
13
|
+
/** Match confidence (0-100) */
|
|
14
|
+
matchConfidence: number;
|
|
15
|
+
/** Additional threat context */
|
|
16
|
+
threatContext: {
|
|
17
|
+
indicatorType: IndicatorType;
|
|
18
|
+
threatSource: string;
|
|
19
|
+
firstSeen: string;
|
|
20
|
+
lastSeen: string;
|
|
21
|
+
threatTags: string[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Matcher configuration
|
|
26
|
+
*/
|
|
27
|
+
interface MatcherConfig {
|
|
28
|
+
/** Minimum confidence threshold for matches */
|
|
29
|
+
minConfidence: number;
|
|
30
|
+
/** Whether to match patterns (can be expensive) */
|
|
31
|
+
enablePatternMatching: boolean;
|
|
32
|
+
/** Maximum number of matches per file */
|
|
33
|
+
maxMatchesPerFile: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Match all indicators against content
|
|
37
|
+
*/
|
|
38
|
+
export declare function matchIndicators(db: ThreatDatabase, file: DiscoveredFile, content: string, config?: Partial<MatcherConfig>): ThreatIntelFinding[];
|
|
39
|
+
/**
|
|
40
|
+
* Check if threat intelligence matching should be performed
|
|
41
|
+
*/
|
|
42
|
+
export declare function shouldMatchIndicators(_file: DiscoveredFile, config: {
|
|
43
|
+
threatIntel: boolean;
|
|
44
|
+
}): boolean;
|
|
45
|
+
declare const _default: {
|
|
46
|
+
matchIndicators: typeof matchIndicators;
|
|
47
|
+
shouldMatchIndicators: typeof shouldMatchIndicators;
|
|
48
|
+
};
|
|
49
|
+
export default _default;
|
|
50
|
+
//# sourceMappingURL=IndicatorMatcher.d.ts.map
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Indicator Matcher - Matches content against threat intelligence indicators
|
|
3
|
+
* Provides fast lookup and matching of IoCs against scanned content
|
|
4
|
+
*/
|
|
5
|
+
import logger from '../utils/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Default matcher configuration
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
minConfidence: 50,
|
|
11
|
+
enablePatternMatching: true,
|
|
12
|
+
maxMatchesPerFile: 100
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Pre-compiled pattern cache for performance
|
|
16
|
+
*/
|
|
17
|
+
const patternCache = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Get or create compiled regex pattern
|
|
20
|
+
*/
|
|
21
|
+
function getCompiledPattern(pattern) {
|
|
22
|
+
if (!patternCache.has(pattern)) {
|
|
23
|
+
try {
|
|
24
|
+
const regex = new RegExp(pattern, 'gi');
|
|
25
|
+
patternCache.set(pattern, regex);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
logger.warn(`Invalid regex pattern: ${pattern}`);
|
|
29
|
+
// Return a regex that never matches
|
|
30
|
+
patternCache.set(pattern, /(?!.*)/);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return patternCache.get(pattern);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create context lines for threat intel finding
|
|
37
|
+
*/
|
|
38
|
+
function createThreatContext(_file, content, line, contextLines = 3) {
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
const start = Math.max(0, line - contextLines);
|
|
41
|
+
const end = Math.min(lines.length, line + contextLines + 1);
|
|
42
|
+
const context = [];
|
|
43
|
+
for (let i = start; i < end; i++) {
|
|
44
|
+
context.push({
|
|
45
|
+
lineNumber: i + 1,
|
|
46
|
+
content: lines[i] ?? '',
|
|
47
|
+
isMatch: i === line
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return context;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Calculate match confidence based on context and indicator confidence
|
|
54
|
+
*/
|
|
55
|
+
function calculateMatchConfidence(indicator, matchContext) {
|
|
56
|
+
let confidence = indicator.confidence;
|
|
57
|
+
// Boost confidence for exact matches
|
|
58
|
+
if (matchContext.exactMatch) {
|
|
59
|
+
confidence = Math.min(100, confidence + 10);
|
|
60
|
+
}
|
|
61
|
+
// Adjust based on file context
|
|
62
|
+
if (matchContext.component === 'hook' || matchContext.component === 'skill') {
|
|
63
|
+
confidence = Math.min(100, confidence + 5);
|
|
64
|
+
}
|
|
65
|
+
// Adjust based on file type relevance
|
|
66
|
+
if (indicator.type === 'package' && matchContext.fileType === 'json') {
|
|
67
|
+
confidence = Math.min(100, confidence + 5);
|
|
68
|
+
}
|
|
69
|
+
return confidence;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Match domain indicators in content
|
|
73
|
+
*/
|
|
74
|
+
function matchDomains(content, indicators, file, config) {
|
|
75
|
+
const findings = [];
|
|
76
|
+
const lines = content.split('\n');
|
|
77
|
+
for (const indicator of indicators) {
|
|
78
|
+
if (indicator.type !== 'domain')
|
|
79
|
+
continue;
|
|
80
|
+
const domain = indicator.value;
|
|
81
|
+
const regex = new RegExp(`\\b${domain.replace('.', '\\.')}\\b`, 'gi');
|
|
82
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
83
|
+
const line = lines[lineIndex] ?? '';
|
|
84
|
+
const match = regex.test(line);
|
|
85
|
+
if (match && findings.length < config.maxMatchesPerFile) {
|
|
86
|
+
const confidence = calculateMatchConfidence(indicator, {
|
|
87
|
+
exactMatch: true,
|
|
88
|
+
fileType: file.type,
|
|
89
|
+
component: file.component
|
|
90
|
+
});
|
|
91
|
+
if (confidence >= config.minConfidence) {
|
|
92
|
+
findings.push(createThreatIntelFinding(indicator, file, content, lineIndex + 1, domain, confidence));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Match package indicators in content
|
|
101
|
+
*/
|
|
102
|
+
function matchPackages(content, indicators, file, config) {
|
|
103
|
+
const findings = [];
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
for (const indicator of indicators) {
|
|
106
|
+
if (indicator.type !== 'package')
|
|
107
|
+
continue;
|
|
108
|
+
const packageName = indicator.value;
|
|
109
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
110
|
+
const line = lines[lineIndex] ?? '';
|
|
111
|
+
// Simple string matching for package names
|
|
112
|
+
if (line.toLowerCase().includes(packageName.toLowerCase()) &&
|
|
113
|
+
findings.length < config.maxMatchesPerFile) {
|
|
114
|
+
const confidence = calculateMatchConfidence(indicator, {
|
|
115
|
+
exactMatch: true,
|
|
116
|
+
fileType: file.type,
|
|
117
|
+
component: file.component
|
|
118
|
+
});
|
|
119
|
+
if (confidence >= config.minConfidence) {
|
|
120
|
+
findings.push(createThreatIntelFinding(indicator, file, content, lineIndex + 1, packageName, confidence));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return findings;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Match pattern indicators in content
|
|
129
|
+
*/
|
|
130
|
+
function matchPatterns(content, indicators, file, config) {
|
|
131
|
+
if (!config.enablePatternMatching) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const findings = [];
|
|
135
|
+
const lines = content.split('\n');
|
|
136
|
+
for (const indicator of indicators) {
|
|
137
|
+
if (indicator.type !== 'pattern')
|
|
138
|
+
continue;
|
|
139
|
+
const regex = getCompiledPattern(indicator.value);
|
|
140
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
141
|
+
const line = lines[lineIndex] ?? '';
|
|
142
|
+
const match = regex.test(line);
|
|
143
|
+
if (match && findings.length < config.maxMatchesPerFile) {
|
|
144
|
+
const confidence = calculateMatchConfidence(indicator, {
|
|
145
|
+
exactMatch: false,
|
|
146
|
+
fileType: file.type,
|
|
147
|
+
component: file.component
|
|
148
|
+
});
|
|
149
|
+
if (confidence >= config.minConfidence) {
|
|
150
|
+
findings.push(createThreatIntelFinding(indicator, file, content, lineIndex + 1, indicator.value, confidence));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return findings;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Match hash indicators in content
|
|
159
|
+
*/
|
|
160
|
+
function matchHashes(content, indicators, file, config) {
|
|
161
|
+
const findings = [];
|
|
162
|
+
for (const indicator of indicators) {
|
|
163
|
+
if (indicator.type !== 'hash')
|
|
164
|
+
continue;
|
|
165
|
+
const hash = indicator.value.toLowerCase();
|
|
166
|
+
// Simple hash matching
|
|
167
|
+
if (content.toLowerCase().includes(hash)) {
|
|
168
|
+
const lines = content.split('\n');
|
|
169
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
170
|
+
const line = lines[lineIndex] ?? '';
|
|
171
|
+
if (line.toLowerCase().includes(hash) && findings.length < config.maxMatchesPerFile) {
|
|
172
|
+
const confidence = calculateMatchConfidence(indicator, {
|
|
173
|
+
exactMatch: true,
|
|
174
|
+
fileType: file.type,
|
|
175
|
+
component: file.component
|
|
176
|
+
});
|
|
177
|
+
if (confidence >= config.minConfidence) {
|
|
178
|
+
findings.push(createThreatIntelFinding(indicator, file, content, lineIndex + 1, hash, confidence));
|
|
179
|
+
}
|
|
180
|
+
break; // Only match once per file
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Create threat intelligence finding
|
|
189
|
+
*/
|
|
190
|
+
function createThreatIntelFinding(indicator, file, content, line, match, confidence) {
|
|
191
|
+
return {
|
|
192
|
+
ruleId: `THREAT-${indicator.type.toUpperCase()}-${indicator.source.toUpperCase().replace(/-/g, '_')}`,
|
|
193
|
+
ruleName: `Threat Intelligence: ${indicator.description}`,
|
|
194
|
+
severity: indicator.severity === 'critical' ? 'CRITICAL' :
|
|
195
|
+
indicator.severity === 'high' ? 'HIGH' :
|
|
196
|
+
indicator.severity === 'medium' ? 'MEDIUM' : 'LOW',
|
|
197
|
+
category: 'behavioral',
|
|
198
|
+
file: file.path,
|
|
199
|
+
relativePath: file.relativePath,
|
|
200
|
+
line,
|
|
201
|
+
match,
|
|
202
|
+
context: createThreatContext(file, content, line - 1),
|
|
203
|
+
remediation: `Review and remove this ${indicator.type} indicator. ${indicator.description}`,
|
|
204
|
+
metadata: {
|
|
205
|
+
threatIntelligence: true,
|
|
206
|
+
indicator,
|
|
207
|
+
threatSource: indicator.source,
|
|
208
|
+
threatTags: indicator.tags
|
|
209
|
+
},
|
|
210
|
+
timestamp: new Date(),
|
|
211
|
+
riskScore: confidence,
|
|
212
|
+
// Threat-specific fields
|
|
213
|
+
threatIndicator: indicator,
|
|
214
|
+
matchConfidence: confidence,
|
|
215
|
+
threatContext: {
|
|
216
|
+
indicatorType: indicator.type,
|
|
217
|
+
threatSource: indicator.source,
|
|
218
|
+
firstSeen: indicator.firstSeen,
|
|
219
|
+
lastSeen: indicator.lastSeen,
|
|
220
|
+
threatTags: indicator.tags
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Match all indicators against content
|
|
226
|
+
*/
|
|
227
|
+
export function matchIndicators(db, file, content, config = {}) {
|
|
228
|
+
const matcherConfig = { ...DEFAULT_CONFIG, ...config };
|
|
229
|
+
const findings = [];
|
|
230
|
+
try {
|
|
231
|
+
logger.debug(`Matching threat indicators against ${file.relativePath}`);
|
|
232
|
+
// Get enabled indicators with minimum confidence
|
|
233
|
+
const eligibleIndicators = db.indicators.filter(indicator => indicator.confidence >= matcherConfig.minConfidence);
|
|
234
|
+
if (eligibleIndicators.length === 0) {
|
|
235
|
+
return findings;
|
|
236
|
+
}
|
|
237
|
+
// Group indicators by type for efficient matching
|
|
238
|
+
const indicatorsByType = new Map();
|
|
239
|
+
for (const indicator of eligibleIndicators) {
|
|
240
|
+
if (!indicatorsByType.has(indicator.type)) {
|
|
241
|
+
indicatorsByType.set(indicator.type, []);
|
|
242
|
+
}
|
|
243
|
+
indicatorsByType.get(indicator.type).push(indicator);
|
|
244
|
+
}
|
|
245
|
+
// Match each indicator type
|
|
246
|
+
for (const [type, indicators] of indicatorsByType) {
|
|
247
|
+
switch (type) {
|
|
248
|
+
case 'domain':
|
|
249
|
+
case 'url':
|
|
250
|
+
findings.push(...matchDomains(content, indicators, file, matcherConfig));
|
|
251
|
+
break;
|
|
252
|
+
case 'package':
|
|
253
|
+
findings.push(...matchPackages(content, indicators, file, matcherConfig));
|
|
254
|
+
break;
|
|
255
|
+
case 'pattern':
|
|
256
|
+
findings.push(...matchPatterns(content, indicators, file, matcherConfig));
|
|
257
|
+
break;
|
|
258
|
+
case 'hash':
|
|
259
|
+
findings.push(...matchHashes(content, indicators, file, matcherConfig));
|
|
260
|
+
break;
|
|
261
|
+
// Additional types can be added here
|
|
262
|
+
}
|
|
263
|
+
// Respect max matches limit
|
|
264
|
+
if (findings.length >= matcherConfig.maxMatchesPerFile) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
logger.debug(`Found ${findings.length} threat intelligence matches in ${file.relativePath}`);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
logger.error(`Error matching threat indicators in ${file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
}
|
|
273
|
+
return findings;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Check if threat intelligence matching should be performed
|
|
277
|
+
*/
|
|
278
|
+
export function shouldMatchIndicators(_file, config) {
|
|
279
|
+
return config.threatIntel;
|
|
280
|
+
}
|
|
281
|
+
export default {
|
|
282
|
+
matchIndicators,
|
|
283
|
+
shouldMatchIndicators
|
|
284
|
+
};
|
|
285
|
+
//# sourceMappingURL=IndicatorMatcher.js.map
|