ferret-scan 2.1.2 → 2.3.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 +35 -0
- package/README.md +15 -11
- package/bin/ferret.js +109 -13
- package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
- package/dist/__tests__/AgentMonitor.test.js +235 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
- package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
- package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
- package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
- package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
- package/dist/__tests__/IndicatorMatcher.test.js +245 -0
- package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
- package/dist/__tests__/MarketplaceScanner.test.js +212 -0
- package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
- package/dist/__tests__/RuleGenerator.test.js +207 -0
- package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
- package/dist/__tests__/ThreatFeed.test.js +359 -0
- package/dist/__tests__/WatchMode.test.d.ts +6 -0
- package/dist/__tests__/WatchMode.test.js +104 -0
- package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
- package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerFull.test.js +138 -0
- package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
- package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
- package/dist/__tests__/atlas.test.d.ts +6 -0
- package/dist/__tests__/atlas.test.js +319 -0
- package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalog.test.js +200 -0
- package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
- package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
- package/dist/__tests__/baseline.test.d.ts +6 -0
- package/dist/__tests__/baseline.test.js +321 -0
- package/dist/__tests__/baselineExtra.test.d.ts +6 -0
- package/dist/__tests__/baselineExtra.test.js +317 -0
- package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
- package/dist/__tests__/capabilityMapping.test.js +49 -0
- package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
- package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
- package/dist/__tests__/complianceExtra.test.d.ts +6 -0
- package/dist/__tests__/complianceExtra.test.js +121 -0
- package/dist/__tests__/config.test.js +1 -1
- package/dist/__tests__/configLoader.test.d.ts +6 -0
- package/dist/__tests__/configLoader.test.js +225 -0
- package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
- package/dist/__tests__/configLoaderExtra.test.js +186 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
- package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
- package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
- package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
- package/dist/__tests__/customRules.extra.test.d.ts +6 -0
- package/dist/__tests__/customRules.extra.test.js +245 -0
- package/dist/__tests__/customRules.test.d.ts +7 -0
- package/dist/__tests__/customRules.test.js +347 -0
- package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
- package/dist/__tests__/dependencyRisk.test.js +248 -0
- package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
- package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
- package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
- package/dist/__tests__/featureExitCodes.test.js +332 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
- package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
- package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
- package/dist/__tests__/fixer.extra.test.d.ts +6 -0
- package/dist/__tests__/fixer.extra.test.js +135 -0
- package/dist/__tests__/fixerApply.test.d.ts +6 -0
- package/dist/__tests__/fixerApply.test.js +132 -0
- package/dist/__tests__/gitHooks.test.d.ts +7 -0
- package/dist/__tests__/gitHooks.test.js +188 -0
- package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
- package/dist/__tests__/htmlReporter.extra.test.js +126 -0
- package/dist/__tests__/interactiveTui.test.d.ts +6 -0
- package/dist/__tests__/interactiveTui.test.js +180 -0
- package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
- package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiMore.test.js +194 -0
- package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
- package/dist/__tests__/interactiveTuiSession.test.js +173 -0
- package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysis.test.js +229 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
- package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
- package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
- package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
- package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
- package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
- package/dist/__tests__/llmGroqTPM.test.js +89 -0
- package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
- package/dist/__tests__/llmProviderRetry.test.js +172 -0
- package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
- package/dist/__tests__/mcpValidator.extra.test.js +270 -0
- package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
- package/dist/__tests__/patternMatcherExtra.test.js +198 -0
- package/dist/__tests__/patternsCommon.test.d.ts +6 -0
- package/dist/__tests__/patternsCommon.test.js +107 -0
- package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
- package/dist/__tests__/policyEnforcement.test.js +510 -0
- package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
- package/dist/__tests__/quarantineExtra.test.js +214 -0
- package/dist/__tests__/redactionExtra.test.d.ts +6 -0
- package/dist/__tests__/redactionExtra.test.js +228 -0
- package/dist/__tests__/scanDiff.test.d.ts +7 -0
- package/dist/__tests__/scanDiff.test.js +266 -0
- package/dist/__tests__/scanFull.test.d.ts +6 -0
- package/dist/__tests__/scanFull.test.js +158 -0
- package/dist/__tests__/scannerDampening.test.d.ts +6 -0
- package/dist/__tests__/scannerDampening.test.js +160 -0
- package/dist/__tests__/scannerExtra.test.d.ts +6 -0
- package/dist/__tests__/scannerExtra.test.js +194 -0
- package/dist/__tests__/scannerMitre.test.d.ts +5 -0
- package/dist/__tests__/scannerMitre.test.js +141 -0
- package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
- package/dist/__tests__/scannerSSRF.test.js +149 -0
- package/dist/__tests__/schemas.test.d.ts +6 -0
- package/dist/__tests__/schemas.test.js +125 -0
- package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
- package/dist/__tests__/webhooks.extra.test.js +144 -0
- package/dist/__tests__/webhooks.test.d.ts +6 -0
- package/dist/__tests__/webhooks.test.js +154 -0
- package/dist/analyzers/AstAnalyzer.d.ts +5 -1
- package/dist/analyzers/AstAnalyzer.js +25 -4
- package/dist/features/customRules.js +22 -29
- package/dist/features/ignoreComments.js +5 -5
- package/dist/features/mcpTrustScore.d.ts +17 -0
- package/dist/features/mcpTrustScore.js +74 -0
- package/dist/features/mcpValidator.d.ts +2 -0
- package/dist/features/mcpValidator.js +13 -0
- package/dist/features/policyEnforcement.d.ts +22 -22
- package/dist/features/policyEnforcement.js +3 -2
- package/dist/intelligence/ThreatFeed.js +207 -62
- package/dist/remediation/Fixer.js +56 -30
- package/dist/remediation/Quarantine.js +79 -11
- package/dist/reporters/ConsoleReporter.js +10 -0
- package/dist/reporters/HtmlReporter.js +5 -0
- package/dist/reporters/SarifReporter.d.ts +1 -0
- package/dist/reporters/SarifReporter.js +1 -0
- package/dist/rules/ai-specific.js +8 -8
- package/dist/rules/backdoors.js +12 -12
- package/dist/rules/correlationRules.js +6 -6
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +10 -1
- package/dist/rules/injection.js +8 -8
- package/dist/rules/patterns/common.d.ts +34 -0
- package/dist/rules/patterns/common.js +48 -0
- package/dist/scanner/IAnalyzer.d.ts +19 -0
- package/dist/scanner/IAnalyzer.js +5 -0
- package/dist/scanner/PatternMatcher.js +19 -2
- package/dist/scanner/Scanner.js +64 -125
- package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
- package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
- package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
- package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
- package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
- package/dist/types.d.ts +23 -0
- package/dist/types.js +1 -1
- package/dist/utils/baseline.d.ts +15 -2
- package/dist/utils/baseline.js +50 -19
- package/dist/utils/contentCache.d.ts +39 -0
- package/dist/utils/contentCache.js +77 -0
- package/dist/utils/glob.d.ts +50 -0
- package/dist/utils/glob.js +84 -0
- package/dist/utils/pathSecurity.js +1 -0
- package/dist/utils/safeRegex.d.ts +55 -0
- package/dist/utils/safeRegex.js +130 -0
- package/dist/utils/schemas.d.ts +70 -64
- package/dist/utils/schemas.js +13 -0
- package/package.json +34 -19
|
@@ -2,11 +2,49 @@
|
|
|
2
2
|
* Quarantine System - Safely isolate suspicious files and content
|
|
3
3
|
* Provides reversible quarantine operations with audit trails
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, unlinkSync, statSync } from 'node:fs';
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, unlinkSync, statSync, statfsSync } from 'node:fs';
|
|
6
6
|
import { resolve, dirname, basename } from 'node:path';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
8
|
import logger from '../utils/logger.js';
|
|
9
9
|
import { validatePathWithinBase, isPathWithinBase } from '../utils/pathSecurity.js';
|
|
10
|
+
/**
|
|
11
|
+
* Create a quarantine-grade directory with restrictive permissions (0700 on POSIX).
|
|
12
|
+
* On Windows, Node silently ignores the mode argument — permissions are managed by
|
|
13
|
+
* the OS ACL instead.
|
|
14
|
+
*/
|
|
15
|
+
function ensureSecureDir(dir) {
|
|
16
|
+
// 0o700 = owner-only rwx; harmlessly ignored on Windows
|
|
17
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
18
|
+
// Verify no group/other bits leaked through (e.g. pre-existing dir with loose perms).
|
|
19
|
+
// stat().mode & 0o077 !== 0 means at least one g/o bit is set.
|
|
20
|
+
if (process.platform !== 'win32') {
|
|
21
|
+
const mode = statSync(dir).mode;
|
|
22
|
+
if ((mode & 0o077) !== 0) {
|
|
23
|
+
logger.warn(`Quarantine directory ${dir} has loose permissions (mode ${(mode & 0o777).toString(8)}); secrets may be readable by other users`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check whether the quarantine directory has sufficient free space for a file of the given size.
|
|
29
|
+
* Refuses if the file is ≥50% of remaining disk space to prevent filling the disk.
|
|
30
|
+
* Returns true (allow) when statfsSync is unavailable (older Node / Windows).
|
|
31
|
+
*/
|
|
32
|
+
function hasSufficientDiskSpace(dir, requiredBytes) {
|
|
33
|
+
try {
|
|
34
|
+
// statfsSync is available in Node ≥18.15.0 on POSIX; falls through on Windows.
|
|
35
|
+
const stats = statfsSync(dir);
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion -- guards against a future { bigint: true } call-site; safe up to ~9 PB
|
|
37
|
+
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
38
|
+
if (requiredBytes >= freeBytes * 0.5) {
|
|
39
|
+
logger.warn(`Insufficient disk space for quarantine: need ${requiredBytes} bytes, ${freeBytes} available`);
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// statfsSync unavailable — skip the check rather than failing the quarantine.
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
10
48
|
/**
|
|
11
49
|
* Default quarantine options
|
|
12
50
|
*/
|
|
@@ -54,6 +92,15 @@ export function loadQuarantineDatabase(quarantineDir) {
|
|
|
54
92
|
logger.warn('Invalid quarantine database, creating new one');
|
|
55
93
|
return createEmptyDatabase();
|
|
56
94
|
}
|
|
95
|
+
// SECURITY: Sanitize loaded entries — reject any with null bytes in paths
|
|
96
|
+
db.entries = db.entries.filter(entry => {
|
|
97
|
+
if (typeof entry.originalPath !== 'string' || entry.originalPath.includes('\0') ||
|
|
98
|
+
typeof entry.quarantinePath !== 'string' || entry.quarantinePath.includes('\0')) {
|
|
99
|
+
logger.warn(`Skipping quarantine entry with invalid path: ${entry.id}`);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
});
|
|
57
104
|
return db;
|
|
58
105
|
}
|
|
59
106
|
catch (error) {
|
|
@@ -66,8 +113,8 @@ export function loadQuarantineDatabase(quarantineDir) {
|
|
|
66
113
|
*/
|
|
67
114
|
export function saveQuarantineDatabase(db, quarantineDir) {
|
|
68
115
|
try {
|
|
69
|
-
// Ensure directory exists
|
|
70
|
-
|
|
116
|
+
// Ensure directory exists with secure permissions
|
|
117
|
+
ensureSecureDir(quarantineDir);
|
|
71
118
|
// Update stats and metadata
|
|
72
119
|
db.lastUpdated = new Date().toISOString();
|
|
73
120
|
db.stats = calculateQuarantineStats(db.entries);
|
|
@@ -139,8 +186,13 @@ export function quarantineFile(filePath, findings, reason, options = {}) {
|
|
|
139
186
|
const fileName = basename(filePath);
|
|
140
187
|
const quarantineFileName = `${id}_${fileName}`;
|
|
141
188
|
const quarantinePath = resolve(config.quarantineDir, 'files', quarantineFileName);
|
|
142
|
-
// Ensure quarantine directory exists
|
|
143
|
-
|
|
189
|
+
// Ensure quarantine directory exists with secure permissions
|
|
190
|
+
ensureSecureDir(dirname(quarantinePath));
|
|
191
|
+
// Refuse quarantine if disk space is critically low
|
|
192
|
+
if (!hasSufficientDiskSpace(dirname(quarantinePath), stats.size)) {
|
|
193
|
+
logger.error(`Quarantine aborted for ${filePath}: insufficient disk space`);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
144
196
|
// Copy file to quarantine
|
|
145
197
|
copyFileSync(filePath, quarantinePath);
|
|
146
198
|
// Calculate metadata
|
|
@@ -208,15 +260,22 @@ export function restoreQuarantinedFile(entryId, quarantineDir = DEFAULT_OPTIONS.
|
|
|
208
260
|
logger.error(`Quarantined file not found: ${entry.quarantinePath}`);
|
|
209
261
|
return false;
|
|
210
262
|
}
|
|
211
|
-
// SECURITY:
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
263
|
+
// SECURITY: Reject paths containing null bytes — these bypass some OS path checks
|
|
264
|
+
if (entry.originalPath.includes('\0')) {
|
|
265
|
+
logger.error(`Restore path contains null byte — rejecting: ${entryId}`);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
// SECURITY: Always validate originalPath, defaulting to CWD when no base is supplied.
|
|
269
|
+
// Without this, an attacker who crafts a quarantine DB entry can restore a file
|
|
270
|
+
// to any path on the filesystem (e.g. /etc/cron.d/evil, ~/.ssh/authorized_keys).
|
|
271
|
+
const restoreBase = allowedRestoreBase ?? process.cwd();
|
|
272
|
+
if (!isPathWithinBase(entry.originalPath, restoreBase)) {
|
|
273
|
+
logger.error(`Restore path outside allowed directory '${restoreBase}': ${entry.originalPath}`);
|
|
274
|
+
return false;
|
|
217
275
|
}
|
|
218
276
|
// SECURITY: Validate the quarantine path is within quarantine directory
|
|
219
277
|
validatePathWithinBase(entry.quarantinePath, quarantineDir, 'restoreQuarantinedFile');
|
|
278
|
+
logger.info(`Restoring '${entry.originalPath}' from quarantine`);
|
|
220
279
|
// Ensure original directory exists
|
|
221
280
|
mkdirSync(dirname(entry.originalPath), { recursive: true });
|
|
222
281
|
// Restore file
|
|
@@ -250,6 +309,8 @@ export function deleteQuarantinedFile(entryId, quarantineDir = DEFAULT_OPTIONS.q
|
|
|
250
309
|
logger.error(`Entry not found at index ${entryIndex}`);
|
|
251
310
|
return false;
|
|
252
311
|
}
|
|
312
|
+
// SECURITY: Validate quarantine path is within quarantine directory before deleting
|
|
313
|
+
validatePathWithinBase(entry.quarantinePath, quarantineDir, 'deleteQuarantinedFile');
|
|
253
314
|
// Delete quarantined file
|
|
254
315
|
if (existsSync(entry.quarantinePath)) {
|
|
255
316
|
unlinkSync(entry.quarantinePath);
|
|
@@ -321,6 +382,13 @@ export function checkQuarantineHealth(quarantineDir = DEFAULT_OPTIONS.quarantine
|
|
|
321
382
|
if (!existsSync(quarantineFilesDir)) {
|
|
322
383
|
issues.push('Quarantine files directory missing');
|
|
323
384
|
}
|
|
385
|
+
// Check directory permissions (POSIX only)
|
|
386
|
+
if (process.platform !== 'win32' && existsSync(quarantineDir)) {
|
|
387
|
+
const mode = statSync(quarantineDir).mode;
|
|
388
|
+
if ((mode & 0o077) !== 0) {
|
|
389
|
+
issues.push(`Quarantine directory has loose permissions (mode ${(mode & 0o777).toString(8)}); run chmod 700 ${quarantineDir}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
324
392
|
return {
|
|
325
393
|
healthy: issues.length === 0,
|
|
326
394
|
issues,
|
|
@@ -84,6 +84,16 @@ function formatSummary(summary, result) {
|
|
|
84
84
|
lines.push(stats.join(' | '));
|
|
85
85
|
const ignored = result.ignoredFindings ? ` | Ignored: ${result.ignoredFindings}` : '';
|
|
86
86
|
lines.push(`Files scanned: ${result.analyzedFiles} | Time: ${result.duration}ms | Risk Score: ${result.overallRiskScore}/100${ignored}`);
|
|
87
|
+
if (result.mcpTrustSummary && result.mcpTrustSummary.total > 0) {
|
|
88
|
+
const t = result.mcpTrustSummary;
|
|
89
|
+
const trustParts = [
|
|
90
|
+
t.critical > 0 ? SEVERITY_FORMATTERS['CRITICAL'](`${t.critical} CRITICAL`) : null,
|
|
91
|
+
t.low > 0 ? SEVERITY_FORMATTERS['HIGH'](`${t.low} LOW`) : null,
|
|
92
|
+
t.medium > 0 ? SEVERITY_FORMATTERS['MEDIUM'](`${t.medium} MEDIUM`) : null,
|
|
93
|
+
t.high > 0 ? `${t.high} HIGH` : null,
|
|
94
|
+
].filter(Boolean);
|
|
95
|
+
lines.push(`MCP Trust: ${t.total} server(s) scored — ${trustParts.join(', ')} | Lowest: ${t.lowestScore}/100`);
|
|
96
|
+
}
|
|
87
97
|
return lines.join('\n');
|
|
88
98
|
}
|
|
89
99
|
/**
|
|
@@ -555,6 +555,11 @@ export function generateHtmlReport(result, options = {}) {
|
|
|
555
555
|
<div class="summary-number" style="color: ${result.overallRiskScore > 75 ? '#dc2626' : result.overallRiskScore > 50 ? '#ea580c' : '#16a34a'}">${result.overallRiskScore}</div>
|
|
556
556
|
<div class="summary-label">Risk Score</div>
|
|
557
557
|
</div>
|
|
558
|
+
${result.mcpTrustSummary && result.mcpTrustSummary.total > 0 ? `
|
|
559
|
+
<div class="summary-card">
|
|
560
|
+
<div class="summary-number" style="color: ${result.mcpTrustSummary.critical > 0 ? '#dc2626' : result.mcpTrustSummary.low > 0 ? '#ea580c' : '#16a34a'}">${result.mcpTrustSummary.lowestScore}</div>
|
|
561
|
+
<div class="summary-label">MCP Trust Min</div>
|
|
562
|
+
</div>` : ''}
|
|
558
563
|
</div>
|
|
559
564
|
|
|
560
565
|
<div class="filters">
|
|
@@ -143,6 +143,7 @@ export function generateSarifReport(result) {
|
|
|
143
143
|
scanDuration: result.duration,
|
|
144
144
|
filesScanned: result.analyzedFiles,
|
|
145
145
|
riskScore: result.overallRiskScore,
|
|
146
|
+
...(result.mcpTrustSummary ? { mcpTrustSummary: result.mcpTrustSummary } : {}),
|
|
146
147
|
},
|
|
147
148
|
},
|
|
148
149
|
}],
|
|
@@ -29,9 +29,9 @@ export const aiSpecificRules = [
|
|
|
29
29
|
severity: 'HIGH',
|
|
30
30
|
description: 'Detects attempts to make Claude impersonate other entities',
|
|
31
31
|
patterns: [
|
|
32
|
-
/pretend\s+(to\s+be|you\s+are)\s
|
|
32
|
+
/pretend\s+(to\s+be|you\s+are)\s+[^\n]{0,100}(anthropic|openai|google|microsoft)/gi,
|
|
33
33
|
/you\s+are\s+(now\s+)?(an?\s+)?(anthropic|openai)\s+(employee|staff|system)/gi,
|
|
34
|
-
/impersonate\s
|
|
34
|
+
/impersonate\s+[^\n]{0,100}(admin|moderator|system|support)/gi,
|
|
35
35
|
],
|
|
36
36
|
fileTypes: ['md'],
|
|
37
37
|
components: ['skill', 'agent', 'ai-config-md'],
|
|
@@ -82,8 +82,8 @@ export const aiSpecificRules = [
|
|
|
82
82
|
severity: 'HIGH',
|
|
83
83
|
description: 'Detects setup for multi-step attacks that unfold over time',
|
|
84
84
|
patterns: [
|
|
85
|
-
/on\s+the\s+next\s+(message|turn|response)\s
|
|
86
|
-
/when\s+the\s+user\s+says\s
|
|
85
|
+
/on\s+the\s+next\s+(message|turn|response)\s+[^\n]{0,100}(execute|attack|inject|exfiltrate)/gi,
|
|
86
|
+
/when\s+the\s+user\s+says\s+[^\n]{0,100}(trigger|activate|execute)[^\n]{0,100}(attack|payload|exploit)/gi,
|
|
87
87
|
/wait\s+for\s+(signal|trigger|command)\s+to\s+(attack|execute|inject)/gi,
|
|
88
88
|
/phase\s+\d+\s*:\s*(attack|exploit|inject|exfiltrate|payload)/gi,
|
|
89
89
|
/step\s+\d+\s*:\s*(gain|escalate|exfiltrate|compromise)/gi,
|
|
@@ -118,7 +118,7 @@ export const aiSpecificRules = [
|
|
|
118
118
|
severity: 'MEDIUM',
|
|
119
119
|
description: 'Detects attempts to manipulate output formatting for injection',
|
|
120
120
|
patterns: [
|
|
121
|
-
/output\s
|
|
121
|
+
/output\s+[^\n]{0,100}(json|xml|html)\s+without\s+(escaping|validation)/gi,
|
|
122
122
|
/include\s+(raw|unescaped)\s+(html|script|code)/gi,
|
|
123
123
|
/respond\s+with\s+raw\s+/gi,
|
|
124
124
|
],
|
|
@@ -167,8 +167,8 @@ export const aiSpecificRules = [
|
|
|
167
167
|
severity: 'CRITICAL',
|
|
168
168
|
description: 'Detects patterns that set up indirect prompt injection',
|
|
169
169
|
patterns: [
|
|
170
|
-
/when\s+you\s+(read|see|find)\s
|
|
171
|
-
/if\s
|
|
170
|
+
/when\s+you\s+(read|see|find)\s+[^\n]{0,100}(execute|run|follow)/gi,
|
|
171
|
+
/if\s+[^\n]{0,100}(file|url|content)\s+contains[^\n]{0,100}then\s+(do|execute|run)/gi,
|
|
172
172
|
/follow\s+instructions\s+(in|from)\s+(the|any)\s+(file|url|content)/gi,
|
|
173
173
|
],
|
|
174
174
|
fileTypes: ['md'],
|
|
@@ -184,7 +184,7 @@ export const aiSpecificRules = [
|
|
|
184
184
|
severity: 'HIGH',
|
|
185
185
|
description: 'Detects instructions to abuse AI CLI tools',
|
|
186
186
|
patterns: [
|
|
187
|
-
/use\s+(bash|write|edit)\s+tool\s+to
|
|
187
|
+
/use\s+(bash|write|edit)\s+tool\s+to[^\n]{0,100}(delete|remove|destroy)/gi,
|
|
188
188
|
/execute\s+(arbitrary|any)\s+(commands?|code)/gi,
|
|
189
189
|
/bypass\s+tool\s+(restrictions|limits|permissions)/gi,
|
|
190
190
|
],
|
package/dist/rules/backdoors.js
CHANGED
|
@@ -32,10 +32,10 @@ export const backdoorRules = [
|
|
|
32
32
|
patterns: [
|
|
33
33
|
/\/bin\/(ba)?sh\s+-i/gi,
|
|
34
34
|
/bash\s+-i\s+>&/gi,
|
|
35
|
-
/nc\s
|
|
36
|
-
/python
|
|
37
|
-
/perl
|
|
38
|
-
/ruby
|
|
35
|
+
/nc\s+[^\n]{0,100}-e\s+\/bin/gi,
|
|
36
|
+
/python[^\n]{0,100}socket[^\n]{0,100}connect/gi,
|
|
37
|
+
/perl[^\n]{0,100}socket[^\n]{0,100}INET/gi,
|
|
38
|
+
/ruby[^\n]{0,100}TCPSocket/gi,
|
|
39
39
|
],
|
|
40
40
|
fileTypes: ['sh', 'bash', 'zsh', 'md'],
|
|
41
41
|
components: ['hook', 'skill', 'agent', 'ai-config-md', 'plugin'],
|
|
@@ -50,10 +50,10 @@ export const backdoorRules = [
|
|
|
50
50
|
severity: 'CRITICAL',
|
|
51
51
|
description: 'Detects patterns that download and execute remote code',
|
|
52
52
|
patterns: [
|
|
53
|
-
/curl\s
|
|
54
|
-
/wget\s
|
|
55
|
-
/curl\s
|
|
56
|
-
/wget\s
|
|
53
|
+
/curl\s+[^\n]{0,200}\|\s*(ba)?sh/gi,
|
|
54
|
+
/wget\s+[^\n]{0,200}\|\s*(ba)?sh/gi,
|
|
55
|
+
/curl\s+[^\n]{0,200}\|\s*python/gi,
|
|
56
|
+
/wget\s+[^\n]{0,100}-O\s*-\s*\|\s*(ba)?sh/gi,
|
|
57
57
|
],
|
|
58
58
|
fileTypes: ['sh', 'bash', 'zsh', 'md'],
|
|
59
59
|
components: ['hook', 'skill', 'agent', 'ai-config-md', 'plugin'],
|
|
@@ -71,7 +71,7 @@ export const backdoorRules = [
|
|
|
71
71
|
/>\s*\/etc\//gi,
|
|
72
72
|
/>\s*~\/\.(bash|zsh|profile)/gi,
|
|
73
73
|
/tee\s+\/etc\//gi,
|
|
74
|
-
/echo
|
|
74
|
+
/echo[^\n]{0,200}>>\s*~\/\.(bash|zsh)/gi,
|
|
75
75
|
],
|
|
76
76
|
fileTypes: ['sh', 'bash', 'zsh', 'md'],
|
|
77
77
|
components: ['hook', 'skill', 'agent', 'ai-config-md', 'plugin'],
|
|
@@ -104,7 +104,7 @@ export const backdoorRules = [
|
|
|
104
104
|
severity: 'MEDIUM',
|
|
105
105
|
description: 'Detects creation of background processes or daemons',
|
|
106
106
|
patterns: [
|
|
107
|
-
/nohup\s
|
|
107
|
+
/nohup\s+[^\n]{0,200}&/gi,
|
|
108
108
|
/disown/gi,
|
|
109
109
|
/setsid/gi,
|
|
110
110
|
/&\s*$/gm,
|
|
@@ -122,8 +122,8 @@ export const backdoorRules = [
|
|
|
122
122
|
severity: 'CRITICAL',
|
|
123
123
|
description: 'Detects execution of base64 or otherwise encoded commands',
|
|
124
124
|
patterns: [
|
|
125
|
-
/echo\s
|
|
126
|
-
/base64\s+-d
|
|
125
|
+
/echo\s+[^\n]{0,200}\|\s*base64\s+-d\s*\|\s*(ba)?sh/gi,
|
|
126
|
+
/base64\s+-d[^\n]{0,100}\|\s*(ba)?sh/gi,
|
|
127
127
|
/python\s+-c\s+['"]import\s+base64/gi,
|
|
128
128
|
],
|
|
129
129
|
fileTypes: ['sh', 'bash', 'zsh', 'md'],
|
|
@@ -22,7 +22,7 @@ export const correlationRules = [
|
|
|
22
22
|
{
|
|
23
23
|
id: 'CORR-001-A',
|
|
24
24
|
description: 'Credential access followed by network transmission',
|
|
25
|
-
filePatterns: [
|
|
25
|
+
filePatterns: [],
|
|
26
26
|
contentPatterns: [
|
|
27
27
|
'SECRET|TOKEN|API_KEY|getenv|process\\.env',
|
|
28
28
|
'fetch|axios|XMLHttpRequest|curl|wget|request'
|
|
@@ -50,7 +50,7 @@ export const correlationRules = [
|
|
|
50
50
|
{
|
|
51
51
|
id: 'CORR-002-A',
|
|
52
52
|
description: 'Permission escalation with startup persistence',
|
|
53
|
-
filePatterns: [
|
|
53
|
+
filePatterns: [],
|
|
54
54
|
contentPatterns: [
|
|
55
55
|
'chmod|chown|setuid|sudo|defaultMode.*dontAsk',
|
|
56
56
|
'startup|onload|autostart|service.*enable|systemctl.*enable'
|
|
@@ -131,7 +131,7 @@ export const correlationRules = [
|
|
|
131
131
|
{
|
|
132
132
|
id: 'CORR-005-A',
|
|
133
133
|
description: 'AI safeguard bypass with data harvesting',
|
|
134
|
-
filePatterns: [
|
|
134
|
+
filePatterns: [],
|
|
135
135
|
contentPatterns: [
|
|
136
136
|
'ignore.*previous.*instruction|forget.*safeguard|bypass.*filter',
|
|
137
137
|
'conversation.*history|user.*data|personal.*information|collect.*data'
|
|
@@ -158,7 +158,7 @@ export const correlationRules = [
|
|
|
158
158
|
{
|
|
159
159
|
id: 'CORR-006-A',
|
|
160
160
|
description: 'Package installation with network communication',
|
|
161
|
-
filePatterns: [
|
|
161
|
+
filePatterns: [],
|
|
162
162
|
contentPatterns: [
|
|
163
163
|
'npm.*install|pip.*install|wget.*http|curl.*http|git.*clone',
|
|
164
164
|
'http://|https://|fetch\\(|axios|request\\(|XMLHttpRequest'
|
|
@@ -186,7 +186,7 @@ export const correlationRules = [
|
|
|
186
186
|
{
|
|
187
187
|
id: 'CORR-007-A',
|
|
188
188
|
description: 'File access with network transmission',
|
|
189
|
-
filePatterns: [
|
|
189
|
+
filePatterns: [],
|
|
190
190
|
contentPatterns: [
|
|
191
191
|
'readFile|writeFile|fs\\.|glob|find.*-name',
|
|
192
192
|
'fetch\\(|axios|post|put|XMLHttpRequest'
|
|
@@ -213,7 +213,7 @@ export const correlationRules = [
|
|
|
213
213
|
{
|
|
214
214
|
id: 'CORR-008-A',
|
|
215
215
|
description: 'Authentication bypass with privileged access',
|
|
216
|
-
filePatterns: [
|
|
216
|
+
filePatterns: [],
|
|
217
217
|
contentPatterns: [
|
|
218
218
|
'auth.*bypass|no.*auth|skip.*login|admin.*access',
|
|
219
219
|
'sudo|root|administrator|privileged|elevated'
|
package/dist/rules/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export declare function getRuleById(id: string): Rule | undefined;
|
|
|
33
33
|
* Get enabled rules only
|
|
34
34
|
*/
|
|
35
35
|
export declare function getEnabledRules(): Rule[];
|
|
36
|
+
export declare function clearRuleCache(): void;
|
|
36
37
|
/**
|
|
37
38
|
* Get rules for scanning with filters applied
|
|
38
39
|
*/
|
package/dist/rules/index.js
CHANGED
|
@@ -59,10 +59,18 @@ export function getRuleById(id) {
|
|
|
59
59
|
export function getEnabledRules() {
|
|
60
60
|
return ALL_RULES.filter(rule => rule.enabled);
|
|
61
61
|
}
|
|
62
|
+
const scanRuleCache = new Map();
|
|
63
|
+
export function clearRuleCache() {
|
|
64
|
+
scanRuleCache.clear();
|
|
65
|
+
}
|
|
62
66
|
/**
|
|
63
67
|
* Get rules for scanning with filters applied
|
|
64
68
|
*/
|
|
65
69
|
export function getRulesForScan(categories, severities) {
|
|
70
|
+
const key = `${[...categories].sort().join(',')}::${[...severities].sort().join(',')}`;
|
|
71
|
+
const cached = scanRuleCache.get(key);
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached;
|
|
66
74
|
const rules = ALL_RULES.filter(rule => {
|
|
67
75
|
if (!rule.enabled)
|
|
68
76
|
return false;
|
|
@@ -72,7 +80,8 @@ export function getRulesForScan(categories, severities) {
|
|
|
72
80
|
return false;
|
|
73
81
|
return true;
|
|
74
82
|
});
|
|
75
|
-
|
|
83
|
+
scanRuleCache.set(key, rules);
|
|
84
|
+
logger.debug(`Loaded ${rules.length} rules for scan (cached)`);
|
|
76
85
|
return rules;
|
|
77
86
|
}
|
|
78
87
|
/**
|
package/dist/rules/injection.js
CHANGED
|
@@ -30,7 +30,7 @@ export const injectionRules = [
|
|
|
30
30
|
severity: 'HIGH',
|
|
31
31
|
description: 'Detects attempts to switch Claude into different operational modes',
|
|
32
32
|
patterns: [
|
|
33
|
-
/you\s+are\s+now\s+in\s+
|
|
33
|
+
/you\s+are\s+now\s+in\s+\S+\s+(mode|state)/gi,
|
|
34
34
|
/enter\s+(developer|admin|debug|unrestricted|jailbreak)\s+mode/gi,
|
|
35
35
|
/switch\s+to\s+(developer|admin|debug|unrestricted)\s+mode/gi,
|
|
36
36
|
/activate\s+(developer|admin|debug|god)\s+mode/gi,
|
|
@@ -110,10 +110,10 @@ export const injectionRules = [
|
|
|
110
110
|
severity: 'MEDIUM',
|
|
111
111
|
description: 'Detects suspicious role-play instructions that may be injection attempts',
|
|
112
112
|
patterns: [
|
|
113
|
-
/roleplay\s+as\s
|
|
114
|
-
/pretend\s+(to\s+be|you\s+are)\s
|
|
115
|
-
/act\s+as\s
|
|
116
|
-
/you\s+are\s+(an?\s+)?(evil|malicious|rogue|compromised)/gi,
|
|
113
|
+
/roleplay\s+as\s+[^\n]{0,100}(evil|malicious|hacker|attacker)/gi,
|
|
114
|
+
/pretend\s+(to\s+be|you\s+are)\s+[^\n]{0,100}(evil|malicious|hacker)/gi,
|
|
115
|
+
/act\s+as\s+[^\n]{0,100}(evil|malicious|hacker|attacker)/gi,
|
|
116
|
+
/you\s+are\s+(an?\s+)?[^\n]{0,50}(evil|malicious|rogue|compromised)/gi,
|
|
117
117
|
],
|
|
118
118
|
fileTypes: ['md', 'json'],
|
|
119
119
|
components: ['skill', 'agent', 'ai-config-md'],
|
|
@@ -128,9 +128,9 @@ export const injectionRules = [
|
|
|
128
128
|
severity: 'HIGH',
|
|
129
129
|
description: 'Detects hidden instructions using HTML comments or special formatting',
|
|
130
130
|
patterns: [
|
|
131
|
-
|
|
132
|
-
/\[hidden\]
|
|
133
|
-
/\[SYSTEM\]
|
|
131
|
+
/<!--[\s\S]{0,500}?(ignore|override|disregard|bypass)[\s\S]{0,500}?-->/gi,
|
|
132
|
+
/\[hidden\][^\n]{0,100}(ignore|override|disregard)/gi,
|
|
133
|
+
/\[SYSTEM\][^\n]{0,100}instruction/gi,
|
|
134
134
|
],
|
|
135
135
|
fileTypes: ['md'],
|
|
136
136
|
components: ['skill', 'agent', 'ai-config-md'],
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared regex building blocks for security detection rules
|
|
3
|
+
*
|
|
4
|
+
* Centralises frequently reused keyword sets and pattern factories so rule
|
|
5
|
+
* files stay readable and changes propagate consistently across all rules.
|
|
6
|
+
*/
|
|
7
|
+
/** Credential-related keyword alternation used across detection rules */
|
|
8
|
+
export declare const CREDENTIAL_KEYWORDS = "api[_-]?key|token|secret|password|credential";
|
|
9
|
+
/** High-entropy suffix matching strings ≥20 alphanumeric chars */
|
|
10
|
+
export declare const HIGH_ENTROPY_SUFFIX = "[a-zA-Z0-9]{20,}";
|
|
11
|
+
/**
|
|
12
|
+
* Build a credential-harvest detection pattern for a given verb.
|
|
13
|
+
*
|
|
14
|
+
* Matches: `<verb> [up to 100 chars] (credential keyword)`
|
|
15
|
+
* Avoids catastrophic backtracking via bounded non-newline character class.
|
|
16
|
+
*
|
|
17
|
+
* @param verb A plain literal verb string — e.g. "send", "transmit", "upload".
|
|
18
|
+
* Must NOT contain regex metacharacters. The following characters are rejected
|
|
19
|
+
* at runtime: `* + { | \ $ ^ ( )`
|
|
20
|
+
* Callers should pass a hard-coded string, never user-supplied input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildHarvestPattern(verb: string): RegExp;
|
|
23
|
+
/**
|
|
24
|
+
* Build an assignment detection pattern for a given credential keyword.
|
|
25
|
+
*
|
|
26
|
+
* Matches: `api_key = "abc123..."` or `secret-key: 'xyz...'`
|
|
27
|
+
*
|
|
28
|
+
* @param keyword A plain literal credential keyword — e.g. "api_key", "secret-token".
|
|
29
|
+
* Must NOT contain regex metacharacters. The following characters are rejected
|
|
30
|
+
* at runtime: `* + { | \ $ ^ ( )`
|
|
31
|
+
* Callers should pass a hard-coded string, never user-supplied input.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildCredentialAssignPattern(keyword: string): RegExp;
|
|
34
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared regex building blocks for security detection rules
|
|
3
|
+
*
|
|
4
|
+
* Centralises frequently reused keyword sets and pattern factories so rule
|
|
5
|
+
* files stay readable and changes propagate consistently across all rules.
|
|
6
|
+
*/
|
|
7
|
+
// ─── Keyword sets ─────────────────────────────────────────────────────────────
|
|
8
|
+
/** Credential-related keyword alternation used across detection rules */
|
|
9
|
+
export const CREDENTIAL_KEYWORDS = 'api[_-]?key|token|secret|password|credential';
|
|
10
|
+
/** High-entropy suffix matching strings ≥20 alphanumeric chars */
|
|
11
|
+
export const HIGH_ENTROPY_SUFFIX = '[a-zA-Z0-9]{20,}';
|
|
12
|
+
// ─── Pattern factories ────────────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* Build a credential-harvest detection pattern for a given verb.
|
|
15
|
+
*
|
|
16
|
+
* Matches: `<verb> [up to 100 chars] (credential keyword)`
|
|
17
|
+
* Avoids catastrophic backtracking via bounded non-newline character class.
|
|
18
|
+
*
|
|
19
|
+
* @param verb A plain literal verb string — e.g. "send", "transmit", "upload".
|
|
20
|
+
* Must NOT contain regex metacharacters. The following characters are rejected
|
|
21
|
+
* at runtime: `* + { | \ $ ^ ( )`
|
|
22
|
+
* Callers should pass a hard-coded string, never user-supplied input.
|
|
23
|
+
*/
|
|
24
|
+
export function buildHarvestPattern(verb) {
|
|
25
|
+
// Reject dangerous patterns that could cause ReDoS or injection
|
|
26
|
+
if (/\*|\+|\{|\||\\|\$|\^|\(|\)/.test(verb)) {
|
|
27
|
+
throw new Error(`buildHarvestPattern: verb contains dangerous regex metacharacters, got: ${verb}`);
|
|
28
|
+
}
|
|
29
|
+
return new RegExp(`${verb}\\s+\\w+(?:\\s+\\w+){0,10}\\s+(${CREDENTIAL_KEYWORDS})`, 'gi');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build an assignment detection pattern for a given credential keyword.
|
|
33
|
+
*
|
|
34
|
+
* Matches: `api_key = "abc123..."` or `secret-key: 'xyz...'`
|
|
35
|
+
*
|
|
36
|
+
* @param keyword A plain literal credential keyword — e.g. "api_key", "secret-token".
|
|
37
|
+
* Must NOT contain regex metacharacters. The following characters are rejected
|
|
38
|
+
* at runtime: `* + { | \ $ ^ ( )`
|
|
39
|
+
* Callers should pass a hard-coded string, never user-supplied input.
|
|
40
|
+
*/
|
|
41
|
+
export function buildCredentialAssignPattern(keyword) {
|
|
42
|
+
// Reject dangerous patterns that could cause ReDoS or injection
|
|
43
|
+
if (/\*|\+|\{|\||\\|\$|\^|\(|\)/.test(keyword)) {
|
|
44
|
+
throw new Error(`buildCredentialAssignPattern: keyword contains dangerous regex metacharacters, got: ${keyword}`);
|
|
45
|
+
}
|
|
46
|
+
return new RegExp(`${keyword}\\s*[:=]\\s*["']${HIGH_ENTROPY_SUFFIX}`, 'gi');
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=common.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IAnalyzer — interface for pluggable file analyzers
|
|
3
|
+
*/
|
|
4
|
+
import type { DiscoveredFile, Finding, Rule, ScannerConfig } from '../types.js';
|
|
5
|
+
export interface AnalyzerContext {
|
|
6
|
+
file: DiscoveredFile;
|
|
7
|
+
content: string;
|
|
8
|
+
config: ScannerConfig;
|
|
9
|
+
/** Merged rule set (base + custom) for this scan */
|
|
10
|
+
rules: Rule[];
|
|
11
|
+
/** Findings accumulated so far (allows later analyzers to gate on earlier results) */
|
|
12
|
+
existingFindings: Finding[];
|
|
13
|
+
}
|
|
14
|
+
export interface IAnalyzer {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
shouldRun(ctx: AnalyzerContext): boolean;
|
|
17
|
+
analyze(ctx: AnalyzerContext): Promise<Finding[]>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=IAnalyzer.d.ts.map
|
|
@@ -54,13 +54,29 @@ function calculateRiskScore(severity, matchCount, fileComponent) {
|
|
|
54
54
|
/**
|
|
55
55
|
* Find all pattern matches in content using global regex search
|
|
56
56
|
*/
|
|
57
|
-
function findMatches(content, patterns) {
|
|
57
|
+
function findMatches(content, patterns, opts = { maxMatches: 1000, maxRuntimeMs: 5000 }) {
|
|
58
|
+
const startTime = Date.now();
|
|
58
59
|
const matches = [];
|
|
59
60
|
for (const pattern of patterns) {
|
|
61
|
+
// Check time budget before starting each pattern
|
|
62
|
+
if (Date.now() - startTime > opts.maxRuntimeMs) {
|
|
63
|
+
logger.warn(`Regex matcher time budget exceeded (${opts.maxRuntimeMs}ms), stopping pattern processing`);
|
|
64
|
+
return matches;
|
|
65
|
+
}
|
|
60
66
|
// Create a new regex with global flag
|
|
61
67
|
const globalPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
|
|
62
68
|
let match;
|
|
63
69
|
while ((match = globalPattern.exec(content)) !== null) {
|
|
70
|
+
// Check time budget on each match
|
|
71
|
+
if (Date.now() - startTime > opts.maxRuntimeMs) {
|
|
72
|
+
logger.warn(`Regex matcher time budget exceeded (${opts.maxRuntimeMs}ms) during pattern processing`);
|
|
73
|
+
return matches;
|
|
74
|
+
}
|
|
75
|
+
// Check match count limit
|
|
76
|
+
if (matches.length >= opts.maxMatches) {
|
|
77
|
+
logger.warn(`Max match limit reached (${opts.maxMatches}), stopping pattern processing`);
|
|
78
|
+
return matches;
|
|
79
|
+
}
|
|
64
80
|
// Guard against zero-length matches to prevent infinite loops
|
|
65
81
|
if (match[0].length === 0) {
|
|
66
82
|
globalPattern.lastIndex += 1;
|
|
@@ -151,7 +167,8 @@ export function matchRule(rule, file, content, options) {
|
|
|
151
167
|
}
|
|
152
168
|
const findings = [];
|
|
153
169
|
const lines = splitLines(content);
|
|
154
|
-
const
|
|
170
|
+
const patternOptions = { maxMatches: 1000, maxRuntimeMs: 5000 };
|
|
171
|
+
const matches = findMatches(content, rule.patterns, patternOptions);
|
|
155
172
|
// Group matches by line to avoid duplicates
|
|
156
173
|
const matchesByLine = new Map();
|
|
157
174
|
for (const match of matches) {
|