ferret-scan 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/bin/ferret.js +822 -0
- package/dist/__tests__/basic.test.d.ts +6 -0
- package/dist/__tests__/basic.test.js +80 -0
- package/dist/analyzers/AstAnalyzer.d.ts +30 -0
- package/dist/analyzers/AstAnalyzer.js +332 -0
- package/dist/analyzers/CorrelationAnalyzer.d.ts +21 -0
- package/dist/analyzers/CorrelationAnalyzer.js +288 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/intelligence/IndicatorMatcher.d.ts +50 -0
- package/dist/intelligence/IndicatorMatcher.js +285 -0
- package/dist/intelligence/ThreatFeed.d.ts +99 -0
- package/dist/intelligence/ThreatFeed.js +296 -0
- package/dist/remediation/Fixer.d.ts +71 -0
- package/dist/remediation/Fixer.js +391 -0
- package/dist/remediation/Quarantine.d.ts +102 -0
- package/dist/remediation/Quarantine.js +329 -0
- package/dist/reporters/ConsoleReporter.d.ts +13 -0
- package/dist/reporters/ConsoleReporter.js +185 -0
- package/dist/reporters/HtmlReporter.d.ts +25 -0
- package/dist/reporters/HtmlReporter.js +604 -0
- package/dist/reporters/SarifReporter.d.ts +86 -0
- package/dist/reporters/SarifReporter.js +117 -0
- package/dist/rules/ai-specific.d.ts +8 -0
- package/dist/rules/ai-specific.js +221 -0
- package/dist/rules/backdoors.d.ts +8 -0
- package/dist/rules/backdoors.js +134 -0
- package/dist/rules/correlationRules.d.ts +8 -0
- package/dist/rules/correlationRules.js +227 -0
- package/dist/rules/credentials.d.ts +8 -0
- package/dist/rules/credentials.js +194 -0
- package/dist/rules/exfiltration.d.ts +8 -0
- package/dist/rules/exfiltration.js +139 -0
- package/dist/rules/index.d.ts +51 -0
- package/dist/rules/index.js +97 -0
- package/dist/rules/injection.d.ts +8 -0
- package/dist/rules/injection.js +136 -0
- package/dist/rules/obfuscation.d.ts +8 -0
- package/dist/rules/obfuscation.js +159 -0
- package/dist/rules/permissions.d.ts +8 -0
- package/dist/rules/permissions.js +129 -0
- package/dist/rules/persistence.d.ts +8 -0
- package/dist/rules/persistence.js +117 -0
- package/dist/rules/semanticRules.d.ts +10 -0
- package/dist/rules/semanticRules.js +212 -0
- package/dist/rules/supply-chain.d.ts +8 -0
- package/dist/rules/supply-chain.js +148 -0
- package/dist/scanner/FileDiscovery.d.ts +24 -0
- package/dist/scanner/FileDiscovery.js +282 -0
- package/dist/scanner/PatternMatcher.d.ts +25 -0
- package/dist/scanner/PatternMatcher.js +206 -0
- package/dist/scanner/Scanner.d.ts +14 -0
- package/dist/scanner/Scanner.js +266 -0
- package/dist/scanner/WatchMode.d.ts +29 -0
- package/dist/scanner/WatchMode.js +195 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.js +53 -0
- package/dist/utils/baseline.d.ts +80 -0
- package/dist/utils/baseline.js +276 -0
- package/dist/utils/config.d.ts +21 -0
- package/dist/utils/config.js +247 -0
- package/dist/utils/ignore.d.ts +18 -0
- package/dist/utils/ignore.js +82 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +75 -0
- package/package.json +119 -0
|
@@ -0,0 +1,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
|