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,276 @@
1
+ /**
2
+ * Baseline Management - Track and ignore accepted findings
3
+ * Allows users to create baselines of known/accepted security findings
4
+ */
5
+ import { writeFileSync, readFileSync, existsSync } from 'node:fs';
6
+ import { resolve, dirname } from 'node:path';
7
+ import { mkdirSync } from 'node:fs';
8
+ import logger from './logger.js';
9
+ /**
10
+ * Generate a hash for a finding to uniquely identify it
11
+ */
12
+ function generateFindingHash(finding) {
13
+ const content = `${finding.ruleId}:${finding.relativePath}:${finding.line}:${finding.match}`;
14
+ // Simple hash function (could use crypto for better security)
15
+ let hash = 0;
16
+ for (let i = 0; i < content.length; i++) {
17
+ const char = content.charCodeAt(i);
18
+ hash = ((hash << 5) - hash) + char;
19
+ hash = hash & hash; // Convert to 32-bit integer
20
+ }
21
+ return Math.abs(hash).toString(36);
22
+ }
23
+ /**
24
+ * Load baseline from file
25
+ */
26
+ export function loadBaseline(baselinePath) {
27
+ try {
28
+ if (!existsSync(baselinePath)) {
29
+ return null;
30
+ }
31
+ const content = readFileSync(baselinePath, 'utf-8');
32
+ const baseline = JSON.parse(content);
33
+ // Validate baseline structure
34
+ if (!baseline.version || !baseline.findings || !Array.isArray(baseline.findings)) {
35
+ throw new Error('Invalid baseline format');
36
+ }
37
+ logger.debug(`Loaded baseline with ${baseline.findings.length} accepted findings`);
38
+ return baseline;
39
+ }
40
+ catch (error) {
41
+ logger.error(`Failed to load baseline from ${baselinePath}:`, error);
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Save baseline to file
47
+ */
48
+ export function saveBaseline(baseline, baselinePath) {
49
+ try {
50
+ // Ensure directory exists
51
+ const dir = dirname(baselinePath);
52
+ mkdirSync(dir, { recursive: true });
53
+ // Update lastUpdated timestamp
54
+ baseline.lastUpdated = new Date().toISOString();
55
+ // Write baseline file
56
+ const content = JSON.stringify(baseline, null, 2);
57
+ writeFileSync(baselinePath, content, 'utf-8');
58
+ logger.info(`Baseline saved to ${baselinePath} with ${baseline.findings.length} findings`);
59
+ }
60
+ catch (error) {
61
+ logger.error(`Failed to save baseline to ${baselinePath}:`, error);
62
+ throw error;
63
+ }
64
+ }
65
+ /**
66
+ * Create a new baseline from scan results
67
+ */
68
+ export function createBaseline(result, description) {
69
+ const now = new Date().toISOString();
70
+ const baselineFindings = result.findings.map(finding => ({
71
+ ruleId: finding.ruleId,
72
+ file: finding.relativePath,
73
+ line: finding.line,
74
+ match: finding.match,
75
+ hash: generateFindingHash(finding),
76
+ acceptedDate: now,
77
+ }));
78
+ return {
79
+ version: '1.0',
80
+ createdDate: now,
81
+ lastUpdated: now,
82
+ description: description ?? `Baseline created from scan of ${result.scannedPaths.join(', ')}`,
83
+ findings: baselineFindings,
84
+ };
85
+ }
86
+ /**
87
+ * Add findings to an existing baseline
88
+ */
89
+ export function addToBaseline(baseline, findings, reason) {
90
+ const now = new Date().toISOString();
91
+ const existingHashes = new Set(baseline.findings.map(f => f.hash));
92
+ const newFindings = findings
93
+ .filter(finding => {
94
+ const hash = generateFindingHash(finding);
95
+ return !existingHashes.has(hash);
96
+ })
97
+ .map(finding => ({
98
+ ruleId: finding.ruleId,
99
+ file: finding.relativePath,
100
+ line: finding.line,
101
+ match: finding.match,
102
+ hash: generateFindingHash(finding),
103
+ acceptedDate: now,
104
+ ...(reason && { reason }),
105
+ }));
106
+ logger.info(`Adding ${newFindings.length} new findings to baseline`);
107
+ return {
108
+ ...baseline,
109
+ lastUpdated: now,
110
+ findings: [...baseline.findings, ...newFindings],
111
+ };
112
+ }
113
+ /**
114
+ * Remove findings from baseline
115
+ */
116
+ export function removeFromBaseline(baseline, findingHashes) {
117
+ const hashSet = new Set(findingHashes);
118
+ const filteredFindings = baseline.findings.filter(f => !hashSet.has(f.hash));
119
+ const removedCount = baseline.findings.length - filteredFindings.length;
120
+ logger.info(`Removed ${removedCount} findings from baseline`);
121
+ return {
122
+ ...baseline,
123
+ lastUpdated: new Date().toISOString(),
124
+ findings: filteredFindings,
125
+ };
126
+ }
127
+ /**
128
+ * Filter scan results against baseline
129
+ */
130
+ export function filterAgainstBaseline(result, baseline) {
131
+ if (!baseline || baseline.findings.length === 0) {
132
+ return result; // No filtering needed
133
+ }
134
+ // Create hash set of baseline findings for fast lookup
135
+ const baselineHashes = new Set(baseline.findings.map(f => f.hash));
136
+ // Filter out findings that exist in baseline
137
+ const filteredFindings = result.findings.filter(finding => {
138
+ const hash = generateFindingHash(finding);
139
+ const isInBaseline = baselineHashes.has(hash);
140
+ if (isInBaseline) {
141
+ logger.debug(`Filtered baseline finding: ${finding.ruleId} in ${finding.relativePath}:${finding.line}`);
142
+ }
143
+ return !isInBaseline;
144
+ });
145
+ const filteredCount = result.findings.length - filteredFindings.length;
146
+ logger.info(`Filtered ${filteredCount} baseline findings, ${filteredFindings.length} new findings remain`);
147
+ // Recalculate summary and groupings
148
+ const newSummary = {
149
+ critical: 0,
150
+ high: 0,
151
+ medium: 0,
152
+ low: 0,
153
+ info: 0,
154
+ total: filteredFindings.length,
155
+ };
156
+ const newFindingsBySeverity = {
157
+ CRITICAL: [],
158
+ HIGH: [],
159
+ MEDIUM: [],
160
+ LOW: [],
161
+ INFO: [],
162
+ };
163
+ const newFindingsByCategory = {
164
+ injection: [],
165
+ credentials: [],
166
+ backdoors: [],
167
+ 'supply-chain': [],
168
+ permissions: [],
169
+ persistence: [],
170
+ obfuscation: [],
171
+ 'ai-specific': [],
172
+ 'advanced-hiding': [],
173
+ behavioral: [],
174
+ exfiltration: [],
175
+ };
176
+ for (const finding of filteredFindings) {
177
+ // Update summary
178
+ switch (finding.severity) {
179
+ case 'CRITICAL':
180
+ newSummary.critical++;
181
+ break;
182
+ case 'HIGH':
183
+ newSummary.high++;
184
+ break;
185
+ case 'MEDIUM':
186
+ newSummary.medium++;
187
+ break;
188
+ case 'LOW':
189
+ newSummary.low++;
190
+ break;
191
+ case 'INFO':
192
+ newSummary.info++;
193
+ break;
194
+ }
195
+ // Group by severity and category
196
+ newFindingsBySeverity[finding.severity]?.push(finding);
197
+ newFindingsByCategory[finding.category]?.push(finding);
198
+ }
199
+ return {
200
+ ...result,
201
+ findings: filteredFindings,
202
+ summary: newSummary,
203
+ findingsBySeverity: newFindingsBySeverity,
204
+ findingsByCategory: newFindingsByCategory,
205
+ };
206
+ }
207
+ /**
208
+ * Check if baseline findings are still valid
209
+ */
210
+ export function validateBaseline(baseline, currentResult) {
211
+ const currentHashes = new Set(currentResult.findings.map(finding => generateFindingHash(finding)));
212
+ const valid = [];
213
+ const invalid = [];
214
+ for (const baselineFinding of baseline.findings) {
215
+ if (currentHashes.has(baselineFinding.hash)) {
216
+ valid.push(baselineFinding);
217
+ }
218
+ else {
219
+ invalid.push(baselineFinding);
220
+ }
221
+ }
222
+ if (invalid.length > 0) {
223
+ logger.info(`Baseline validation: ${valid.length} valid, ${invalid.length} invalid findings`);
224
+ }
225
+ return { valid, invalid };
226
+ }
227
+ /**
228
+ * Get default baseline path for a project
229
+ */
230
+ export function getDefaultBaselinePath(scanPaths) {
231
+ // Try to find a good location for baseline file
232
+ const firstPath = scanPaths[0] ?? process.cwd();
233
+ return resolve(firstPath, '.ferret-baseline.json');
234
+ }
235
+ /**
236
+ * Baseline statistics
237
+ */
238
+ export function getBaselineStats(baseline) {
239
+ const byRule = {};
240
+ const bySeverity = {};
241
+ let oldestDate = new Date().toISOString();
242
+ let newestDate = '1970-01-01T00:00:00.000Z';
243
+ for (const finding of baseline.findings) {
244
+ // Count by rule
245
+ byRule[finding.ruleId] = (byRule[finding.ruleId] ?? 0) + 1;
246
+ // Extract severity from rule ID (if follows pattern like CRED-001)
247
+ const severity = finding.ruleId.split('-')[0] ?? 'UNKNOWN';
248
+ bySeverity[severity] = (bySeverity[severity] ?? 0) + 1;
249
+ // Track date range
250
+ if (finding.acceptedDate < oldestDate) {
251
+ oldestDate = finding.acceptedDate;
252
+ }
253
+ if (finding.acceptedDate > newestDate) {
254
+ newestDate = finding.acceptedDate;
255
+ }
256
+ }
257
+ return {
258
+ totalFindings: baseline.findings.length,
259
+ byRule,
260
+ bySeverity,
261
+ oldestFinding: oldestDate,
262
+ newestFinding: newestDate,
263
+ };
264
+ }
265
+ export default {
266
+ loadBaseline,
267
+ saveBaseline,
268
+ createBaseline,
269
+ addToBaseline,
270
+ removeFromBaseline,
271
+ filterAgainstBaseline,
272
+ validateBaseline,
273
+ getDefaultBaselinePath,
274
+ getBaselineStats,
275
+ };
276
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Configuration loader for Ferret-Scan
3
+ * Loads and merges configuration from files and CLI options
4
+ */
5
+ import type { ScannerConfig, CliOptions } from '../types.js';
6
+ /**
7
+ * Get AI CLI configuration paths
8
+ * Detects configurations for Claude Code, Cursor, Windsurf, Continue, Aider, Cline, and generic AI configs
9
+ */
10
+ export declare function getAIConfigPaths(): string[];
11
+ /**
12
+ * Get Claude Code configuration paths
13
+ * @deprecated Use getAIConfigPaths() instead
14
+ */
15
+ export declare function getClaudeConfigPaths(): string[];
16
+ /**
17
+ * Load and merge configuration from all sources
18
+ */
19
+ export declare function loadConfig(cliOptions: CliOptions): ScannerConfig;
20
+ export default loadConfig;
21
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Configuration loader for Ferret-Scan
3
+ * Loads and merges configuration from files and CLI options
4
+ */
5
+ import { readFileSync, existsSync } from 'node:fs';
6
+ import { resolve, dirname } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ import { DEFAULT_CONFIG } from '../types.js';
9
+ import logger from './logger.js';
10
+ const CONFIG_FILE_NAMES = [
11
+ '.ferretrc.json',
12
+ '.ferretrc',
13
+ 'ferret.config.json',
14
+ '.ferret/config.json',
15
+ ];
16
+ /**
17
+ * Find configuration file starting from a directory and walking up
18
+ */
19
+ function findConfigFile(startDir) {
20
+ let currentDir = resolve(startDir);
21
+ const root = dirname(currentDir);
22
+ while (currentDir !== root) {
23
+ for (const configName of CONFIG_FILE_NAMES) {
24
+ const configPath = resolve(currentDir, configName);
25
+ if (existsSync(configPath)) {
26
+ logger.debug(`Found config file: ${configPath}`);
27
+ return configPath;
28
+ }
29
+ }
30
+ currentDir = dirname(currentDir);
31
+ }
32
+ // Check home directory
33
+ const homeConfig = resolve(homedir(), '.ferretrc.json');
34
+ if (existsSync(homeConfig)) {
35
+ logger.debug(`Found home config: ${homeConfig}`);
36
+ return homeConfig;
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * Load configuration file
42
+ */
43
+ function loadConfigFile(configPath) {
44
+ try {
45
+ const content = readFileSync(configPath, 'utf-8');
46
+ const config = JSON.parse(content);
47
+ logger.debug(`Loaded config from: ${configPath}`);
48
+ return config;
49
+ }
50
+ catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ logger.warn(`Failed to load config file ${configPath}: ${message}`);
53
+ return {};
54
+ }
55
+ }
56
+ /**
57
+ * Parse severity string to array
58
+ */
59
+ function parseSeverities(severityStr) {
60
+ if (!severityStr)
61
+ return undefined;
62
+ const severities = severityStr.split(',').map(s => s.trim().toUpperCase());
63
+ const validSeverities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
64
+ return severities.filter(s => validSeverities.includes(s));
65
+ }
66
+ /**
67
+ * Parse categories string to array
68
+ */
69
+ function parseCategories(categoriesStr) {
70
+ if (!categoriesStr)
71
+ return undefined;
72
+ const categories = categoriesStr.split(',').map(c => c.trim().toLowerCase());
73
+ const validCategories = [
74
+ 'exfiltration', 'credentials', 'injection', 'backdoors',
75
+ 'supply-chain', 'permissions', 'persistence', 'obfuscation',
76
+ 'ai-specific', 'advanced-hiding', 'behavioral'
77
+ ];
78
+ return categories.filter(c => validCategories.includes(c));
79
+ }
80
+ /**
81
+ * AI CLI configuration directory patterns
82
+ * Supports multiple AI assistants and their config locations
83
+ */
84
+ const AI_CLI_PATTERNS = {
85
+ // Claude Code
86
+ claude: {
87
+ dirs: ['.claude'],
88
+ files: ['CLAUDE.md', '.mcp.json'],
89
+ },
90
+ // Cursor
91
+ cursor: {
92
+ dirs: ['.cursor'],
93
+ files: ['.cursorrules'],
94
+ },
95
+ // Windsurf
96
+ windsurf: {
97
+ dirs: ['.windsurf'],
98
+ files: ['.windsurfrules'],
99
+ },
100
+ // Continue
101
+ continue: {
102
+ dirs: ['.continue'],
103
+ files: [],
104
+ },
105
+ // Aider
106
+ aider: {
107
+ dirs: ['.aider'],
108
+ files: ['.aider.conf.yml', '.aiderignore'],
109
+ },
110
+ // Cline
111
+ cline: {
112
+ dirs: ['.cline'],
113
+ files: ['.clinerules'],
114
+ },
115
+ // Generic AI
116
+ generic: {
117
+ dirs: ['.ai'],
118
+ files: ['AI.md', 'AGENT.md', 'AGENTS.md'],
119
+ },
120
+ };
121
+ /**
122
+ * Get AI CLI configuration paths
123
+ * Detects configurations for Claude Code, Cursor, Windsurf, Continue, Aider, Cline, and generic AI configs
124
+ */
125
+ export function getAIConfigPaths() {
126
+ const paths = [];
127
+ const cwd = process.cwd();
128
+ const home = homedir();
129
+ // Check all AI CLI patterns
130
+ for (const cli of Object.values(AI_CLI_PATTERNS)) {
131
+ // Check directories (both global and project-level)
132
+ for (const dir of cli.dirs) {
133
+ const globalDir = resolve(home, dir);
134
+ if (existsSync(globalDir)) {
135
+ paths.push(globalDir);
136
+ }
137
+ const projectDir = resolve(cwd, dir);
138
+ if (existsSync(projectDir)) {
139
+ paths.push(projectDir);
140
+ }
141
+ }
142
+ // Check files (project-level only)
143
+ for (const file of cli.files) {
144
+ const projectFile = resolve(cwd, file);
145
+ if (existsSync(projectFile)) {
146
+ paths.push(projectFile);
147
+ }
148
+ }
149
+ }
150
+ return paths;
151
+ }
152
+ /**
153
+ * Get Claude Code configuration paths
154
+ * @deprecated Use getAIConfigPaths() instead
155
+ */
156
+ export function getClaudeConfigPaths() {
157
+ return getAIConfigPaths();
158
+ }
159
+ /**
160
+ * Load and merge configuration from all sources
161
+ */
162
+ export function loadConfig(cliOptions) {
163
+ // Start with defaults
164
+ const config = { ...DEFAULT_CONFIG };
165
+ // Load config file if exists
166
+ const configPath = cliOptions.config ?? findConfigFile(process.cwd());
167
+ if (configPath) {
168
+ const fileConfig = loadConfigFile(configPath);
169
+ // Merge file config
170
+ if (fileConfig.severity) {
171
+ config.severities = fileConfig.severity;
172
+ }
173
+ if (fileConfig.categories) {
174
+ config.categories = fileConfig.categories;
175
+ }
176
+ if (fileConfig.ignore) {
177
+ config.ignore = [...config.ignore, ...fileConfig.ignore];
178
+ }
179
+ if (fileConfig.customRules) {
180
+ config.customRules = fileConfig.customRules;
181
+ }
182
+ if (fileConfig.failOn) {
183
+ config.failOn = fileConfig.failOn;
184
+ }
185
+ if (fileConfig.aiDetection?.enabled !== undefined) {
186
+ config.aiDetection = fileConfig.aiDetection.enabled;
187
+ }
188
+ if (fileConfig.threatIntelligence?.enabled !== undefined) {
189
+ config.threatIntel = fileConfig.threatIntelligence.enabled;
190
+ }
191
+ if (fileConfig.behaviorAnalysis?.enabled !== undefined) {
192
+ config.behaviorAnalysis = fileConfig.behaviorAnalysis.enabled;
193
+ }
194
+ }
195
+ // Apply CLI options (highest priority)
196
+ if (cliOptions.path) {
197
+ config.paths = [resolve(cliOptions.path)];
198
+ }
199
+ else {
200
+ // Default to AI CLI config paths
201
+ config.paths = getAIConfigPaths();
202
+ }
203
+ const parsedSeverities = parseSeverities(cliOptions.severity);
204
+ if (parsedSeverities?.length) {
205
+ config.severities = parsedSeverities;
206
+ }
207
+ const parsedCategories = parseCategories(cliOptions.categories);
208
+ if (parsedCategories?.length) {
209
+ config.categories = parsedCategories;
210
+ }
211
+ if (cliOptions.failOn) {
212
+ config.failOn = cliOptions.failOn.toUpperCase();
213
+ }
214
+ if (cliOptions.format) {
215
+ config.format = cliOptions.format;
216
+ }
217
+ if (cliOptions.output) {
218
+ config.outputFile = cliOptions.output;
219
+ }
220
+ if (cliOptions.watch !== undefined) {
221
+ config.watch = cliOptions.watch;
222
+ }
223
+ if (cliOptions.ci !== undefined) {
224
+ config.ci = cliOptions.ci;
225
+ }
226
+ if (cliOptions.verbose !== undefined) {
227
+ config.verbose = cliOptions.verbose;
228
+ }
229
+ if (cliOptions.aiDetection !== undefined) {
230
+ config.aiDetection = cliOptions.aiDetection;
231
+ }
232
+ if (cliOptions.threatIntel !== undefined) {
233
+ config.threatIntel = cliOptions.threatIntel;
234
+ }
235
+ if (cliOptions.semanticAnalysis !== undefined) {
236
+ config.semanticAnalysis = cliOptions.semanticAnalysis;
237
+ }
238
+ if (cliOptions.correlationAnalysis !== undefined) {
239
+ config.correlationAnalysis = cliOptions.correlationAnalysis;
240
+ }
241
+ if (cliOptions.autoRemediation !== undefined) {
242
+ config.autoRemediation = cliOptions.autoRemediation;
243
+ }
244
+ return config;
245
+ }
246
+ export default loadConfig;
247
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ignore file parser for Ferret-Scan
3
+ * Handles .ferretignore files similar to .gitignore
4
+ */
5
+ export interface Ignore {
6
+ add(patterns: string | string[]): Ignore;
7
+ ignores(path: string): boolean;
8
+ }
9
+ /**
10
+ * Create an ignore filter instance
11
+ */
12
+ export declare function createIgnoreFilter(baseDir: string, additionalPatterns?: string[]): Ignore;
13
+ /**
14
+ * Check if a path should be ignored
15
+ */
16
+ export declare function shouldIgnore(ig: Ignore, filePath: string, baseDir: string): boolean;
17
+ export default createIgnoreFilter;
18
+ //# sourceMappingURL=ignore.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Ignore file parser for Ferret-Scan
3
+ * Handles .ferretignore files similar to .gitignore
4
+ */
5
+ import { readFileSync, existsSync } from 'node:fs';
6
+ import { resolve, dirname, relative } from 'node:path';
7
+ import { createRequire } from 'node:module';
8
+ const require = createRequire(import.meta.url);
9
+ const ignoreFactory = require('ignore');
10
+ import logger from './logger.js';
11
+ const IGNORE_FILE_NAMES = [
12
+ '.ferretignore',
13
+ '.ferret/ignore',
14
+ ];
15
+ /**
16
+ * Find and load ignore patterns from files
17
+ */
18
+ function findIgnoreFiles(startDir) {
19
+ const files = [];
20
+ let currentDir = resolve(startDir);
21
+ const root = dirname(currentDir);
22
+ while (currentDir !== root) {
23
+ for (const ignoreName of IGNORE_FILE_NAMES) {
24
+ const ignorePath = resolve(currentDir, ignoreName);
25
+ if (existsSync(ignorePath)) {
26
+ files.push(ignorePath);
27
+ }
28
+ }
29
+ currentDir = dirname(currentDir);
30
+ }
31
+ return files;
32
+ }
33
+ /**
34
+ * Load patterns from an ignore file
35
+ */
36
+ function loadIgnorePatterns(filePath) {
37
+ try {
38
+ const content = readFileSync(filePath, 'utf-8');
39
+ return content
40
+ .split('\n')
41
+ .map(line => line.trim())
42
+ .filter(line => line && !line.startsWith('#'));
43
+ }
44
+ catch (error) {
45
+ const message = error instanceof Error ? error.message : String(error);
46
+ logger.warn(`Failed to load ignore file ${filePath}: ${message}`);
47
+ return [];
48
+ }
49
+ }
50
+ /**
51
+ * Create an ignore filter instance
52
+ */
53
+ export function createIgnoreFilter(baseDir, additionalPatterns = []) {
54
+ const ig = ignoreFactory();
55
+ // Load patterns from ignore files
56
+ const ignoreFiles = findIgnoreFiles(baseDir);
57
+ for (const file of ignoreFiles) {
58
+ const patterns = loadIgnorePatterns(file);
59
+ logger.debug(`Loaded ${patterns.length} patterns from ${file}`);
60
+ ig.add(patterns);
61
+ }
62
+ // Add additional patterns (from config)
63
+ if (additionalPatterns.length > 0) {
64
+ ig.add(additionalPatterns);
65
+ }
66
+ // Always ignore certain patterns
67
+ ig.add([
68
+ '.git',
69
+ 'node_modules',
70
+ '.ferret-quarantine',
71
+ ]);
72
+ return ig;
73
+ }
74
+ /**
75
+ * Check if a path should be ignored
76
+ */
77
+ export function shouldIgnore(ig, filePath, baseDir) {
78
+ const relativePath = relative(baseDir, filePath);
79
+ return ig.ignores(relativePath);
80
+ }
81
+ export default createIgnoreFilter;
82
+ //# sourceMappingURL=ignore.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Logger utility for Ferret-Scan
3
+ * Provides consistent logging with levels and formatting
4
+ */
5
+ import type { Severity } from '../types.js';
6
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent';
7
+ interface LoggerConfig {
8
+ level: LogLevel;
9
+ verbose: boolean;
10
+ ci: boolean;
11
+ }
12
+ declare class Logger {
13
+ private config;
14
+ configure(config: Partial<LoggerConfig>): void;
15
+ private shouldLog;
16
+ private formatMessage;
17
+ debug(message: string, ...args: unknown[]): void;
18
+ info(message: string, ...args: unknown[]): void;
19
+ warn(message: string, ...args: unknown[]): void;
20
+ error(message: string, ...args: unknown[]): void;
21
+ /** Log without any formatting - for direct output */
22
+ raw(message: string): void;
23
+ /** Log finding with severity-appropriate formatting */
24
+ finding(severity: Severity, message: string): void;
25
+ /** Get current log level */
26
+ getLevel(): LogLevel;
27
+ /** Check if verbose mode is enabled */
28
+ isVerbose(): boolean;
29
+ }
30
+ export declare const logger: Logger;
31
+ export default logger;
32
+ //# sourceMappingURL=logger.d.ts.map