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,25 @@
1
+ /**
2
+ * PatternMatcher - Regex-based pattern matching engine
3
+ * Applies security rules to file content
4
+ */
5
+ import type { Rule, Finding, DiscoveredFile } from '../types.js';
6
+ interface MatchOptions {
7
+ contextLines: number;
8
+ }
9
+ /**
10
+ * Match a single rule against file content
11
+ */
12
+ export declare function matchRule(rule: Rule, file: DiscoveredFile, content: string, options: MatchOptions): Finding[];
13
+ /**
14
+ * Match all rules against file content
15
+ */
16
+ export declare function matchRules(rules: Rule[], file: DiscoveredFile, content: string, options: MatchOptions): Finding[];
17
+ /**
18
+ * Create a PatternMatcher instance
19
+ */
20
+ export declare function createPatternMatcher(options: MatchOptions): {
21
+ matchRule: (rule: Rule, file: DiscoveredFile, content: string) => Finding[];
22
+ matchRules: (rules: Rule[], file: DiscoveredFile, content: string) => Finding[];
23
+ };
24
+ export default createPatternMatcher;
25
+ //# sourceMappingURL=PatternMatcher.d.ts.map
@@ -0,0 +1,206 @@
1
+ /**
2
+ * PatternMatcher - Regex-based pattern matching engine
3
+ * Applies security rules to file content
4
+ */
5
+ import { SEVERITY_WEIGHTS } from '../types.js';
6
+ import logger from '../utils/logger.js';
7
+ /**
8
+ * Split content into lines
9
+ */
10
+ function splitLines(content) {
11
+ return content.split(/\r?\n/);
12
+ }
13
+ /**
14
+ * Find line number and column for a given offset
15
+ */
16
+ function getLineAndColumn(content, offset) {
17
+ const lines = content.slice(0, offset).split(/\r?\n/);
18
+ const line = lines.length;
19
+ const column = (lines[lines.length - 1]?.length ?? 0) + 1;
20
+ return { line, column };
21
+ }
22
+ /**
23
+ * Get context lines around a match
24
+ */
25
+ function getContext(lines, matchLine, contextCount) {
26
+ const context = [];
27
+ const startLine = Math.max(0, matchLine - contextCount - 1);
28
+ const endLine = Math.min(lines.length, matchLine + contextCount);
29
+ for (let i = startLine; i < endLine; i++) {
30
+ context.push({
31
+ lineNumber: i + 1,
32
+ content: lines[i] ?? '',
33
+ isMatch: i === matchLine - 1,
34
+ });
35
+ }
36
+ return context;
37
+ }
38
+ /**
39
+ * Calculate risk score based on severity and context
40
+ */
41
+ function calculateRiskScore(severity, matchCount, fileComponent) {
42
+ let score = SEVERITY_WEIGHTS[severity];
43
+ // Multiply by match count (diminishing returns)
44
+ if (matchCount > 1) {
45
+ score = Math.min(100, score + Math.log2(matchCount) * 10);
46
+ }
47
+ // Increase score for high-risk components
48
+ const highRiskComponents = ['hook', 'plugin', 'mcp'];
49
+ if (highRiskComponents.includes(fileComponent)) {
50
+ score = Math.min(100, score * 1.2);
51
+ }
52
+ return Math.round(score);
53
+ }
54
+ /**
55
+ * Find all pattern matches in content using global regex search
56
+ */
57
+ function findMatches(content, patterns) {
58
+ const matches = [];
59
+ for (const pattern of patterns) {
60
+ // Create a new regex with global flag
61
+ const globalPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
62
+ let match;
63
+ while ((match = globalPattern.exec(content)) !== null) {
64
+ const { line, column } = getLineAndColumn(content, match.index);
65
+ matches.push({
66
+ pattern,
67
+ match,
68
+ lineNumber: line,
69
+ column,
70
+ });
71
+ }
72
+ }
73
+ return matches;
74
+ }
75
+ /**
76
+ * Check if a match should be excluded based on rule filters
77
+ */
78
+ function shouldExcludeMatch(rule, matchText, lineContent, contextLines) {
79
+ // Check minimum match length
80
+ if (rule.minMatchLength && matchText.length < rule.minMatchLength) {
81
+ return true;
82
+ }
83
+ // Check exclude patterns (false positive filters)
84
+ if (rule.excludePatterns) {
85
+ for (const excludePattern of rule.excludePatterns) {
86
+ if (excludePattern.test(lineContent)) {
87
+ logger.debug(`[${rule.id}] Excluded by excludePattern: ${lineContent.slice(0, 50)}`);
88
+ return true;
89
+ }
90
+ }
91
+ }
92
+ // Check exclude context (documentation indicators)
93
+ if (rule.excludeContext) {
94
+ const fullContext = contextLines.join('\n');
95
+ for (const excludeCtx of rule.excludeContext) {
96
+ if (excludeCtx.test(fullContext)) {
97
+ logger.debug(`[${rule.id}] Excluded by excludeContext`);
98
+ return true;
99
+ }
100
+ }
101
+ }
102
+ // Check require context (must be present)
103
+ if (rule.requireContext && rule.requireContext.length > 0) {
104
+ const fullContext = contextLines.join('\n');
105
+ let hasRequiredContext = false;
106
+ for (const reqCtx of rule.requireContext) {
107
+ if (reqCtx.test(fullContext)) {
108
+ hasRequiredContext = true;
109
+ break;
110
+ }
111
+ }
112
+ if (!hasRequiredContext) {
113
+ logger.debug(`[${rule.id}] Missing required context`);
114
+ return true;
115
+ }
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Check if a rule applies to a file
121
+ */
122
+ function ruleApplies(rule, file) {
123
+ // Check file type
124
+ if (!rule.fileTypes.includes(file.type)) {
125
+ return false;
126
+ }
127
+ // Check component type
128
+ if (!rule.components.includes(file.component)) {
129
+ return false;
130
+ }
131
+ return true;
132
+ }
133
+ /**
134
+ * Match a single rule against file content
135
+ */
136
+ export function matchRule(rule, file, content, options) {
137
+ if (!ruleApplies(rule, file)) {
138
+ return [];
139
+ }
140
+ const findings = [];
141
+ const lines = splitLines(content);
142
+ const matches = findMatches(content, rule.patterns);
143
+ // Group matches by line to avoid duplicates
144
+ const matchesByLine = new Map();
145
+ for (const match of matches) {
146
+ const existing = matchesByLine.get(match.lineNumber) ?? [];
147
+ existing.push(match);
148
+ matchesByLine.set(match.lineNumber, existing);
149
+ }
150
+ for (const [lineNumber, lineMatches] of matchesByLine) {
151
+ const firstMatch = lineMatches[0];
152
+ if (!firstMatch)
153
+ continue;
154
+ const matchText = firstMatch.match[0];
155
+ const lineContent = lines[lineNumber - 1] ?? '';
156
+ const contextForCheck = getContext(lines, lineNumber, options.contextLines);
157
+ const contextStrings = contextForCheck.map(c => c.content);
158
+ // Check if this match should be excluded (false positive filter)
159
+ if (shouldExcludeMatch(rule, matchText, lineContent, contextStrings)) {
160
+ continue;
161
+ }
162
+ const finding = {
163
+ ruleId: rule.id,
164
+ ruleName: rule.name,
165
+ severity: rule.severity,
166
+ category: rule.category,
167
+ file: file.path,
168
+ relativePath: file.relativePath,
169
+ line: lineNumber,
170
+ column: firstMatch.column,
171
+ match: matchText,
172
+ context: contextForCheck,
173
+ remediation: rule.remediation,
174
+ timestamp: new Date(),
175
+ riskScore: calculateRiskScore(rule.severity, lineMatches.length, file.component),
176
+ };
177
+ findings.push(finding);
178
+ logger.debug(`[${rule.id}] Found in ${file.relativePath}:${lineNumber}: ${matchText.slice(0, 50)}`);
179
+ }
180
+ return findings;
181
+ }
182
+ /**
183
+ * Match all rules against file content
184
+ */
185
+ export function matchRules(rules, file, content, options) {
186
+ const findings = [];
187
+ for (const rule of rules) {
188
+ if (!rule.enabled) {
189
+ continue;
190
+ }
191
+ const ruleFindings = matchRule(rule, file, content, options);
192
+ findings.push(...ruleFindings);
193
+ }
194
+ return findings;
195
+ }
196
+ /**
197
+ * Create a PatternMatcher instance
198
+ */
199
+ export function createPatternMatcher(options) {
200
+ return {
201
+ matchRule: (rule, file, content) => matchRule(rule, file, content, options),
202
+ matchRules: (rules, file, content) => matchRules(rules, file, content, options),
203
+ };
204
+ }
205
+ export default createPatternMatcher;
206
+ //# sourceMappingURL=PatternMatcher.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Scanner - Core orchestrator for Ferret security scanning
3
+ */
4
+ import type { ScannerConfig, ScanResult } from '../types.js';
5
+ /**
6
+ * Main scan function
7
+ */
8
+ export declare function scan(config: ScannerConfig): Promise<ScanResult>;
9
+ /**
10
+ * Determine exit code based on findings and config
11
+ */
12
+ export declare function getExitCode(result: ScanResult, config: ScannerConfig): number;
13
+ export default scan;
14
+ //# sourceMappingURL=Scanner.d.ts.map
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Scanner - Core orchestrator for Ferret security scanning
3
+ */
4
+ import { readFileSync } from 'node:fs';
5
+ import { SEVERITY_ORDER, SEVERITY_WEIGHTS } from '../types.js';
6
+ import { discoverFiles } from './FileDiscovery.js';
7
+ import { matchRules } from './PatternMatcher.js';
8
+ import { getRulesForScan } from '../rules/index.js';
9
+ import { analyzeFile as analyzeFileSemantics, shouldAnalyze as shouldAnalyzeSemantics, getMemoryUsage } from '../analyzers/AstAnalyzer.js';
10
+ import { analyzeCorrelations, shouldAnalyzeCorrelations } from '../analyzers/CorrelationAnalyzer.js';
11
+ import { loadThreatDatabase } from '../intelligence/ThreatFeed.js';
12
+ import { matchIndicators, shouldMatchIndicators } from '../intelligence/IndicatorMatcher.js';
13
+ import logger from '../utils/logger.js';
14
+ /**
15
+ * Create an empty scan summary
16
+ */
17
+ function createEmptySummary() {
18
+ return {
19
+ critical: 0,
20
+ high: 0,
21
+ medium: 0,
22
+ low: 0,
23
+ info: 0,
24
+ total: 0,
25
+ };
26
+ }
27
+ /**
28
+ * Calculate overall risk score from findings
29
+ */
30
+ function calculateOverallRiskScore(findings) {
31
+ if (findings.length === 0)
32
+ return 0;
33
+ const totalWeight = findings.reduce((sum, finding) => {
34
+ return sum + SEVERITY_WEIGHTS[finding.severity];
35
+ }, 0);
36
+ // Normalize to 0-100 scale with diminishing returns
37
+ const normalizedScore = Math.min(100, Math.log1p(totalWeight) * 15);
38
+ return Math.round(normalizedScore);
39
+ }
40
+ /**
41
+ * Group findings by severity
42
+ */
43
+ function groupBySeverity(findings) {
44
+ const grouped = {
45
+ CRITICAL: [],
46
+ HIGH: [],
47
+ MEDIUM: [],
48
+ LOW: [],
49
+ INFO: [],
50
+ };
51
+ for (const finding of findings) {
52
+ grouped[finding.severity].push(finding);
53
+ }
54
+ return grouped;
55
+ }
56
+ /**
57
+ * Group findings by category
58
+ */
59
+ function groupByCategory(findings) {
60
+ const grouped = {};
61
+ for (const finding of findings) {
62
+ grouped[finding.category] ??= [];
63
+ grouped[finding.category].push(finding);
64
+ }
65
+ return grouped;
66
+ }
67
+ /**
68
+ * Calculate summary from findings
69
+ */
70
+ function calculateSummary(findings) {
71
+ const summary = createEmptySummary();
72
+ for (const finding of findings) {
73
+ switch (finding.severity) {
74
+ case 'CRITICAL':
75
+ summary.critical++;
76
+ break;
77
+ case 'HIGH':
78
+ summary.high++;
79
+ break;
80
+ case 'MEDIUM':
81
+ summary.medium++;
82
+ break;
83
+ case 'LOW':
84
+ summary.low++;
85
+ break;
86
+ case 'INFO':
87
+ summary.info++;
88
+ break;
89
+ }
90
+ summary.total++;
91
+ }
92
+ return summary;
93
+ }
94
+ /**
95
+ * Sort findings by severity (most severe first)
96
+ */
97
+ function sortFindings(findings) {
98
+ return findings.sort((a, b) => {
99
+ const severityDiff = SEVERITY_ORDER.indexOf(a.severity) - SEVERITY_ORDER.indexOf(b.severity);
100
+ if (severityDiff !== 0)
101
+ return severityDiff;
102
+ // Then by risk score
103
+ if (a.riskScore !== b.riskScore)
104
+ return b.riskScore - a.riskScore;
105
+ // Then by file
106
+ return a.relativePath.localeCompare(b.relativePath);
107
+ });
108
+ }
109
+ /**
110
+ * Scan a single file
111
+ */
112
+ function scanFile(file, config) {
113
+ try {
114
+ const content = readFileSync(file.path, 'utf-8');
115
+ const rules = getRulesForScan(config.categories, config.severities);
116
+ const allFindings = [];
117
+ // Regular pattern matching
118
+ const patternFindings = matchRules(rules, file, content, {
119
+ contextLines: config.contextLines,
120
+ });
121
+ allFindings.push(...patternFindings);
122
+ // Semantic analysis if enabled and applicable
123
+ if (config.semanticAnalysis && shouldAnalyzeSemantics(file, config)) {
124
+ // Monitor memory usage
125
+ const memBefore = getMemoryUsage();
126
+ if (memBefore.used > 1000) { // More than 1GB used
127
+ logger.warn(`High memory usage (${memBefore.used}MB) - skipping semantic analysis for ${file.relativePath}`);
128
+ }
129
+ else {
130
+ try {
131
+ logger.debug(`Running semantic analysis on ${file.relativePath}`);
132
+ const semanticFindings = analyzeFileSemantics(file, content, rules);
133
+ // Convert SemanticFinding to Finding for compatibility
134
+ allFindings.push(...semanticFindings);
135
+ const memAfter = getMemoryUsage();
136
+ logger.debug(`Semantic analysis memory: ${memAfter.used - memBefore.used}MB delta`);
137
+ }
138
+ catch (semanticError) {
139
+ const semanticMessage = semanticError instanceof Error ? semanticError.message : String(semanticError);
140
+ logger.warn(`Semantic analysis error for ${file.relativePath}: ${semanticMessage}`);
141
+ }
142
+ }
143
+ }
144
+ // Threat intelligence matching if enabled
145
+ if (config.threatIntel && shouldMatchIndicators(file, config)) {
146
+ try {
147
+ const threatDB = loadThreatDatabase();
148
+ logger.debug(`Running threat intelligence matching on ${file.relativePath}`);
149
+ const threatFindings = matchIndicators(threatDB, file, content, {
150
+ minConfidence: 50,
151
+ enablePatternMatching: true,
152
+ maxMatchesPerFile: 50
153
+ });
154
+ allFindings.push(...threatFindings);
155
+ logger.debug(`Found ${threatFindings.length} threat intelligence matches`);
156
+ }
157
+ catch (threatError) {
158
+ const threatMessage = threatError instanceof Error ? threatError.message : String(threatError);
159
+ logger.warn(`Threat intelligence error for ${file.relativePath}: ${threatMessage}`);
160
+ }
161
+ }
162
+ return { findings: allFindings };
163
+ }
164
+ catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ logger.warn(`Error scanning ${file.relativePath}: ${message}`);
167
+ return { findings: [], error: message };
168
+ }
169
+ }
170
+ /**
171
+ * Main scan function
172
+ */
173
+ export async function scan(config) {
174
+ const startTime = new Date();
175
+ const allFindings = [];
176
+ const errors = [];
177
+ logger.info(`Starting scan of ${config.paths.length} path(s)`);
178
+ // Discover files
179
+ const discovery = discoverFiles(config.paths, {
180
+ maxFileSize: config.maxFileSize,
181
+ ignore: config.ignore,
182
+ });
183
+ // Add discovery errors
184
+ for (const error of discovery.errors) {
185
+ errors.push({
186
+ file: error.path,
187
+ message: error.error,
188
+ fatal: false,
189
+ });
190
+ }
191
+ if (discovery.files.length === 0) {
192
+ logger.warn('No files found to scan');
193
+ }
194
+ // Scan each file
195
+ for (const file of discovery.files) {
196
+ logger.debug(`Scanning: ${file.relativePath}`);
197
+ const result = scanFile(file, config);
198
+ if (result.error) {
199
+ errors.push({
200
+ file: file.path,
201
+ message: result.error,
202
+ fatal: false,
203
+ });
204
+ }
205
+ allFindings.push(...result.findings);
206
+ }
207
+ // Cross-file correlation analysis if enabled
208
+ if (shouldAnalyzeCorrelations(discovery.files, config)) {
209
+ try {
210
+ logger.debug('Running cross-file correlation analysis');
211
+ const correlationFindings = analyzeCorrelations(discovery.files, getRulesForScan(config.categories, config.severities));
212
+ allFindings.push(...correlationFindings);
213
+ logger.debug(`Found ${correlationFindings.length} correlation findings`);
214
+ }
215
+ catch (correlationError) {
216
+ const correlationMessage = correlationError instanceof Error ? correlationError.message : String(correlationError);
217
+ logger.warn(`Correlation analysis error: ${correlationMessage}`);
218
+ errors.push({
219
+ message: `Correlation analysis failed: ${correlationMessage}`,
220
+ fatal: false,
221
+ });
222
+ }
223
+ }
224
+ // Sort findings
225
+ const sortedFindings = sortFindings(allFindings);
226
+ const endTime = new Date();
227
+ const duration = endTime.getTime() - startTime.getTime();
228
+ const result = {
229
+ success: true,
230
+ startTime,
231
+ endTime,
232
+ duration,
233
+ scannedPaths: config.paths,
234
+ totalFiles: discovery.files.length + discovery.skipped,
235
+ analyzedFiles: discovery.files.length,
236
+ skippedFiles: discovery.skipped,
237
+ findings: sortedFindings,
238
+ findingsBySeverity: groupBySeverity(sortedFindings),
239
+ findingsByCategory: groupByCategory(sortedFindings),
240
+ overallRiskScore: calculateOverallRiskScore(sortedFindings),
241
+ summary: calculateSummary(sortedFindings),
242
+ errors,
243
+ };
244
+ logger.info(`Scan complete: ${result.summary.total} findings in ${result.analyzedFiles} files (${duration}ms)`);
245
+ return result;
246
+ }
247
+ /**
248
+ * Determine exit code based on findings and config
249
+ */
250
+ export function getExitCode(result, config) {
251
+ if (!result.success)
252
+ return 3; // Scanner error
253
+ const failOnIndex = SEVERITY_ORDER.indexOf(config.failOn);
254
+ // Check if any finding meets or exceeds the fail threshold
255
+ for (const severity of SEVERITY_ORDER.slice(0, failOnIndex + 1)) {
256
+ if (result.findingsBySeverity[severity].length > 0) {
257
+ // Critical findings always return 2
258
+ if (severity === 'CRITICAL')
259
+ return 2;
260
+ return 1;
261
+ }
262
+ }
263
+ return 0; // No findings at or above threshold
264
+ }
265
+ export default scan;
266
+ //# sourceMappingURL=Scanner.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * WatchMode - Real-time file watching and scanning
3
+ * Monitors files for changes and automatically rescans
4
+ */
5
+ import type { ScannerConfig } from '../types.js';
6
+ interface WatchOptions {
7
+ debounceMs: number;
8
+ batchChanges: boolean;
9
+ ignored: string[];
10
+ }
11
+ /**
12
+ * Start watching files and scanning on changes
13
+ */
14
+ export declare function startWatchMode(config: ScannerConfig, options?: Partial<WatchOptions>): Promise<() => void>;
15
+ /**
16
+ * Watch mode with enhanced console output
17
+ */
18
+ export declare function startEnhancedWatchMode(config: ScannerConfig, options?: Partial<WatchOptions>): Promise<() => void>;
19
+ /**
20
+ * Create a simple file change notifier
21
+ */
22
+ export declare function createChangeNotifier(paths: string[], callback: (changedFiles: string[]) => void, options?: Partial<WatchOptions>): () => void;
23
+ declare const _default: {
24
+ startWatchMode: typeof startWatchMode;
25
+ startEnhancedWatchMode: typeof startEnhancedWatchMode;
26
+ createChangeNotifier: typeof createChangeNotifier;
27
+ };
28
+ export default _default;
29
+ //# sourceMappingURL=WatchMode.d.ts.map