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
package/dist/utils/baseline.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Baseline Management - Track and ignore accepted findings
|
|
3
3
|
* Allows users to create baselines of known/accepted security findings
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { statSync } from 'node:fs';
|
|
6
|
+
import { writeFile, readFile, mkdir, access } from 'node:fs/promises';
|
|
6
7
|
import { resolve, dirname, extname } from 'node:path';
|
|
7
8
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { mkdirSync } from 'node:fs';
|
|
9
9
|
import logger from './logger.js';
|
|
10
10
|
/**
|
|
11
11
|
* Generate a hash for a finding to uniquely identify it
|
|
@@ -14,20 +14,51 @@ function generateFindingHash(finding) {
|
|
|
14
14
|
const content = `${finding.ruleId}:${finding.relativePath}:${finding.line}:${finding.match}`;
|
|
15
15
|
return createHash('sha256').update(content).digest('hex');
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Compute integrity hash of a baseline (excluding the integrity field itself)
|
|
19
|
+
*/
|
|
20
|
+
export function computeBaselineIntegrity(baseline) {
|
|
21
|
+
const payload = JSON.stringify({
|
|
22
|
+
version: baseline.version,
|
|
23
|
+
createdDate: baseline.createdDate,
|
|
24
|
+
lastUpdated: baseline.lastUpdated,
|
|
25
|
+
description: baseline.description,
|
|
26
|
+
findings: baseline.findings,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
algorithm: 'sha256',
|
|
30
|
+
hash: createHash('sha256').update(payload).digest('hex'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Verify that a loaded baseline has not been tampered with
|
|
35
|
+
*/
|
|
36
|
+
export function verifyBaselineIntegrity(baseline) {
|
|
37
|
+
if (!baseline.integrity) {
|
|
38
|
+
return true; // Old baselines without integrity field are accepted
|
|
39
|
+
}
|
|
40
|
+
const expected = computeBaselineIntegrity(baseline);
|
|
41
|
+
return expected.hash === baseline.integrity.hash;
|
|
42
|
+
}
|
|
17
43
|
/**
|
|
18
44
|
* Load baseline from file
|
|
19
45
|
*/
|
|
20
|
-
export function loadBaseline(baselinePath) {
|
|
46
|
+
export async function loadBaseline(baselinePath) {
|
|
21
47
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
48
|
+
await access(baselinePath);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const content = await readFile(baselinePath, 'utf-8');
|
|
26
55
|
const baseline = JSON.parse(content);
|
|
27
|
-
// Validate baseline structure
|
|
28
56
|
if (!baseline.version || !baseline.findings || !Array.isArray(baseline.findings)) {
|
|
29
57
|
throw new Error('Invalid baseline format');
|
|
30
58
|
}
|
|
59
|
+
if (baseline.integrity && !verifyBaselineIntegrity(baseline)) {
|
|
60
|
+
logger.warn(`Baseline integrity check failed for ${baselinePath} — file may have been tampered with`);
|
|
61
|
+
}
|
|
31
62
|
logger.debug(`Loaded baseline with ${baseline.findings.length} accepted findings`);
|
|
32
63
|
return baseline;
|
|
33
64
|
}
|
|
@@ -39,17 +70,17 @@ export function loadBaseline(baselinePath) {
|
|
|
39
70
|
/**
|
|
40
71
|
* Save baseline to file
|
|
41
72
|
*/
|
|
42
|
-
export function saveBaseline(baseline, baselinePath) {
|
|
73
|
+
export async function saveBaseline(baseline, baselinePath) {
|
|
43
74
|
try {
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const content = JSON.stringify(
|
|
51
|
-
|
|
52
|
-
logger.info(`Baseline saved to ${baselinePath} with ${
|
|
75
|
+
await mkdir(dirname(baselinePath), { recursive: true });
|
|
76
|
+
const updated = { ...baseline, lastUpdated: new Date().toISOString() };
|
|
77
|
+
const baselineWithIntegrity = {
|
|
78
|
+
...updated,
|
|
79
|
+
integrity: computeBaselineIntegrity(updated),
|
|
80
|
+
};
|
|
81
|
+
const content = JSON.stringify(baselineWithIntegrity, null, 2);
|
|
82
|
+
await writeFile(baselinePath, content, 'utf-8');
|
|
83
|
+
logger.info(`Baseline saved to ${baselinePath} with ${baselineWithIntegrity.findings.length} findings`);
|
|
53
84
|
}
|
|
54
85
|
catch (error) {
|
|
55
86
|
logger.error(`Failed to save baseline to ${baselinePath}:`, error);
|
|
@@ -227,7 +258,7 @@ export function getDefaultBaselinePath(scanPaths) {
|
|
|
227
258
|
// Try to find a good location for baseline file
|
|
228
259
|
const firstPath = scanPaths[0] ?? process.cwd();
|
|
229
260
|
try {
|
|
230
|
-
if (
|
|
261
|
+
if (statSync(firstPath).isFile()) {
|
|
231
262
|
return resolve(dirname(firstPath), '.ferret-baseline.json');
|
|
232
263
|
}
|
|
233
264
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU-bounded in-memory cache for file content.
|
|
3
|
+
*
|
|
4
|
+
* Prevents unbounded memory growth when the scanner reads thousands of files:
|
|
5
|
+
* - Per-file cap: individual files larger than `maxFileSize` bytes are never cached.
|
|
6
|
+
* - Aggregate cap: once `totalBytes` would exceed `maxBytes`, the least-recently-used
|
|
7
|
+
* entry is evicted first. Same policy applies to the `maxEntries` cap.
|
|
8
|
+
*
|
|
9
|
+
* Insertion order of a Map mirrors access order after each get() refresh,
|
|
10
|
+
* giving us O(1) LRU with the iteration-based eviction below.
|
|
11
|
+
*/
|
|
12
|
+
export interface BoundedContentCacheOpts {
|
|
13
|
+
/** Maximum total cached bytes. Default: 256 MB. */
|
|
14
|
+
maxBytes?: number;
|
|
15
|
+
/** Maximum number of cached entries. Default: 10 000. */
|
|
16
|
+
maxEntries?: number;
|
|
17
|
+
/** Maximum size of a single file to admit into the cache. Default: 1 MB. */
|
|
18
|
+
maxFileSize?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class BoundedContentCache {
|
|
21
|
+
private readonly map;
|
|
22
|
+
private totalBytes;
|
|
23
|
+
private readonly maxBytes;
|
|
24
|
+
private readonly maxEntries;
|
|
25
|
+
private readonly maxFileSize;
|
|
26
|
+
constructor(opts?: BoundedContentCacheOpts);
|
|
27
|
+
set(path: string, content: string): void;
|
|
28
|
+
get(path: string): string | undefined;
|
|
29
|
+
has(path: string): boolean;
|
|
30
|
+
/** Number of cached entries. */
|
|
31
|
+
size(): number;
|
|
32
|
+
/** Total cached bytes (UTF-8 encoded). */
|
|
33
|
+
bytes(): number;
|
|
34
|
+
/** Expose for CorrelationAnalyzer compatibility (read-only iteration). */
|
|
35
|
+
entries(): IterableIterator<[string, string]>;
|
|
36
|
+
/** Allow spread / array-from for compatibility with Map-based consumers. */
|
|
37
|
+
[Symbol.iterator](): IterableIterator<[string, string]>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=contentCache.d.ts.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LRU-bounded in-memory cache for file content.
|
|
3
|
+
*
|
|
4
|
+
* Prevents unbounded memory growth when the scanner reads thousands of files:
|
|
5
|
+
* - Per-file cap: individual files larger than `maxFileSize` bytes are never cached.
|
|
6
|
+
* - Aggregate cap: once `totalBytes` would exceed `maxBytes`, the least-recently-used
|
|
7
|
+
* entry is evicted first. Same policy applies to the `maxEntries` cap.
|
|
8
|
+
*
|
|
9
|
+
* Insertion order of a Map mirrors access order after each get() refresh,
|
|
10
|
+
* giving us O(1) LRU with the iteration-based eviction below.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_MAX_BYTES = 256 * 1024 * 1024; // 256 MB
|
|
13
|
+
const DEFAULT_MAX_ENTRIES = 10_000;
|
|
14
|
+
const DEFAULT_MAX_FILE = 1_000_000; // 1 MB
|
|
15
|
+
export class BoundedContentCache {
|
|
16
|
+
map = new Map();
|
|
17
|
+
totalBytes = 0;
|
|
18
|
+
maxBytes;
|
|
19
|
+
maxEntries;
|
|
20
|
+
maxFileSize;
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
23
|
+
this.maxEntries = opts.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
24
|
+
this.maxFileSize = opts.maxFileSize ?? DEFAULT_MAX_FILE;
|
|
25
|
+
}
|
|
26
|
+
set(path, content) {
|
|
27
|
+
const incoming = Buffer.byteLength(content, 'utf-8');
|
|
28
|
+
// Refuse files that exceed the per-file cap.
|
|
29
|
+
if (incoming > this.maxFileSize)
|
|
30
|
+
return;
|
|
31
|
+
// If the key already exists, remove its contribution before re-inserting.
|
|
32
|
+
const existing = this.map.get(path);
|
|
33
|
+
if (existing !== undefined) {
|
|
34
|
+
this.map.delete(path);
|
|
35
|
+
this.totalBytes -= Buffer.byteLength(existing, 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
// Evict the oldest (first-in-map) entries until this one fits.
|
|
38
|
+
while (this.map.size > 0 &&
|
|
39
|
+
(this.totalBytes + incoming > this.maxBytes || this.map.size >= this.maxEntries)) {
|
|
40
|
+
const oldestKey = this.map.keys().next().value;
|
|
41
|
+
const oldestVal = this.map.get(oldestKey);
|
|
42
|
+
this.map.delete(oldestKey);
|
|
43
|
+
this.totalBytes -= Buffer.byteLength(oldestVal, 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
this.map.set(path, content);
|
|
46
|
+
this.totalBytes += incoming;
|
|
47
|
+
}
|
|
48
|
+
get(path) {
|
|
49
|
+
const val = this.map.get(path);
|
|
50
|
+
if (val === undefined)
|
|
51
|
+
return undefined;
|
|
52
|
+
// Refresh to most-recently-used position (LRU via Map insertion order).
|
|
53
|
+
this.map.delete(path);
|
|
54
|
+
this.map.set(path, val);
|
|
55
|
+
return val;
|
|
56
|
+
}
|
|
57
|
+
has(path) {
|
|
58
|
+
return this.map.has(path);
|
|
59
|
+
}
|
|
60
|
+
/** Number of cached entries. */
|
|
61
|
+
size() {
|
|
62
|
+
return this.map.size;
|
|
63
|
+
}
|
|
64
|
+
/** Total cached bytes (UTF-8 encoded). */
|
|
65
|
+
bytes() {
|
|
66
|
+
return this.totalBytes;
|
|
67
|
+
}
|
|
68
|
+
/** Expose for CorrelationAnalyzer compatibility (read-only iteration). */
|
|
69
|
+
entries() {
|
|
70
|
+
return this.map.entries();
|
|
71
|
+
}
|
|
72
|
+
/** Allow spread / array-from for compatibility with Map-based consumers. */
|
|
73
|
+
[Symbol.iterator]() {
|
|
74
|
+
return this.map.entries();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=contentCache.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe glob-to-regex conversion utility
|
|
3
|
+
*
|
|
4
|
+
* Prevents regex injection attacks and ReDoS by escaping metacharacters
|
|
5
|
+
* and bounding wildcard replacements.
|
|
6
|
+
*/
|
|
7
|
+
export interface GlobOptions {
|
|
8
|
+
/** Whether to anchor with ^$ (default: true) */
|
|
9
|
+
anchored?: boolean;
|
|
10
|
+
/** Whether this is a file path (affects wildcard replacement) */
|
|
11
|
+
pathLike?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Convert a glob pattern to a safe RegExp with bounded wildcards.
|
|
15
|
+
*
|
|
16
|
+
* - Escapes all regex metacharacters except `*`
|
|
17
|
+
* - Replaces `*` with bounded character classes to prevent ReDoS
|
|
18
|
+
* - Anchors patterns to prevent unintended substring matches
|
|
19
|
+
* - Caches compiled patterns for performance
|
|
20
|
+
*
|
|
21
|
+
* @param glob The glob pattern (e.g. "*.env", "CRED-*")
|
|
22
|
+
* @param opts Configuration options
|
|
23
|
+
* @returns A safe RegExp that won't cause ReDoS or over-match
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // File pattern matching
|
|
28
|
+
* const filePattern = globToRegex("*.env", { pathLike: true });
|
|
29
|
+
* filePattern.test("/path/to/file.env"); // true
|
|
30
|
+
* filePattern.test("file.env.backup"); // false (anchored)
|
|
31
|
+
*
|
|
32
|
+
* // Rule ID pattern matching
|
|
33
|
+
* const rulePattern = globToRegex("CRED-*");
|
|
34
|
+
* rulePattern.test("CRED-001"); // true
|
|
35
|
+
* rulePattern.test("CREDENTIAL-001"); // false (literal dot required)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function globToRegex(glob: string, opts?: GlobOptions): RegExp;
|
|
39
|
+
/**
|
|
40
|
+
* Clear the compiled pattern cache (useful for testing)
|
|
41
|
+
*/
|
|
42
|
+
export declare function clearCache(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Get cache statistics (useful for debugging)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getCacheStats(): {
|
|
47
|
+
size: number;
|
|
48
|
+
keys: string[];
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe glob-to-regex conversion utility
|
|
3
|
+
*
|
|
4
|
+
* Prevents regex injection attacks and ReDoS by escaping metacharacters
|
|
5
|
+
* and bounding wildcard replacements.
|
|
6
|
+
*/
|
|
7
|
+
// Regex metacharacters that need escaping (all except asterisk)
|
|
8
|
+
const REGEX_META = /[.+?^${}()|[\]\\]/g;
|
|
9
|
+
// Cache compiled regexes to avoid recompilation in hot paths
|
|
10
|
+
const cache = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Convert a glob pattern to a safe RegExp with bounded wildcards.
|
|
13
|
+
*
|
|
14
|
+
* - Escapes all regex metacharacters except `*`
|
|
15
|
+
* - Replaces `*` with bounded character classes to prevent ReDoS
|
|
16
|
+
* - Anchors patterns to prevent unintended substring matches
|
|
17
|
+
* - Caches compiled patterns for performance
|
|
18
|
+
*
|
|
19
|
+
* @param glob The glob pattern (e.g. "*.env", "CRED-*")
|
|
20
|
+
* @param opts Configuration options
|
|
21
|
+
* @returns A safe RegExp that won't cause ReDoS or over-match
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* // File pattern matching
|
|
26
|
+
* const filePattern = globToRegex("*.env", { pathLike: true });
|
|
27
|
+
* filePattern.test("/path/to/file.env"); // true
|
|
28
|
+
* filePattern.test("file.env.backup"); // false (anchored)
|
|
29
|
+
*
|
|
30
|
+
* // Rule ID pattern matching
|
|
31
|
+
* const rulePattern = globToRegex("CRED-*");
|
|
32
|
+
* rulePattern.test("CRED-001"); // true
|
|
33
|
+
* rulePattern.test("CREDENTIAL-001"); // false (literal dot required)
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function globToRegex(glob, opts = {}) {
|
|
37
|
+
const anchored = opts.anchored !== false;
|
|
38
|
+
const pathLike = opts.pathLike ?? false;
|
|
39
|
+
// Create cache key including options
|
|
40
|
+
const key = `${glob}::${anchored}::${pathLike}`;
|
|
41
|
+
// Return cached pattern if available
|
|
42
|
+
const hit = cache.get(key);
|
|
43
|
+
if (hit) {
|
|
44
|
+
return hit;
|
|
45
|
+
}
|
|
46
|
+
// Escape all regex metacharacters except asterisk
|
|
47
|
+
const escaped = glob.replace(REGEX_META, '\\$&');
|
|
48
|
+
// Replace asterisk with bounded character class
|
|
49
|
+
// Path-like: match non-newlines (for file paths)
|
|
50
|
+
// Rule-like: match non-whitespace (for rule IDs)
|
|
51
|
+
const wildcard = pathLike
|
|
52
|
+
? '[^\\n]{0,200}' // File paths: no newlines, bound to 200 chars
|
|
53
|
+
: '[^\\s]{0,200}'; // Rule IDs: no whitespace, bound to 200 chars
|
|
54
|
+
const body = escaped.replace(/\*/g, wildcard);
|
|
55
|
+
// Anchor pattern if requested (default)
|
|
56
|
+
const pattern = anchored ? `^${body}$` : body;
|
|
57
|
+
try {
|
|
58
|
+
const compiled = new RegExp(pattern);
|
|
59
|
+
cache.set(key, compiled);
|
|
60
|
+
return compiled;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Fallback to never-matching pattern if compilation fails
|
|
64
|
+
const fallback = /(?!)/; // Negative lookahead - never matches
|
|
65
|
+
cache.set(key, fallback);
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Clear the compiled pattern cache (useful for testing)
|
|
71
|
+
*/
|
|
72
|
+
export function clearCache() {
|
|
73
|
+
cache.clear();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Get cache statistics (useful for debugging)
|
|
77
|
+
*/
|
|
78
|
+
export function getCacheStats() {
|
|
79
|
+
return {
|
|
80
|
+
size: cache.size,
|
|
81
|
+
keys: Array.from(cache.keys())
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=glob.js.map
|
|
@@ -36,6 +36,7 @@ export function sanitizeFilename(filename) {
|
|
|
36
36
|
.replace(/[\/\\]/g, '_') // Replace path separators
|
|
37
37
|
.replace(/\.\./g, '_') // Replace parent directory references
|
|
38
38
|
.replace(/[<>:"|?*]/g, '_') // Remove invalid filename characters
|
|
39
|
+
.replace(/\0/g, '_') // Replace null bytes
|
|
39
40
|
.replace(/^\.+/, '_'); // Remove leading dots
|
|
40
41
|
}
|
|
41
42
|
/**
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe regex runtime utilities with bounded runtime and match limits.
|
|
3
|
+
*
|
|
4
|
+
* Uses Google RE2 (linear-time engine) when available for categorically
|
|
5
|
+
* safe pattern execution. Falls back to the screened native JS engine.
|
|
6
|
+
* Prevents ReDoS attacks and runaway regex matching in user-controlled patterns.
|
|
7
|
+
*/
|
|
8
|
+
export interface BoundedOptions {
|
|
9
|
+
/** Maximum runtime in milliseconds (default: 1000) */
|
|
10
|
+
maxMs?: number;
|
|
11
|
+
/** Maximum number of matches to collect (default: 500) */
|
|
12
|
+
maxMatches?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface BoundedResult {
|
|
15
|
+
/** Array of captured matches */
|
|
16
|
+
matches: RegExpExecArray[];
|
|
17
|
+
/** Whether runtime was truncated due to time/count limits */
|
|
18
|
+
truncated: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Compile a pattern string into a RegExp (or RE2 instance when available).
|
|
22
|
+
*
|
|
23
|
+
* Tries RE2 first — it is a linear-time engine that categorically eliminates
|
|
24
|
+
* ReDoS. If RE2 is unavailable or rejects the pattern (e.g. lookaheads), falls
|
|
25
|
+
* back to the static ReDoS screener + native RegExp.
|
|
26
|
+
*
|
|
27
|
+
* @param raw The raw pattern string
|
|
28
|
+
* @param flags Regex flags (default: 'gi')
|
|
29
|
+
* @returns Compiled RegExp/RE2 or null if pattern is unsafe/invalid
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const safe = compileSafePattern('test\\d+'); // OK
|
|
34
|
+
* const invalid = compileSafePattern('[unclosed'); // null - syntax error
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function compileSafePattern(raw: string, flags?: string): RegExp | null;
|
|
38
|
+
/**
|
|
39
|
+
* Run a regex against content with bounded runtime and match limits.
|
|
40
|
+
*
|
|
41
|
+
* When RE2 is active the time budget is largely redundant (RE2 is linear),
|
|
42
|
+
* but the match-count ceiling still prevents unbounded result arrays.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runBounded(pattern: RegExp, content: string, options?: BoundedOptions): BoundedResult;
|
|
45
|
+
/**
|
|
46
|
+
* Safe pattern matching that combines compilation and bounded runtime.
|
|
47
|
+
*/
|
|
48
|
+
export declare function safeMatch(rawPattern: string, content: string, flags?: string, options?: BoundedOptions): BoundedResult | null;
|
|
49
|
+
/**
|
|
50
|
+
* Test if a pattern matches content safely, returning boolean result.
|
|
51
|
+
*/
|
|
52
|
+
export declare function safeTest(rawPattern: string, content: string, flags?: string): boolean;
|
|
53
|
+
/** Returns true when RE2 is active (linear-time engine). */
|
|
54
|
+
export declare function isRE2Active(): boolean;
|
|
55
|
+
//# sourceMappingURL=safeRegex.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe regex runtime utilities with bounded runtime and match limits.
|
|
3
|
+
*
|
|
4
|
+
* Uses Google RE2 (linear-time engine) when available for categorically
|
|
5
|
+
* safe pattern execution. Falls back to the screened native JS engine.
|
|
6
|
+
* Prevents ReDoS attacks and runaway regex matching in user-controlled patterns.
|
|
7
|
+
*/
|
|
8
|
+
// Lazy-load RE2 so the module is still usable when re2 is not installed.
|
|
9
|
+
let RE2 = null;
|
|
10
|
+
let re2Attempted = false;
|
|
11
|
+
function getRE2() {
|
|
12
|
+
if (re2Attempted)
|
|
13
|
+
return RE2;
|
|
14
|
+
re2Attempted = true;
|
|
15
|
+
try {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
|
+
RE2 = require('re2');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
RE2 = null;
|
|
21
|
+
}
|
|
22
|
+
return RE2;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compile a pattern string into a RegExp (or RE2 instance when available).
|
|
26
|
+
*
|
|
27
|
+
* Tries RE2 first — it is a linear-time engine that categorically eliminates
|
|
28
|
+
* ReDoS. If RE2 is unavailable or rejects the pattern (e.g. lookaheads), falls
|
|
29
|
+
* back to the static ReDoS screener + native RegExp.
|
|
30
|
+
*
|
|
31
|
+
* @param raw The raw pattern string
|
|
32
|
+
* @param flags Regex flags (default: 'gi')
|
|
33
|
+
* @returns Compiled RegExp/RE2 or null if pattern is unsafe/invalid
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const safe = compileSafePattern('test\\d+'); // OK
|
|
38
|
+
* const invalid = compileSafePattern('[unclosed'); // null - syntax error
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function compileSafePattern(raw, flags = 'gi') {
|
|
42
|
+
const RE2Ctor = getRE2();
|
|
43
|
+
if (RE2Ctor !== null) {
|
|
44
|
+
// RE2 is linear-time — no static ReDoS screening needed.
|
|
45
|
+
// If RE2 rejects the pattern (lookaheads, backreferences) it throws;
|
|
46
|
+
// we fall through to the native screener below.
|
|
47
|
+
try {
|
|
48
|
+
return new RE2Ctor(raw, flags);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Pattern uses features RE2 does not support — fall through.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Fallback: static screen for exponential-backtracking structures before
|
|
55
|
+
// handing the pattern to the native JS engine.
|
|
56
|
+
const redosPatterns = [
|
|
57
|
+
/(\?\+)/, // Possessive quantifier abuse: a+?+
|
|
58
|
+
/(\+\+)/, // Double plus: a++
|
|
59
|
+
/(\*\*)/, // Double star: a**
|
|
60
|
+
/(\(.*\+\)\+)/, // Nested quantifiers: (a+)+
|
|
61
|
+
/(\(.*\*\)\*)/, // Nested quantifiers: (a*)*
|
|
62
|
+
/(\(.*\+\)\{)/, // Quantified groups: (a+){2,}
|
|
63
|
+
/(\(.*\|.*\)\+)/, // Alternation inside quantified group: (a|b)+
|
|
64
|
+
/(\(.*\|.*\)\*)/, // Alternation inside quantified group: (a|b)*
|
|
65
|
+
/(\(.*\|.*\)\{)/, // Alternation inside bounded group: (a|b){2,}
|
|
66
|
+
];
|
|
67
|
+
for (const redos of redosPatterns) {
|
|
68
|
+
if (redos.test(raw)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
return new RegExp(raw, flags);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run a regex against content with bounded runtime and match limits.
|
|
81
|
+
*
|
|
82
|
+
* When RE2 is active the time budget is largely redundant (RE2 is linear),
|
|
83
|
+
* but the match-count ceiling still prevents unbounded result arrays.
|
|
84
|
+
*/
|
|
85
|
+
export function runBounded(pattern, content, options = {}) {
|
|
86
|
+
const maxMs = options.maxMs ?? 1000;
|
|
87
|
+
const maxMatches = options.maxMatches ?? 500;
|
|
88
|
+
const deadline = Date.now() + maxMs;
|
|
89
|
+
const matches = [];
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
92
|
+
if (Date.now() > deadline) {
|
|
93
|
+
return { matches, truncated: true };
|
|
94
|
+
}
|
|
95
|
+
if (matches.length >= maxMatches) {
|
|
96
|
+
return { matches, truncated: true };
|
|
97
|
+
}
|
|
98
|
+
matches.push(match);
|
|
99
|
+
if (!pattern.global) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (match[0].length === 0) {
|
|
103
|
+
pattern.lastIndex++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { matches, truncated: false };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Safe pattern matching that combines compilation and bounded runtime.
|
|
110
|
+
*/
|
|
111
|
+
export function safeMatch(rawPattern, content, flags = 'gi', options = {}) {
|
|
112
|
+
const pattern = compileSafePattern(rawPattern, flags);
|
|
113
|
+
if (pattern === null) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return runBounded(pattern, content, options);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Test if a pattern matches content safely, returning boolean result.
|
|
120
|
+
*/
|
|
121
|
+
export function safeTest(rawPattern, content, flags = 'i') {
|
|
122
|
+
const testFlags = flags.replace(/g/g, '');
|
|
123
|
+
const result = safeMatch(rawPattern, content, testFlags, { maxMatches: 1 });
|
|
124
|
+
return result !== null && result.matches.length > 0 && !result.truncated;
|
|
125
|
+
}
|
|
126
|
+
/** Returns true when RE2 is active (linear-time engine). */
|
|
127
|
+
export function isRE2Active() {
|
|
128
|
+
return getRE2() !== null;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=safeRegex.js.map
|