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,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
|