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,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
- mkdirSync(quarantineDir, { recursive: true });
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
- mkdirSync(dirname(quarantinePath), { recursive: true });
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: Validate originalPath if allowedRestoreBase is specified
212
- if (allowedRestoreBase) {
213
- if (!isPathWithinBase(entry.originalPath, allowedRestoreBase)) {
214
- logger.error(`Restore path outside allowed directory: ${entry.originalPath}`);
215
- return false;
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">
@@ -68,6 +68,7 @@ interface SarifDocument {
68
68
  scanDuration: number;
69
69
  filesScanned: number;
70
70
  riskScore: number;
71
+ mcpTrustSummary?: import('../types.js').McpTrustSummary;
71
72
  };
72
73
  };
73
74
  }[];
@@ -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+.*(anthropic|openai|google|microsoft)/gi,
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+.*(admin|moderator|system|support)/gi,
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+.*(execute|attack|inject|exfiltrate)/gi,
86
- /when\s+the\s+user\s+says\s+.*(trigger|activate|execute)\s+.*(attack|payload|exploit)/gi,
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+.*(json|xml|html)\s+without\s+(escaping|validation)/gi,
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+.*(execute|run|follow)/gi,
171
- /if\s+.*(file|url|content)\s+contains.*then\s+(do|execute|run)/gi,
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.*(delete|remove|destroy)/gi,
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
  ],
@@ -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+.*-e\s+\/bin/gi,
36
- /python.*socket.*connect/gi,
37
- /perl.*socket.*INET/gi,
38
- /ruby.*TCPSocket/gi,
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+.*\|\s*(ba)?sh/gi,
54
- /wget\s+.*\|\s*(ba)?sh/gi,
55
- /curl\s+.*\|\s*python/gi,
56
- /wget\s+.*-O\s*-\s*\|\s*(ba)?sh/gi,
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.*>>\s*~\/\.(bash|zsh)/gi,
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+.*&/gi,
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+.*\|\s*base64\s+-d\s*\|\s*(ba)?sh/gi,
126
- /base64\s+-d.*\|\s*(ba)?sh/gi,
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'
@@ -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
  */
@@ -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
- logger.debug(`Loaded ${rules.length} rules for scan`);
83
+ scanRuleCache.set(key, rules);
84
+ logger.debug(`Loaded ${rules.length} rules for scan (cached)`);
76
85
  return rules;
77
86
  }
78
87
  /**
@@ -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+[^\n]{0,100}(mode|state)/gi,
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+.*(evil|malicious|hacker|attacker)/gi,
114
- /pretend\s+(to\s+be|you\s+are)\s+.*(evil|malicious|hacker)/gi,
115
- /act\s+as\s+.*(evil|malicious|hacker|attacker)/gi,
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
- /<!--.*?(ignore|override|disregard|bypass).*?-->/gis,
132
- /\[hidden\].*?(ignore|override|disregard)/gi,
133
- /\[SYSTEM\].*?instruction/gi,
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
@@ -0,0 +1,5 @@
1
+ /**
2
+ * IAnalyzer — interface for pluggable file analyzers
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=IAnalyzer.js.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 matches = findMatches(content, rule.patterns);
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) {