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.
Files changed (181) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +15 -11
  3. package/bin/ferret.js +109 -13
  4. package/dist/__tests__/AgentMonitor.test.d.ts +6 -0
  5. package/dist/__tests__/AgentMonitor.test.js +235 -0
  6. package/dist/__tests__/AtlasNavigatorReporter.test.d.ts +6 -0
  7. package/dist/__tests__/AtlasNavigatorReporter.test.js +193 -0
  8. package/dist/__tests__/CorrelationAnalyzer.test.d.ts +6 -0
  9. package/dist/__tests__/CorrelationAnalyzer.test.js +211 -0
  10. package/dist/__tests__/IndicatorMatcher.test.d.ts +6 -0
  11. package/dist/__tests__/IndicatorMatcher.test.js +245 -0
  12. package/dist/__tests__/MarketplaceScanner.test.d.ts +5 -0
  13. package/dist/__tests__/MarketplaceScanner.test.js +212 -0
  14. package/dist/__tests__/RuleGenerator.test.d.ts +6 -0
  15. package/dist/__tests__/RuleGenerator.test.js +207 -0
  16. package/dist/__tests__/ThreatFeed.test.d.ts +6 -0
  17. package/dist/__tests__/ThreatFeed.test.js +359 -0
  18. package/dist/__tests__/WatchMode.test.d.ts +6 -0
  19. package/dist/__tests__/WatchMode.test.js +104 -0
  20. package/dist/__tests__/astAnalyzerExtra.test.d.ts +6 -0
  21. package/dist/__tests__/astAnalyzerExtra.test.js +67 -0
  22. package/dist/__tests__/astAnalyzerFull.test.d.ts +6 -0
  23. package/dist/__tests__/astAnalyzerFull.test.js +138 -0
  24. package/dist/__tests__/astAnalyzerPatterns.test.d.ts +6 -0
  25. package/dist/__tests__/astAnalyzerPatterns.test.js +143 -0
  26. package/dist/__tests__/atlas.test.d.ts +6 -0
  27. package/dist/__tests__/atlas.test.js +319 -0
  28. package/dist/__tests__/atlasCatalog.test.d.ts +6 -0
  29. package/dist/__tests__/atlasCatalog.test.js +200 -0
  30. package/dist/__tests__/atlasCatalogExtra.test.d.ts +6 -0
  31. package/dist/__tests__/atlasCatalogExtra.test.js +215 -0
  32. package/dist/__tests__/baseline.test.d.ts +6 -0
  33. package/dist/__tests__/baseline.test.js +321 -0
  34. package/dist/__tests__/baselineExtra.test.d.ts +6 -0
  35. package/dist/__tests__/baselineExtra.test.js +317 -0
  36. package/dist/__tests__/capabilityMapping.test.d.ts +5 -0
  37. package/dist/__tests__/capabilityMapping.test.js +49 -0
  38. package/dist/__tests__/capabilityMappingExtra.test.d.ts +5 -0
  39. package/dist/__tests__/capabilityMappingExtra.test.js +200 -0
  40. package/dist/__tests__/complianceExtra.test.d.ts +6 -0
  41. package/dist/__tests__/complianceExtra.test.js +121 -0
  42. package/dist/__tests__/config.test.js +1 -1
  43. package/dist/__tests__/configLoader.test.d.ts +6 -0
  44. package/dist/__tests__/configLoader.test.js +225 -0
  45. package/dist/__tests__/configLoaderExtra.test.d.ts +6 -0
  46. package/dist/__tests__/configLoaderExtra.test.js +186 -0
  47. package/dist/__tests__/correlationAnalyzerExtra.test.d.ts +5 -0
  48. package/dist/__tests__/correlationAnalyzerExtra.test.js +98 -0
  49. package/dist/__tests__/correlationAnalyzerFull.test.d.ts +6 -0
  50. package/dist/__tests__/correlationAnalyzerFull.test.js +154 -0
  51. package/dist/__tests__/customRules.extra.test.d.ts +6 -0
  52. package/dist/__tests__/customRules.extra.test.js +245 -0
  53. package/dist/__tests__/customRules.test.d.ts +7 -0
  54. package/dist/__tests__/customRules.test.js +347 -0
  55. package/dist/__tests__/dependencyRisk.test.d.ts +5 -0
  56. package/dist/__tests__/dependencyRisk.test.js +248 -0
  57. package/dist/__tests__/dependencyRiskExtra.test.d.ts +6 -0
  58. package/dist/__tests__/dependencyRiskExtra.test.js +177 -0
  59. package/dist/__tests__/featureExitCodes.test.d.ts +7 -0
  60. package/dist/__tests__/featureExitCodes.test.js +332 -0
  61. package/dist/__tests__/fileDiscoveryConfigOnly.test.d.ts +6 -0
  62. package/dist/__tests__/fileDiscoveryConfigOnly.test.js +195 -0
  63. package/dist/__tests__/fileDiscoveryExtra.test.d.ts +6 -0
  64. package/dist/__tests__/fileDiscoveryExtra.test.js +149 -0
  65. package/dist/__tests__/fixer.extra.test.d.ts +6 -0
  66. package/dist/__tests__/fixer.extra.test.js +135 -0
  67. package/dist/__tests__/fixerApply.test.d.ts +6 -0
  68. package/dist/__tests__/fixerApply.test.js +132 -0
  69. package/dist/__tests__/gitHooks.test.d.ts +7 -0
  70. package/dist/__tests__/gitHooks.test.js +188 -0
  71. package/dist/__tests__/htmlReporter.extra.test.d.ts +5 -0
  72. package/dist/__tests__/htmlReporter.extra.test.js +126 -0
  73. package/dist/__tests__/interactiveTui.test.d.ts +6 -0
  74. package/dist/__tests__/interactiveTui.test.js +180 -0
  75. package/dist/__tests__/interactiveTuiCommands.test.d.ts +6 -0
  76. package/dist/__tests__/interactiveTuiCommands.test.js +187 -0
  77. package/dist/__tests__/interactiveTuiMore.test.d.ts +6 -0
  78. package/dist/__tests__/interactiveTuiMore.test.js +194 -0
  79. package/dist/__tests__/interactiveTuiSession.test.d.ts +6 -0
  80. package/dist/__tests__/interactiveTuiSession.test.js +173 -0
  81. package/dist/__tests__/llmAnalysis.test.d.ts +6 -0
  82. package/dist/__tests__/llmAnalysis.test.js +229 -0
  83. package/dist/__tests__/llmAnalysisBuildExcerpt.test.d.ts +6 -0
  84. package/dist/__tests__/llmAnalysisBuildExcerpt.test.js +132 -0
  85. package/dist/__tests__/llmAnalysisExtra.test.d.ts +6 -0
  86. package/dist/__tests__/llmAnalysisExtra.test.js +214 -0
  87. package/dist/__tests__/llmAnalysisFilters.test.d.ts +6 -0
  88. package/dist/__tests__/llmAnalysisFilters.test.js +181 -0
  89. package/dist/__tests__/llmAnalysisMitre.test.d.ts +6 -0
  90. package/dist/__tests__/llmAnalysisMitre.test.js +192 -0
  91. package/dist/__tests__/llmGroqTPM.test.d.ts +6 -0
  92. package/dist/__tests__/llmGroqTPM.test.js +89 -0
  93. package/dist/__tests__/llmProviderRetry.test.d.ts +6 -0
  94. package/dist/__tests__/llmProviderRetry.test.js +172 -0
  95. package/dist/__tests__/mcpValidator.extra.test.d.ts +5 -0
  96. package/dist/__tests__/mcpValidator.extra.test.js +270 -0
  97. package/dist/__tests__/patternMatcherExtra.test.d.ts +7 -0
  98. package/dist/__tests__/patternMatcherExtra.test.js +198 -0
  99. package/dist/__tests__/patternsCommon.test.d.ts +6 -0
  100. package/dist/__tests__/patternsCommon.test.js +107 -0
  101. package/dist/__tests__/policyEnforcement.test.d.ts +5 -0
  102. package/dist/__tests__/policyEnforcement.test.js +510 -0
  103. package/dist/__tests__/quarantineExtra.test.d.ts +5 -0
  104. package/dist/__tests__/quarantineExtra.test.js +214 -0
  105. package/dist/__tests__/redactionExtra.test.d.ts +6 -0
  106. package/dist/__tests__/redactionExtra.test.js +228 -0
  107. package/dist/__tests__/scanDiff.test.d.ts +7 -0
  108. package/dist/__tests__/scanDiff.test.js +266 -0
  109. package/dist/__tests__/scanFull.test.d.ts +6 -0
  110. package/dist/__tests__/scanFull.test.js +158 -0
  111. package/dist/__tests__/scannerDampening.test.d.ts +6 -0
  112. package/dist/__tests__/scannerDampening.test.js +160 -0
  113. package/dist/__tests__/scannerExtra.test.d.ts +6 -0
  114. package/dist/__tests__/scannerExtra.test.js +194 -0
  115. package/dist/__tests__/scannerMitre.test.d.ts +5 -0
  116. package/dist/__tests__/scannerMitre.test.js +141 -0
  117. package/dist/__tests__/scannerSSRF.test.d.ts +5 -0
  118. package/dist/__tests__/scannerSSRF.test.js +149 -0
  119. package/dist/__tests__/schemas.test.d.ts +6 -0
  120. package/dist/__tests__/schemas.test.js +125 -0
  121. package/dist/__tests__/webhooks.extra.test.d.ts +6 -0
  122. package/dist/__tests__/webhooks.extra.test.js +144 -0
  123. package/dist/__tests__/webhooks.test.d.ts +6 -0
  124. package/dist/__tests__/webhooks.test.js +154 -0
  125. package/dist/analyzers/AstAnalyzer.d.ts +5 -1
  126. package/dist/analyzers/AstAnalyzer.js +25 -4
  127. package/dist/features/customRules.js +22 -29
  128. package/dist/features/ignoreComments.js +5 -5
  129. package/dist/features/mcpTrustScore.d.ts +17 -0
  130. package/dist/features/mcpTrustScore.js +74 -0
  131. package/dist/features/mcpValidator.d.ts +2 -0
  132. package/dist/features/mcpValidator.js +13 -0
  133. package/dist/features/policyEnforcement.d.ts +22 -22
  134. package/dist/features/policyEnforcement.js +3 -2
  135. package/dist/intelligence/ThreatFeed.js +207 -62
  136. package/dist/remediation/Fixer.js +56 -30
  137. package/dist/remediation/Quarantine.js +79 -11
  138. package/dist/reporters/ConsoleReporter.js +10 -0
  139. package/dist/reporters/HtmlReporter.js +5 -0
  140. package/dist/reporters/SarifReporter.d.ts +1 -0
  141. package/dist/reporters/SarifReporter.js +1 -0
  142. package/dist/rules/ai-specific.js +8 -8
  143. package/dist/rules/backdoors.js +12 -12
  144. package/dist/rules/correlationRules.js +6 -6
  145. package/dist/rules/index.d.ts +1 -0
  146. package/dist/rules/index.js +10 -1
  147. package/dist/rules/injection.js +8 -8
  148. package/dist/rules/patterns/common.d.ts +34 -0
  149. package/dist/rules/patterns/common.js +48 -0
  150. package/dist/scanner/IAnalyzer.d.ts +19 -0
  151. package/dist/scanner/IAnalyzer.js +5 -0
  152. package/dist/scanner/PatternMatcher.js +19 -2
  153. package/dist/scanner/Scanner.js +64 -125
  154. package/dist/scanner/analyzers/CapabilityAnalyzer.d.ts +8 -0
  155. package/dist/scanner/analyzers/CapabilityAnalyzer.js +19 -0
  156. package/dist/scanner/analyzers/DependencyAnalyzer.d.ts +8 -0
  157. package/dist/scanner/analyzers/DependencyAnalyzer.js +18 -0
  158. package/dist/scanner/analyzers/EntropyAnalyzer.d.ts +8 -0
  159. package/dist/scanner/analyzers/EntropyAnalyzer.js +12 -0
  160. package/dist/scanner/analyzers/LlmAnalyzer.d.ts +17 -0
  161. package/dist/scanner/analyzers/LlmAnalyzer.js +36 -0
  162. package/dist/scanner/analyzers/McpAnalyzer.d.ts +8 -0
  163. package/dist/scanner/analyzers/McpAnalyzer.js +19 -0
  164. package/dist/scanner/analyzers/SemanticAnalyzer.d.ts +8 -0
  165. package/dist/scanner/analyzers/SemanticAnalyzer.js +21 -0
  166. package/dist/scanner/analyzers/ThreatIntelAnalyzer.d.ts +8 -0
  167. package/dist/scanner/analyzers/ThreatIntelAnalyzer.js +21 -0
  168. package/dist/types.d.ts +23 -0
  169. package/dist/types.js +1 -1
  170. package/dist/utils/baseline.d.ts +15 -2
  171. package/dist/utils/baseline.js +50 -19
  172. package/dist/utils/contentCache.d.ts +39 -0
  173. package/dist/utils/contentCache.js +77 -0
  174. package/dist/utils/glob.d.ts +50 -0
  175. package/dist/utils/glob.js +84 -0
  176. package/dist/utils/pathSecurity.js +1 -0
  177. package/dist/utils/safeRegex.d.ts +55 -0
  178. package/dist/utils/safeRegex.js +130 -0
  179. package/dist/utils/schemas.d.ts +70 -64
  180. package/dist/utils/schemas.js +13 -0
  181. package/package.json +34 -19
@@ -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 { writeFileSync, readFileSync, existsSync, statSync } from 'node:fs';
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
- if (!existsSync(baselinePath)) {
23
- return null;
24
- }
25
- const content = readFileSync(baselinePath, 'utf-8');
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
- // Ensure directory exists
45
- const dir = dirname(baselinePath);
46
- mkdirSync(dir, { recursive: true });
47
- // Update lastUpdated timestamp
48
- baseline.lastUpdated = new Date().toISOString();
49
- // Write baseline file
50
- const content = JSON.stringify(baseline, null, 2);
51
- writeFileSync(baselinePath, content, 'utf-8');
52
- logger.info(`Baseline saved to ${baselinePath} with ${baseline.findings.length} findings`);
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 (existsSync(firstPath) && statSync(firstPath).isFile()) {
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