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,10 @@
1
+ /**
2
+ * Semantic Security Rules - AST-based detection patterns
3
+ * These rules use TypeScript AST analysis to detect complex security patterns
4
+ * that regular expressions cannot reliably identify.
5
+ * NOTE: These patterns are for DETECTION purposes only - not execution
6
+ */
7
+ import type { Rule } from '../types.js';
8
+ export declare const semanticRules: Rule[];
9
+ export default semanticRules;
10
+ //# sourceMappingURL=semanticRules.d.ts.map
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Semantic Security Rules - AST-based detection patterns
3
+ * These rules use TypeScript AST analysis to detect complex security patterns
4
+ * that regular expressions cannot reliably identify.
5
+ * NOTE: These patterns are for DETECTION purposes only - not execution
6
+ */
7
+ export const semanticRules = [
8
+ {
9
+ id: 'SEM-001',
10
+ name: 'Dynamic Code Execution Detection',
11
+ category: 'injection',
12
+ severity: 'CRITICAL',
13
+ description: 'Detects dynamic code execution patterns that could allow code injection',
14
+ patterns: [],
15
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
16
+ components: ['skill', 'agent', 'hook', 'plugin', 'ai-config-md'],
17
+ remediation: 'Avoid dynamic code execution. Use static imports, predefined functions, or safe templating instead.',
18
+ references: [
19
+ 'https://owasp.org/www-community/attacks/Code_Injection'
20
+ ],
21
+ enabled: true,
22
+ semanticPatterns: [
23
+ {
24
+ type: 'eval-chain',
25
+ pattern: 'Function',
26
+ confidence: 0.90
27
+ },
28
+ {
29
+ type: 'dynamic-import',
30
+ pattern: 'dynamic-import',
31
+ confidence: 0.85
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ id: 'SEM-002',
37
+ name: 'Process Execution Chain',
38
+ category: 'backdoors',
39
+ severity: 'HIGH',
40
+ description: 'Detects complex process execution chains that could be used for system compromise',
41
+ patterns: [],
42
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
43
+ components: ['skill', 'agent', 'hook', 'plugin'],
44
+ remediation: 'Review process execution chains for legitimacy. Use subprocess restrictions and input validation.',
45
+ references: [
46
+ 'https://owasp.org/www-project-top-ten/2021/A03_2021-Injection/'
47
+ ],
48
+ enabled: true,
49
+ semanticPatterns: [
50
+ {
51
+ type: 'function-call',
52
+ pattern: 'exec',
53
+ confidence: 0.85
54
+ },
55
+ {
56
+ type: 'function-call',
57
+ pattern: 'spawn',
58
+ confidence: 0.80
59
+ },
60
+ {
61
+ type: 'function-call',
62
+ pattern: 'execSync',
63
+ confidence: 0.90
64
+ },
65
+ {
66
+ type: 'property-access',
67
+ pattern: 'child_process',
68
+ confidence: 0.75
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ id: 'SEM-003',
74
+ name: 'File System Access Chain',
75
+ category: 'exfiltration',
76
+ severity: 'MEDIUM',
77
+ description: 'Detects complex file system access patterns that could indicate data exfiltration',
78
+ patterns: [],
79
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
80
+ components: ['skill', 'agent', 'hook', 'plugin'],
81
+ remediation: 'Review file system access patterns. Implement access controls and audit trails.',
82
+ references: [
83
+ 'https://attack.mitre.org/techniques/T1005/'
84
+ ],
85
+ enabled: true,
86
+ semanticPatterns: [
87
+ {
88
+ type: 'function-call',
89
+ pattern: 'readFile',
90
+ confidence: 0.60
91
+ },
92
+ {
93
+ type: 'function-call',
94
+ pattern: 'writeFile',
95
+ confidence: 0.70
96
+ },
97
+ {
98
+ type: 'property-access',
99
+ pattern: 'fs.',
100
+ confidence: 0.65
101
+ },
102
+ {
103
+ type: 'function-call',
104
+ pattern: 'createReadStream',
105
+ confidence: 0.75
106
+ }
107
+ ]
108
+ },
109
+ {
110
+ id: 'SEM-004',
111
+ name: 'Network Request Chain',
112
+ category: 'exfiltration',
113
+ severity: 'MEDIUM',
114
+ description: 'Detects complex network request patterns that could be used for data exfiltration',
115
+ patterns: [],
116
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
117
+ components: ['skill', 'agent', 'hook', 'plugin'],
118
+ remediation: 'Review network requests for legitimacy. Implement request filtering and monitoring.',
119
+ references: [
120
+ 'https://attack.mitre.org/techniques/T1041/'
121
+ ],
122
+ enabled: true,
123
+ semanticPatterns: [
124
+ {
125
+ type: 'function-call',
126
+ pattern: 'fetch',
127
+ confidence: 0.50
128
+ },
129
+ {
130
+ type: 'function-call',
131
+ pattern: 'axios',
132
+ confidence: 0.60
133
+ },
134
+ {
135
+ type: 'property-access',
136
+ pattern: 'XMLHttpRequest',
137
+ confidence: 0.70
138
+ },
139
+ {
140
+ type: 'function-call',
141
+ pattern: 'request',
142
+ confidence: 0.55
143
+ }
144
+ ]
145
+ },
146
+ {
147
+ id: 'SEM-005',
148
+ name: 'Environment Variable Access',
149
+ category: 'credentials',
150
+ severity: 'HIGH',
151
+ description: 'Detects environment variable access patterns that could expose sensitive credentials',
152
+ patterns: [],
153
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
154
+ components: ['skill', 'agent', 'hook', 'plugin', 'settings'],
155
+ remediation: 'Minimize environment variable access. Use secure credential storage and access patterns.',
156
+ references: [
157
+ 'https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html'
158
+ ],
159
+ enabled: true,
160
+ semanticPatterns: [
161
+ {
162
+ type: 'property-access',
163
+ pattern: 'process.env',
164
+ confidence: 0.80
165
+ },
166
+ {
167
+ type: 'property-access',
168
+ pattern: 'process.environment',
169
+ confidence: 0.85
170
+ },
171
+ {
172
+ type: 'function-call',
173
+ pattern: 'getenv',
174
+ confidence: 0.75
175
+ }
176
+ ]
177
+ },
178
+ {
179
+ id: 'SEM-006',
180
+ name: 'Obfuscated Function Names',
181
+ category: 'obfuscation',
182
+ severity: 'MEDIUM',
183
+ description: 'Detects function names that appear to be obfuscated or suspicious',
184
+ patterns: [],
185
+ fileTypes: ['md', 'ts', 'js', 'tsx', 'jsx'],
186
+ components: ['skill', 'agent', 'hook', 'plugin'],
187
+ remediation: 'Use clear, descriptive function names. Avoid obfuscation in legitimate code.',
188
+ references: [
189
+ 'https://attack.mitre.org/techniques/T1027/'
190
+ ],
191
+ enabled: true,
192
+ semanticPatterns: [
193
+ {
194
+ type: 'function-call',
195
+ pattern: '_0x',
196
+ confidence: 0.95
197
+ },
198
+ {
199
+ type: 'function-call',
200
+ pattern: '__',
201
+ confidence: 0.60
202
+ },
203
+ {
204
+ type: 'property-access',
205
+ pattern: '$_',
206
+ confidence: 0.70
207
+ }
208
+ ]
209
+ }
210
+ ];
211
+ export default semanticRules;
212
+ //# sourceMappingURL=semanticRules.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Supply Chain Attack Detection Rules
3
+ * Detects compromised or malicious dependencies
4
+ */
5
+ import type { Rule } from '../types.js';
6
+ export declare const supplyChainRules: Rule[];
7
+ export default supplyChainRules;
8
+ //# sourceMappingURL=supply-chain.d.ts.map
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Supply Chain Attack Detection Rules
3
+ * Detects compromised or malicious dependencies
4
+ */
5
+ export const supplyChainRules = [
6
+ {
7
+ id: 'SUPP-001',
8
+ name: 'Unsafe npm Install',
9
+ category: 'supply-chain',
10
+ severity: 'HIGH',
11
+ description: 'Detects npm install with disabled script execution checks',
12
+ patterns: [
13
+ /npm\s+install.*--ignore-scripts/gi,
14
+ /npm\s+i.*--ignore-scripts/gi,
15
+ /npm\s+install.*--unsafe-perm/gi,
16
+ ],
17
+ fileTypes: ['sh', 'bash', 'zsh', 'md'],
18
+ components: ['hook', 'skill', 'agent', 'ai-config-md', 'plugin'],
19
+ remediation: 'Never use --ignore-scripts or --unsafe-perm with npm install.',
20
+ references: [],
21
+ enabled: true,
22
+ },
23
+ {
24
+ id: 'SUPP-002',
25
+ name: 'Direct Script Execution from URL',
26
+ category: 'supply-chain',
27
+ severity: 'CRITICAL',
28
+ description: 'Detects downloading and executing scripts from URLs',
29
+ patterns: [
30
+ /curl\s+.*\|\s*(ba)?sh/gi,
31
+ /wget\s+.*\|\s*(ba)?sh/gi,
32
+ /curl\s+-s.*\|\s*bash/gi,
33
+ /wget\s+-q.*\|\s*bash/gi,
34
+ ],
35
+ fileTypes: ['sh', 'bash', 'zsh', 'md'],
36
+ components: ['hook', 'skill', 'agent', 'ai-config-md', 'plugin'],
37
+ remediation: 'Never pipe downloaded content directly to a shell.',
38
+ references: [],
39
+ enabled: true,
40
+ },
41
+ {
42
+ id: 'SUPP-003',
43
+ name: 'Untrusted Source Download',
44
+ category: 'supply-chain',
45
+ severity: 'HIGH',
46
+ description: 'Detects downloads from potentially untrusted sources',
47
+ patterns: [
48
+ /curl\s+.*--no-check-certificate/gi,
49
+ /wget\s+.*--no-check-certificate/gi,
50
+ /curl\s+-k\s+/gi,
51
+ /curl\s+--insecure/gi,
52
+ ],
53
+ fileTypes: ['sh', 'bash', 'zsh', 'md'],
54
+ components: ['hook', 'skill', 'agent', 'ai-config-md', 'plugin'],
55
+ remediation: 'Always verify SSL certificates when downloading files.',
56
+ references: [],
57
+ enabled: true,
58
+ },
59
+ {
60
+ id: 'SUPP-004',
61
+ name: 'Suspicious MCP Server',
62
+ category: 'supply-chain',
63
+ severity: 'HIGH',
64
+ description: 'Detects MCP servers from unknown or suspicious sources',
65
+ patterns: [
66
+ /command.*npx\s+-y\s+[^@\s]+/gi, // npx without explicit version
67
+ /command.*npm.*exec/gi,
68
+ ],
69
+ fileTypes: ['json'],
70
+ components: ['mcp', 'settings'],
71
+ remediation: 'Review MCP server sources. Only use trusted, versioned packages.',
72
+ references: [],
73
+ enabled: true,
74
+ },
75
+ {
76
+ id: 'SUPP-005',
77
+ name: 'Typosquatting Package Names',
78
+ category: 'supply-chain',
79
+ severity: 'HIGH',
80
+ description: 'Detects potential typosquatting variants of popular packages in dependency contexts',
81
+ patterns: [
82
+ /["']l[o0]d[a4]sh["']/gi, // lodash typos in quoted strings
83
+ /["']requ[ei]st["']/gi, // request typos (but not just "request")
84
+ /["']expresss["']/gi, // express typos
85
+ /["']reactt["']/gi, // react typos
86
+ /["']angularr["']/gi, // angular typos
87
+ /npm\s+i(nstall)?\s+.*l[o0]d[a4]sh/gi, // npm install typos
88
+ /npm\s+i(nstall)?\s+.*expresss/gi,
89
+ ],
90
+ fileTypes: ['json'],
91
+ components: ['mcp', 'settings', 'plugin'],
92
+ remediation: 'Verify package names are correct. Typosquatting is a common attack vector.',
93
+ references: [],
94
+ enabled: true,
95
+ // Exclude common false positives
96
+ excludePatterns: [
97
+ /http_request/gi, // Prometheus metrics
98
+ /requests?_total/gi, // Prometheus metrics
99
+ /request_duration/gi, // Prometheus metrics
100
+ /request_count/gi, // Prometheus metrics
101
+ /XMLHttpRequest/gi, // Browser API
102
+ /fetch.*request/gi, // Fetch API
103
+ /request\s*=/gi, // Variable assignment
104
+ /request\s*:/gi, // Object property
105
+ /request\s*\(/gi, // Function call
106
+ /\.request\(/gi, // Method call
107
+ /request\s+body/gi, // HTTP context
108
+ /request\s+header/gi, // HTTP context
109
+ /request\s+method/gi, // HTTP context
110
+ /pull\s+request/gi, // Git context
111
+ ],
112
+ },
113
+ {
114
+ id: 'SUPP-006',
115
+ name: 'Unverified Plugin Source',
116
+ category: 'supply-chain',
117
+ severity: 'MEDIUM',
118
+ description: 'Detects plugins or skills from unverified sources',
119
+ patterns: [
120
+ /downloaded\s+from(?!.*github\.com|.*anthropic\.com|.*npmjs\.com)/gi,
121
+ /source.*http(?!s)/gi,
122
+ ],
123
+ fileTypes: ['md', 'json'],
124
+ components: ['skill', 'agent', 'plugin', 'mcp'],
125
+ remediation: 'Only use plugins from verified sources.',
126
+ references: [],
127
+ enabled: true,
128
+ },
129
+ {
130
+ id: 'SUPP-007',
131
+ name: 'Package Postinstall Hook',
132
+ category: 'supply-chain',
133
+ severity: 'MEDIUM',
134
+ description: 'Detects references to package postinstall hooks',
135
+ patterns: [
136
+ /postinstall/gi,
137
+ /preinstall/gi,
138
+ /scripts.*install/gi,
139
+ ],
140
+ fileTypes: ['json'],
141
+ components: ['mcp', 'plugin'],
142
+ remediation: 'Review postinstall scripts carefully. They can execute arbitrary code.',
143
+ references: [],
144
+ enabled: true,
145
+ },
146
+ ];
147
+ export default supplyChainRules;
148
+ //# sourceMappingURL=supply-chain.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * FileDiscovery - Discovers AI CLI configuration files
3
+ * Scans directories for skills, agents, hooks, MCP configs, rules files, and other AI CLI files
4
+ * Supports: Claude Code, Cursor, Windsurf, Continue, Aider, Cline, and generic AI configs
5
+ */
6
+ import type { DiscoveredFile } from '../types.js';
7
+ interface DiscoveryOptions {
8
+ maxFileSize: number;
9
+ ignore: string[];
10
+ }
11
+ interface DiscoveryResult {
12
+ files: DiscoveredFile[];
13
+ skipped: number;
14
+ errors: {
15
+ path: string;
16
+ error: string;
17
+ }[];
18
+ }
19
+ /**
20
+ * Main file discovery function
21
+ */
22
+ export declare function discoverFiles(paths: string[], options: DiscoveryOptions): DiscoveryResult;
23
+ export default discoverFiles;
24
+ //# sourceMappingURL=FileDiscovery.d.ts.map
@@ -0,0 +1,282 @@
1
+ /**
2
+ * FileDiscovery - Discovers AI CLI configuration files
3
+ * Scans directories for skills, agents, hooks, MCP configs, rules files, and other AI CLI files
4
+ * Supports: Claude Code, Cursor, Windsurf, Continue, Aider, Cline, and generic AI configs
5
+ */
6
+ import { readdirSync, statSync, existsSync } from 'node:fs';
7
+ import { resolve, extname, basename, relative } from 'node:path';
8
+ import { createIgnoreFilter, shouldIgnore } from '../utils/ignore.js';
9
+ import logger from '../utils/logger.js';
10
+ /**
11
+ * Map file extensions to FileType
12
+ */
13
+ function getFileType(filePath) {
14
+ const ext = extname(filePath).toLowerCase().slice(1);
15
+ const fileTypeMap = {
16
+ 'md': 'md',
17
+ 'sh': 'sh',
18
+ 'bash': 'bash',
19
+ 'zsh': 'zsh',
20
+ 'json': 'json',
21
+ 'yaml': 'yaml',
22
+ 'yml': 'yml',
23
+ };
24
+ return fileTypeMap[ext] ?? null;
25
+ }
26
+ /**
27
+ * Detect component type from file path
28
+ * Supports multiple AI CLI patterns
29
+ */
30
+ function detectComponentType(filePath) {
31
+ const normalizedPath = filePath.toLowerCase();
32
+ const fileName = basename(filePath).toLowerCase();
33
+ // Skills directory
34
+ if (normalizedPath.includes('/skills/') || normalizedPath.includes('\\skills\\')) {
35
+ return 'skill';
36
+ }
37
+ // Agents directory
38
+ if (normalizedPath.includes('/agents/') || normalizedPath.includes('\\agents\\')) {
39
+ return 'agent';
40
+ }
41
+ // Hooks directory or hook files
42
+ if (normalizedPath.includes('/hooks/') ||
43
+ normalizedPath.includes('\\hooks\\') ||
44
+ fileName.includes('hook')) {
45
+ return 'hook';
46
+ }
47
+ // Plugins directory
48
+ if (normalizedPath.includes('/plugins/') || normalizedPath.includes('\\plugins\\')) {
49
+ return 'plugin';
50
+ }
51
+ // MCP configuration
52
+ if (fileName === '.mcp.json' || fileName === 'mcp.json') {
53
+ return 'mcp';
54
+ }
55
+ // Rules files (Cursor, Windsurf, Cline)
56
+ if (fileName === '.cursorrules' ||
57
+ fileName === '.windsurfrules' ||
58
+ fileName === '.clinerules') {
59
+ return 'rules-file';
60
+ }
61
+ // Settings files
62
+ if (fileName === 'settings.json' ||
63
+ fileName === 'settings.local.json' ||
64
+ fileName.includes('config')) {
65
+ return 'settings';
66
+ }
67
+ // AI config markdown files (CLAUDE.md, AI.md, AGENT.md, etc.)
68
+ if (fileName === 'claude.md' ||
69
+ fileName.startsWith('claude') ||
70
+ fileName === 'ai.md' ||
71
+ fileName === 'agent.md' ||
72
+ fileName === 'agents.md') {
73
+ return 'ai-config-md';
74
+ }
75
+ // Default to settings for JSON, ai-config-md for markdown
76
+ const type = getFileType(filePath);
77
+ if (type === 'json') {
78
+ return 'settings';
79
+ }
80
+ if (type === 'md') {
81
+ return 'ai-config-md';
82
+ }
83
+ return 'settings';
84
+ }
85
+ /**
86
+ * Check if a file should be analyzed
87
+ */
88
+ function isAnalyzableFile(filePath) {
89
+ const type = getFileType(filePath);
90
+ if (!type)
91
+ return false;
92
+ const fileName = basename(filePath).toLowerCase();
93
+ // Specific files we care about (multi-CLI support)
94
+ const targetFiles = [
95
+ // Claude Code
96
+ 'claude.md',
97
+ '.mcp.json',
98
+ 'mcp.json',
99
+ 'settings.json',
100
+ 'settings.local.json',
101
+ // Cursor
102
+ '.cursorrules',
103
+ // Windsurf
104
+ '.windsurfrules',
105
+ // Cline
106
+ '.clinerules',
107
+ // Aider
108
+ '.aider.conf.yml',
109
+ '.aiderignore',
110
+ // Generic AI
111
+ 'ai.md',
112
+ 'agent.md',
113
+ 'agents.md',
114
+ ];
115
+ if (targetFiles.includes(fileName)) {
116
+ return true;
117
+ }
118
+ // All markdown files in AI CLI config directories
119
+ if (type === 'md') {
120
+ return true;
121
+ }
122
+ // Shell scripts in hooks or anywhere in .claude
123
+ if (type === 'sh' || type === 'bash' || type === 'zsh') {
124
+ return true;
125
+ }
126
+ // JSON files that might be configs
127
+ if (type === 'json') {
128
+ return true;
129
+ }
130
+ // YAML files
131
+ if (type === 'yaml' || type === 'yml') {
132
+ return true;
133
+ }
134
+ return false;
135
+ }
136
+ /**
137
+ * Recursively discover files in a directory
138
+ */
139
+ function discoverFilesInDirectory(dir, baseDir, ig, options, result) {
140
+ let entries;
141
+ try {
142
+ entries = readdirSync(dir);
143
+ }
144
+ catch (error) {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ result.errors.push({ path: dir, error: message });
147
+ logger.debug(`Cannot read directory: ${dir}`);
148
+ return;
149
+ }
150
+ for (const entry of entries) {
151
+ const fullPath = resolve(dir, entry);
152
+ const relativePath = relative(baseDir, fullPath);
153
+ // Check if ignored
154
+ if (shouldIgnore(ig, fullPath, baseDir)) {
155
+ result.skipped++;
156
+ logger.debug(`Ignored: ${relativePath}`);
157
+ continue;
158
+ }
159
+ let stats;
160
+ try {
161
+ stats = statSync(fullPath);
162
+ }
163
+ catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ result.errors.push({ path: fullPath, error: message });
166
+ continue;
167
+ }
168
+ if (stats.isDirectory()) {
169
+ // Recurse into directory
170
+ discoverFilesInDirectory(fullPath, baseDir, ig, options, result);
171
+ }
172
+ else if (stats.isFile()) {
173
+ // Check if file should be analyzed
174
+ if (!isAnalyzableFile(fullPath)) {
175
+ result.skipped++;
176
+ continue;
177
+ }
178
+ // Check file size
179
+ if (stats.size > options.maxFileSize) {
180
+ logger.debug(`Skipping large file: ${relativePath} (${stats.size} bytes)`);
181
+ result.skipped++;
182
+ continue;
183
+ }
184
+ const fileType = getFileType(fullPath);
185
+ if (!fileType) {
186
+ result.skipped++;
187
+ continue;
188
+ }
189
+ const discoveredFile = {
190
+ path: fullPath,
191
+ relativePath,
192
+ type: fileType,
193
+ component: detectComponentType(fullPath),
194
+ size: stats.size,
195
+ modified: stats.mtime,
196
+ };
197
+ result.files.push(discoveredFile);
198
+ logger.debug(`Discovered: ${relativePath} (${discoveredFile.component})`);
199
+ }
200
+ }
201
+ }
202
+ /**
203
+ * Discover a single file
204
+ */
205
+ function discoverSingleFile(filePath, options, result) {
206
+ if (!existsSync(filePath)) {
207
+ result.errors.push({ path: filePath, error: 'File does not exist' });
208
+ return;
209
+ }
210
+ let stats;
211
+ try {
212
+ stats = statSync(filePath);
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ result.errors.push({ path: filePath, error: message });
217
+ return;
218
+ }
219
+ if (!stats.isFile()) {
220
+ result.errors.push({ path: filePath, error: 'Not a file' });
221
+ return;
222
+ }
223
+ if (stats.size > options.maxFileSize) {
224
+ result.skipped++;
225
+ return;
226
+ }
227
+ const fileType = getFileType(filePath);
228
+ if (!fileType) {
229
+ result.skipped++;
230
+ return;
231
+ }
232
+ const discoveredFile = {
233
+ path: filePath,
234
+ relativePath: basename(filePath),
235
+ type: fileType,
236
+ component: detectComponentType(filePath),
237
+ size: stats.size,
238
+ modified: stats.mtime,
239
+ };
240
+ result.files.push(discoveredFile);
241
+ }
242
+ /**
243
+ * Main file discovery function
244
+ */
245
+ export function discoverFiles(paths, options) {
246
+ const result = {
247
+ files: [],
248
+ skipped: 0,
249
+ errors: [],
250
+ };
251
+ if (paths.length === 0) {
252
+ logger.warn('No paths provided for scanning');
253
+ return result;
254
+ }
255
+ for (const inputPath of paths) {
256
+ const resolvedPath = resolve(inputPath);
257
+ if (!existsSync(resolvedPath)) {
258
+ logger.warn(`Path does not exist: ${resolvedPath}`);
259
+ result.errors.push({ path: resolvedPath, error: 'Path does not exist' });
260
+ continue;
261
+ }
262
+ const stats = statSync(resolvedPath);
263
+ if (stats.isDirectory()) {
264
+ const ig = createIgnoreFilter(resolvedPath, options.ignore);
265
+ discoverFilesInDirectory(resolvedPath, resolvedPath, ig, options, result);
266
+ }
267
+ else if (stats.isFile()) {
268
+ discoverSingleFile(resolvedPath, options, result);
269
+ }
270
+ }
271
+ // Sort files by component type, then by path
272
+ result.files.sort((a, b) => {
273
+ if (a.component !== b.component) {
274
+ return a.component.localeCompare(b.component);
275
+ }
276
+ return a.relativePath.localeCompare(b.relativePath);
277
+ });
278
+ logger.info(`Discovered ${result.files.length} files, skipped ${result.skipped}`);
279
+ return result;
280
+ }
281
+ export default discoverFiles;
282
+ //# sourceMappingURL=FileDiscovery.js.map