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,6 @@
1
+ /**
2
+ * Basic Tests
3
+ * Simple tests to verify core functionality
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=basic.test.d.ts.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Basic Tests
3
+ * Simple tests to verify core functionality
4
+ */
5
+ import { getAllRules } from '../rules/index.js';
6
+ import { formatSarifReport } from '../reporters/SarifReporter.js';
7
+ describe('Basic Functionality', () => {
8
+ describe('Rules', () => {
9
+ it('should load all rules', () => {
10
+ const rules = getAllRules();
11
+ expect(rules.length).toBeGreaterThan(0);
12
+ expect(rules.every(rule => rule.id && rule.name)).toBe(true);
13
+ });
14
+ it('should have valid patterns', () => {
15
+ const rules = getAllRules();
16
+ for (const rule of rules) {
17
+ // Rules may have patterns, semanticPatterns, or correlationRules
18
+ const hasPatterns = rule.patterns.length > 0;
19
+ const hasSemanticPatterns = (rule.semanticPatterns?.length ?? 0) > 0;
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
21
+ const hasCorrelationRules = (rule.correlationRules?.length ?? 0) > 0;
22
+ expect(hasPatterns || hasSemanticPatterns || hasCorrelationRules).toBe(true);
23
+ for (const pattern of rule.patterns) {
24
+ expect(pattern).toBeInstanceOf(RegExp);
25
+ }
26
+ }
27
+ });
28
+ });
29
+ describe('SARIF Reporter', () => {
30
+ it('should generate valid SARIF for empty results', () => {
31
+ const mockResult = {
32
+ success: true,
33
+ startTime: new Date(),
34
+ endTime: new Date(),
35
+ duration: 100,
36
+ scannedPaths: [],
37
+ totalFiles: 0,
38
+ analyzedFiles: 0,
39
+ skippedFiles: 0,
40
+ findings: [],
41
+ findingsBySeverity: {
42
+ CRITICAL: [],
43
+ HIGH: [],
44
+ MEDIUM: [],
45
+ LOW: [],
46
+ INFO: [],
47
+ },
48
+ findingsByCategory: {
49
+ injection: [],
50
+ credentials: [],
51
+ backdoors: [],
52
+ 'supply-chain': [],
53
+ permissions: [],
54
+ persistence: [],
55
+ obfuscation: [],
56
+ 'ai-specific': [],
57
+ 'advanced-hiding': [],
58
+ behavioral: [],
59
+ exfiltration: [],
60
+ },
61
+ overallRiskScore: 0,
62
+ summary: {
63
+ critical: 0,
64
+ high: 0,
65
+ medium: 0,
66
+ low: 0,
67
+ info: 0,
68
+ total: 0,
69
+ },
70
+ errors: [],
71
+ };
72
+ const sarifOutput = formatSarifReport(mockResult);
73
+ const parsed = JSON.parse(sarifOutput);
74
+ expect(parsed.version).toBe('2.1.0');
75
+ expect(parsed.runs).toHaveLength(1);
76
+ expect(parsed.runs[0]?.tool.driver.name).toBe('ferret-scan');
77
+ });
78
+ });
79
+ });
80
+ //# sourceMappingURL=basic.test.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AST Analyzer - TypeScript/JavaScript semantic analysis for security scanning
3
+ * Analyzes code blocks in markdown and TypeScript configurations for complex patterns
4
+ */
5
+ import type { SemanticFinding, DiscoveredFile, Rule } from '../types.js';
6
+ /**
7
+ * Analyze a single file for semantic patterns
8
+ */
9
+ export declare function analyzeFile(file: DiscoveredFile, content: string, rules: Rule[]): SemanticFinding[];
10
+ /**
11
+ * Check if semantic analysis should be performed
12
+ */
13
+ export declare function shouldAnalyze(file: DiscoveredFile, config: {
14
+ semanticAnalysis: boolean;
15
+ maxFileSize: number;
16
+ }): boolean;
17
+ /**
18
+ * Get memory usage for monitoring
19
+ */
20
+ export declare function getMemoryUsage(): {
21
+ used: number;
22
+ total: number;
23
+ };
24
+ declare const _default: {
25
+ analyzeFile: typeof analyzeFile;
26
+ shouldAnalyze: typeof shouldAnalyze;
27
+ getMemoryUsage: typeof getMemoryUsage;
28
+ };
29
+ export default _default;
30
+ //# sourceMappingURL=AstAnalyzer.d.ts.map
@@ -0,0 +1,332 @@
1
+ /**
2
+ * AST Analyzer - TypeScript/JavaScript semantic analysis for security scanning
3
+ * Analyzes code blocks in markdown and TypeScript configurations for complex patterns
4
+ */
5
+ import * as ts from 'typescript';
6
+ import logger from '../utils/logger.js';
7
+ /**
8
+ * Extract code blocks from markdown content
9
+ */
10
+ function extractCodeBlocks(content) {
11
+ const codeBlocks = [];
12
+ const lines = content.split('\n');
13
+ let inCodeBlock = false;
14
+ let currentBlock = [];
15
+ let currentLanguage = '';
16
+ let blockStartLine = 0;
17
+ for (let i = 0; i < lines.length; i++) {
18
+ const line = lines[i];
19
+ const trimmedLine = line?.trim() ?? '';
20
+ if (trimmedLine.startsWith('```')) {
21
+ if (!inCodeBlock) {
22
+ // Starting a code block
23
+ inCodeBlock = true;
24
+ currentLanguage = trimmedLine.slice(3).trim().toLowerCase();
25
+ blockStartLine = i + 2; // +2 because we want the first line of actual code
26
+ currentBlock = [];
27
+ }
28
+ else {
29
+ // Ending a code block
30
+ inCodeBlock = false;
31
+ if (currentBlock.length > 0 && isAnalyzableLanguage(currentLanguage)) {
32
+ codeBlocks.push({
33
+ code: currentBlock.join('\n'),
34
+ language: currentLanguage,
35
+ line: blockStartLine || 1 // Ensure we have a valid line number
36
+ });
37
+ }
38
+ currentBlock = [];
39
+ currentLanguage = '';
40
+ }
41
+ }
42
+ else if (inCodeBlock) {
43
+ currentBlock.push(line ?? '');
44
+ }
45
+ }
46
+ return codeBlocks;
47
+ }
48
+ /**
49
+ * Check if language can be analyzed
50
+ */
51
+ function isAnalyzableLanguage(language) {
52
+ const supportedLanguages = ['typescript', 'ts', 'javascript', 'js', 'jsx', 'tsx'];
53
+ return supportedLanguages.includes(language);
54
+ }
55
+ /**
56
+ * Create TypeScript AST from code
57
+ */
58
+ function createAST(code, fileName = 'analysis.ts') {
59
+ return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, fileName?.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
60
+ }
61
+ /**
62
+ * Extract semantic context from AST
63
+ */
64
+ function extractSemanticContext(sourceFile) {
65
+ const context = {
66
+ variables: [],
67
+ imports: [],
68
+ callChain: []
69
+ };
70
+ function visit(node) {
71
+ switch (node.kind) {
72
+ case ts.SyntaxKind.ImportDeclaration: {
73
+ const importDecl = node;
74
+ if (importDecl.moduleSpecifier && ts.isStringLiteral(importDecl.moduleSpecifier)) {
75
+ context.imports.push(importDecl.moduleSpecifier.text);
76
+ }
77
+ break;
78
+ }
79
+ case ts.SyntaxKind.VariableDeclaration: {
80
+ const varDecl = node;
81
+ if (varDecl.name && ts.isIdentifier(varDecl.name)) {
82
+ context.variables.push(varDecl.name.text);
83
+ }
84
+ break;
85
+ }
86
+ case ts.SyntaxKind.CallExpression: {
87
+ const callExpr = node;
88
+ const callText = callExpr.expression.getText(sourceFile);
89
+ context.callChain.push(callText);
90
+ break;
91
+ }
92
+ }
93
+ ts.forEachChild(node, visit);
94
+ }
95
+ visit(sourceFile);
96
+ return context;
97
+ }
98
+ /**
99
+ * Find security patterns in AST
100
+ */
101
+ function findSecurityPatterns(sourceFile, patterns) {
102
+ const matches = [];
103
+ function visit(node) {
104
+ for (const pattern of patterns) {
105
+ const match = matchSemanticPattern(node, pattern, sourceFile);
106
+ if (match) {
107
+ matches.push({
108
+ pattern,
109
+ node,
110
+ confidence: match.confidence
111
+ });
112
+ }
113
+ }
114
+ ts.forEachChild(node, visit);
115
+ }
116
+ visit(sourceFile);
117
+ return matches;
118
+ }
119
+ /**
120
+ * Match a semantic pattern against an AST node
121
+ */
122
+ function matchSemanticPattern(node, pattern, sourceFile) {
123
+ const nodeText = node.getText(sourceFile);
124
+ let confidence = pattern.confidence ?? 0.8;
125
+ switch (pattern.type) {
126
+ case 'function-call':
127
+ if (ts.isCallExpression(node)) {
128
+ const functionName = node.expression.getText(sourceFile);
129
+ if (functionName.includes(pattern.pattern)) {
130
+ return { confidence };
131
+ }
132
+ }
133
+ break;
134
+ case 'property-access':
135
+ if (ts.isPropertyAccessExpression(node)) {
136
+ const fullAccess = node.getText(sourceFile);
137
+ if (fullAccess.includes(pattern.pattern)) {
138
+ return { confidence };
139
+ }
140
+ }
141
+ break;
142
+ case 'dynamic-import':
143
+ if (ts.isCallExpression(node)) {
144
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
145
+ // Dynamic import detected - pattern for security analysis only
146
+ if (pattern.pattern === 'dynamic-import' || nodeText.includes(pattern.pattern)) {
147
+ confidence += 0.1; // Higher confidence for dynamic imports
148
+ return { confidence };
149
+ }
150
+ }
151
+ }
152
+ break;
153
+ case 'eval-chain':
154
+ if (nodeText.toLowerCase().includes('eval') ||
155
+ nodeText.includes('Function(')) {
156
+ // Detecting eval patterns for security analysis - not executing
157
+ if (pattern.pattern === 'eval' || nodeText.includes(pattern.pattern)) {
158
+ confidence += 0.2; // Higher confidence for eval usage
159
+ return { confidence };
160
+ }
161
+ }
162
+ break;
163
+ case 'object-structure':
164
+ if (ts.isObjectLiteralExpression(node)) {
165
+ if (nodeText.includes(pattern.pattern)) {
166
+ return { confidence };
167
+ }
168
+ }
169
+ break;
170
+ }
171
+ return null;
172
+ }
173
+ /**
174
+ * Create AST node info
175
+ */
176
+ function createASTNodeInfo(node, sourceFile) {
177
+ const nodeName = getNodeName(node);
178
+ return {
179
+ nodeType: ts.SyntaxKind[node.kind],
180
+ ...(nodeName && { name: nodeName }),
181
+ ...(node.parent && { parent: ts.SyntaxKind[node.parent.kind] }),
182
+ children: node.getChildren(sourceFile).map(child => ts.SyntaxKind[child.kind])
183
+ };
184
+ }
185
+ /**
186
+ * Get node name/identifier
187
+ */
188
+ function getNodeName(node) {
189
+ if (ts.isIdentifier(node)) {
190
+ return node.text;
191
+ }
192
+ if (ts.isFunctionDeclaration(node) && node.name) {
193
+ return node.name.text;
194
+ }
195
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
196
+ return node.name.text;
197
+ }
198
+ return undefined;
199
+ }
200
+ /**
201
+ * Get line and column from AST node
202
+ */
203
+ function getPositionFromNode(node, sourceFile) {
204
+ const pos = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
205
+ return {
206
+ line: pos.line + 1, // Convert to 1-based
207
+ column: pos.character + 1
208
+ };
209
+ }
210
+ /**
211
+ * Create context lines for semantic finding
212
+ */
213
+ function createContextLines(sourceFile, node, contextLines = 3) {
214
+ const text = sourceFile.getText();
215
+ const lines = text.split('\n');
216
+ const pos = getPositionFromNode(node, sourceFile);
217
+ const matchLine = pos.line - 1; // Convert back to 0-based
218
+ const start = Math.max(0, matchLine - contextLines);
219
+ const end = Math.min(lines.length, matchLine + contextLines + 1);
220
+ const context = [];
221
+ for (let i = start; i < end; i++) {
222
+ context.push({
223
+ lineNumber: i + 1,
224
+ content: lines[i] ?? '',
225
+ isMatch: i === matchLine
226
+ });
227
+ }
228
+ return context;
229
+ }
230
+ /**
231
+ * Analyze a single file for semantic patterns
232
+ */
233
+ export function analyzeFile(file, content, rules) {
234
+ const findings = [];
235
+ try {
236
+ // Get rules with semantic patterns
237
+ const semanticRules = rules.filter(rule => rule.semanticPatterns && rule.semanticPatterns.length > 0);
238
+ if (semanticRules.length === 0) {
239
+ return findings;
240
+ }
241
+ logger.debug(`AST analysis for ${file.relativePath} with ${semanticRules.length} semantic rules`);
242
+ let codeBlocksToAnalyze = [];
243
+ // Extract code blocks from markdown files
244
+ if (file.type === 'md') {
245
+ codeBlocksToAnalyze = extractCodeBlocks(content);
246
+ }
247
+ else if (['ts', 'js', 'tsx', 'jsx'].includes(file.type)) {
248
+ // Analyze the entire file for TypeScript/JavaScript files
249
+ codeBlocksToAnalyze = [{ code: content, language: file.type, line: 1 }];
250
+ }
251
+ // Analyze each code block
252
+ for (const codeBlock of codeBlocksToAnalyze) {
253
+ try {
254
+ const sourceFile = createAST(codeBlock.code, `${file.relativePath}_block_${codeBlock.line}.${codeBlock.language}`);
255
+ const semanticContext = extractSemanticContext(sourceFile);
256
+ // Check each semantic rule
257
+ for (const rule of semanticRules) {
258
+ if (!rule.semanticPatterns)
259
+ continue;
260
+ const patternMatches = findSecurityPatterns(sourceFile, rule.semanticPatterns);
261
+ for (const match of patternMatches) {
262
+ const position = getPositionFromNode(match.node, sourceFile);
263
+ const astNodeInfo = createASTNodeInfo(match.node, sourceFile);
264
+ const contextLines = createContextLines(sourceFile, match.node, 3);
265
+ const finding = {
266
+ ruleId: rule.id,
267
+ ruleName: rule.name,
268
+ severity: rule.severity,
269
+ category: rule.category,
270
+ file: file.path,
271
+ relativePath: file.relativePath,
272
+ line: (codeBlock.line || 1) + position.line - 1, // Adjust for code block position
273
+ column: position.column,
274
+ match: match.node.getText(sourceFile).substring(0, 100), // Limit match length
275
+ context: contextLines,
276
+ remediation: rule.remediation,
277
+ metadata: {
278
+ semanticPattern: match.pattern,
279
+ codeBlock: codeBlock.line,
280
+ language: codeBlock.language
281
+ },
282
+ timestamp: new Date(),
283
+ riskScore: Math.round(match.confidence * 100),
284
+ astNode: astNodeInfo,
285
+ semanticContext,
286
+ confidence: match.confidence
287
+ };
288
+ findings.push(finding);
289
+ }
290
+ }
291
+ }
292
+ catch (error) {
293
+ logger.warn(`Error analyzing code block at line ${codeBlock.line} in ${file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
294
+ }
295
+ }
296
+ }
297
+ catch (error) {
298
+ logger.error(`Error in semantic analysis for ${file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
299
+ }
300
+ return findings;
301
+ }
302
+ /**
303
+ * Check if semantic analysis should be performed
304
+ */
305
+ export function shouldAnalyze(file, config) {
306
+ if (!config.semanticAnalysis) {
307
+ return false;
308
+ }
309
+ // Skip files that are too large
310
+ if (file.size > config.maxFileSize) {
311
+ return false;
312
+ }
313
+ // Only analyze markdown and TypeScript/JavaScript files
314
+ const supportedTypes = ['md', 'ts', 'js', 'tsx', 'jsx'];
315
+ return supportedTypes.includes(file.type);
316
+ }
317
+ /**
318
+ * Get memory usage for monitoring
319
+ */
320
+ export function getMemoryUsage() {
321
+ const memUsage = process.memoryUsage();
322
+ return {
323
+ used: Math.round(memUsage.heapUsed / 1024 / 1024), // MB
324
+ total: Math.round(memUsage.heapTotal / 1024 / 1024) // MB
325
+ };
326
+ }
327
+ export default {
328
+ analyzeFile,
329
+ shouldAnalyze,
330
+ getMemoryUsage
331
+ };
332
+ //# sourceMappingURL=AstAnalyzer.js.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Correlation Analyzer - Cross-file attack pattern detection
3
+ * Detects sophisticated attack patterns that span multiple configuration files
4
+ */
5
+ import type { CorrelationFinding, DiscoveredFile, Rule } from '../types.js';
6
+ /**
7
+ * Analyze files for cross-file correlation patterns
8
+ */
9
+ export declare function analyzeCorrelations(files: DiscoveredFile[], rules: Rule[]): CorrelationFinding[];
10
+ /**
11
+ * Check if correlation analysis should be performed
12
+ */
13
+ export declare function shouldAnalyzeCorrelations(files: DiscoveredFile[], config: {
14
+ correlationAnalysis: boolean;
15
+ }): boolean;
16
+ declare const _default: {
17
+ analyzeCorrelations: typeof analyzeCorrelations;
18
+ shouldAnalyzeCorrelations: typeof shouldAnalyzeCorrelations;
19
+ };
20
+ export default _default;
21
+ //# sourceMappingURL=CorrelationAnalyzer.d.ts.map