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.
Files changed (69) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +21 -0
  3. package/README.md +416 -0
  4. package/bin/ferret.js +822 -0
  5. package/dist/__tests__/basic.test.d.ts +6 -0
  6. package/dist/__tests__/basic.test.js +80 -0
  7. package/dist/analyzers/AstAnalyzer.d.ts +30 -0
  8. package/dist/analyzers/AstAnalyzer.js +332 -0
  9. package/dist/analyzers/CorrelationAnalyzer.d.ts +21 -0
  10. package/dist/analyzers/CorrelationAnalyzer.js +288 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.js +22 -0
  13. package/dist/intelligence/IndicatorMatcher.d.ts +50 -0
  14. package/dist/intelligence/IndicatorMatcher.js +285 -0
  15. package/dist/intelligence/ThreatFeed.d.ts +99 -0
  16. package/dist/intelligence/ThreatFeed.js +296 -0
  17. package/dist/remediation/Fixer.d.ts +71 -0
  18. package/dist/remediation/Fixer.js +391 -0
  19. package/dist/remediation/Quarantine.d.ts +102 -0
  20. package/dist/remediation/Quarantine.js +329 -0
  21. package/dist/reporters/ConsoleReporter.d.ts +13 -0
  22. package/dist/reporters/ConsoleReporter.js +185 -0
  23. package/dist/reporters/HtmlReporter.d.ts +25 -0
  24. package/dist/reporters/HtmlReporter.js +604 -0
  25. package/dist/reporters/SarifReporter.d.ts +86 -0
  26. package/dist/reporters/SarifReporter.js +117 -0
  27. package/dist/rules/ai-specific.d.ts +8 -0
  28. package/dist/rules/ai-specific.js +221 -0
  29. package/dist/rules/backdoors.d.ts +8 -0
  30. package/dist/rules/backdoors.js +134 -0
  31. package/dist/rules/correlationRules.d.ts +8 -0
  32. package/dist/rules/correlationRules.js +227 -0
  33. package/dist/rules/credentials.d.ts +8 -0
  34. package/dist/rules/credentials.js +194 -0
  35. package/dist/rules/exfiltration.d.ts +8 -0
  36. package/dist/rules/exfiltration.js +139 -0
  37. package/dist/rules/index.d.ts +51 -0
  38. package/dist/rules/index.js +97 -0
  39. package/dist/rules/injection.d.ts +8 -0
  40. package/dist/rules/injection.js +136 -0
  41. package/dist/rules/obfuscation.d.ts +8 -0
  42. package/dist/rules/obfuscation.js +159 -0
  43. package/dist/rules/permissions.d.ts +8 -0
  44. package/dist/rules/permissions.js +129 -0
  45. package/dist/rules/persistence.d.ts +8 -0
  46. package/dist/rules/persistence.js +117 -0
  47. package/dist/rules/semanticRules.d.ts +10 -0
  48. package/dist/rules/semanticRules.js +212 -0
  49. package/dist/rules/supply-chain.d.ts +8 -0
  50. package/dist/rules/supply-chain.js +148 -0
  51. package/dist/scanner/FileDiscovery.d.ts +24 -0
  52. package/dist/scanner/FileDiscovery.js +282 -0
  53. package/dist/scanner/PatternMatcher.d.ts +25 -0
  54. package/dist/scanner/PatternMatcher.js +206 -0
  55. package/dist/scanner/Scanner.d.ts +14 -0
  56. package/dist/scanner/Scanner.js +266 -0
  57. package/dist/scanner/WatchMode.d.ts +29 -0
  58. package/dist/scanner/WatchMode.js +195 -0
  59. package/dist/types.d.ts +332 -0
  60. package/dist/types.js +53 -0
  61. package/dist/utils/baseline.d.ts +80 -0
  62. package/dist/utils/baseline.js +276 -0
  63. package/dist/utils/config.d.ts +21 -0
  64. package/dist/utils/config.js +247 -0
  65. package/dist/utils/ignore.d.ts +18 -0
  66. package/dist/utils/ignore.js +82 -0
  67. package/dist/utils/logger.d.ts +32 -0
  68. package/dist/utils/logger.js +75 -0
  69. 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
@@ -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