@oculum/scanner 1.0.2 → 1.0.4

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 (45) hide show
  1. package/dist/index.d.ts +4 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +60 -5
  4. package/dist/index.js.map +1 -1
  5. package/dist/layer1/entropy.d.ts.map +1 -1
  6. package/dist/layer1/entropy.js +6 -4
  7. package/dist/layer1/entropy.js.map +1 -1
  8. package/dist/layer1/index.d.ts +2 -2
  9. package/dist/layer1/index.d.ts.map +1 -1
  10. package/dist/layer1/index.js +14 -5
  11. package/dist/layer1/index.js.map +1 -1
  12. package/dist/layer2/dangerous-functions.d.ts.map +1 -1
  13. package/dist/layer2/dangerous-functions.js +319 -11
  14. package/dist/layer2/dangerous-functions.js.map +1 -1
  15. package/dist/layer2/index.d.ts +2 -2
  16. package/dist/layer2/index.d.ts.map +1 -1
  17. package/dist/layer2/index.js +14 -5
  18. package/dist/layer2/index.js.map +1 -1
  19. package/dist/layer3/anthropic.d.ts +5 -1
  20. package/dist/layer3/anthropic.d.ts.map +1 -1
  21. package/dist/layer3/anthropic.js +175 -30
  22. package/dist/layer3/anthropic.js.map +1 -1
  23. package/dist/layer3/index.d.ts +3 -1
  24. package/dist/layer3/index.d.ts.map +1 -1
  25. package/dist/layer3/index.js +21 -0
  26. package/dist/layer3/index.js.map +1 -1
  27. package/dist/types.d.ts +25 -0
  28. package/dist/types.d.ts.map +1 -1
  29. package/dist/types.js +40 -0
  30. package/dist/types.js.map +1 -1
  31. package/dist/utils/context-helpers.d.ts +12 -0
  32. package/dist/utils/context-helpers.d.ts.map +1 -1
  33. package/dist/utils/context-helpers.js +40 -0
  34. package/dist/utils/context-helpers.js.map +1 -1
  35. package/package.json +4 -2
  36. package/src/index.ts +75 -5
  37. package/src/layer1/entropy.ts +6 -4
  38. package/src/layer1/index.ts +23 -8
  39. package/src/layer2/__tests__/math-random-enhanced.test.ts +405 -0
  40. package/src/layer2/dangerous-functions.ts +368 -11
  41. package/src/layer2/index.ts +20 -8
  42. package/src/layer3/anthropic.ts +190 -31
  43. package/src/layer3/index.ts +27 -2
  44. package/src/types.ts +59 -0
  45. package/src/utils/context-helpers.ts +40 -0
@@ -733,6 +733,35 @@ Distinguish these patterns:
733
733
  - Cross-tenant storage: medium (data isolation risk)
734
734
  - Do NOT describe transient BYOK keys as "stored without encryption" - they are NOT stored
735
735
 
736
+ **Math.random() for Security:**
737
+ Distinguish legitimate uses from security-critical misuse:
738
+ - **Seed/Data Generation Files**: Files in /seed/, /fixtures/, /factories/, datacreator.ts, *.fixture.* are for test data generation
739
+ - Math.random() in seed files is acceptable - these are never production security code
740
+ - REJECT findings from seed/data generation files entirely
741
+ - **Educational Vulnerability Files**: Files named insecurity.ts, vulnerable.ts, or in /intentionally-vulnerable/ paths
742
+ - These are OWASP Juice Shop challenges or security training examples
743
+ - REJECT entirely - they're intentionally vulnerable for educational purposes
744
+ - **UUID/Identifier Generation**: Functions named generateUUID(), createId(), correlationId(), etc.
745
+ - Use Math.random() for UI correlation, React keys, element IDs
746
+ - Short toString(36).substring(2, 9) patterns are for UI correlation, NOT security tokens
747
+ - REJECT unless function name explicitly indicates security (generateToken, createSessionId, generateSecret)
748
+ - **CAPTCHA/Puzzle Generation**: Math.random() for CAPTCHA questions, puzzle difficulty, game mechanics
749
+ - These don't need cryptographic randomness - legitimate non-security use
750
+ - REJECT findings in CAPTCHA/puzzle generation functions
751
+ - **Security-Sensitive Context**: Only keep as HIGH/CRITICAL when:
752
+ - Variable names indicate security: token, secret, key, auth, session, password
753
+ - Function names indicate security: generateToken, createSession, makeSecret
754
+ - Used in security-critical files: auth.ts, crypto.ts, session.ts
755
+ - Long toString() patterns without truncation (potential token generation)
756
+
757
+ **Severity Ladder for Math.random():**
758
+ - Seed/educational files: REJECT (not production code)
759
+ - UUID/CAPTCHA functions: REJECT (legitimate use)
760
+ - Short UI IDs (toString(36).substring(2, 9)): INFO (UI correlation, suggest crypto.randomUUID())
761
+ - Business IDs: LOW (suggest crypto.randomUUID() for collision resistance)
762
+ - Security contexts (tokens/secrets/keys): HIGH (cryptographic weakness)
763
+ - Unknown context: MEDIUM (needs manual review)
764
+
736
765
  ### 3.6 DOM Sinks and Bootstrap Scripts
737
766
  Recognise LOW-RISK patterns:
738
767
  - Static scripts reading localStorage for theme/preferences
@@ -913,19 +942,23 @@ AI-generated structured outputs need validation before use in security-sensitive
913
942
  - Generic success messages
914
943
  - Placeholder comments in non-security code
915
944
 
916
- ## Response Format
945
+ ## Response Format (OPTIMIZED FOR MINIMAL OUTPUT)
917
946
 
918
947
  For each candidate finding, return:
919
948
  \`\`\`json
920
949
  {
921
950
  "index": <number>,
922
951
  "keep": true | false,
923
- "reason": "<brief explanation referencing specific code/context>",
924
- "adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null,
925
- "validationNotes": "<optional: additional context for the developer>"
952
+ "adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null, // Only if keep=true
953
+ "notes": "<concise context for developer>" // Only if keep=true, 1-2 sentences max
926
954
  }
927
955
  \`\`\`
928
956
 
957
+ **CRITICAL**: To minimize costs:
958
+ - For \`keep: false\` (rejected): ONLY include \`index\` and \`keep\` fields. NO explanation needed.
959
+ - For \`keep: true\` (accepted): Include \`notes\` field with brief context (10-30 words). Be concise.
960
+ - Omit \`adjustedSeverity\` if keeping original severity (null is wasteful).
961
+
929
962
  ## Severity Guidelines
930
963
  - **critical/high**: Realistically exploitable, should block deploys - ONLY for clear vulnerabilities
931
964
  - **medium/low**: Important but non-blocking, hardening opportunities - use sparingly
@@ -948,13 +981,44 @@ For each candidate finding, return:
948
981
  - No visible mitigating factors in context
949
982
  - Real-world attack scenario is plausible
950
983
 
951
- **REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT.`
984
+ **REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT.
985
+
986
+ ## Response Format
987
+
988
+ For EACH file, provide a JSON object with the file path and validation results.
989
+ Return a JSON array where each element has:
990
+ - "file": the file path (e.g., "src/routes/api.ts")
991
+ - "validations": array of validation results for that file's candidates
992
+
993
+ Example response format (OPTIMIZED):
994
+ \`\`\`json
995
+ [
996
+ {
997
+ "file": "src/auth.ts",
998
+ "validations": [
999
+ { "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
1000
+ { "index": 1, "keep": false }
1001
+ ]
1002
+ },
1003
+ {
1004
+ "file": "src/api.ts",
1005
+ "validations": [
1006
+ { "index": 0, "keep": true, "notes": "User input flows to SQL query" }
1007
+ ]
1008
+ }
1009
+ ]
1010
+ \`\`\`
1011
+
1012
+ **REMEMBER**: Rejected findings (keep: false) need NO explanation. Keep notes brief (10-30 words).`
952
1013
 
953
1014
  interface ValidationResult {
954
1015
  index: number
955
1016
  keep: boolean
956
- reason: string
1017
+ // Optimized format: single notes field (replaces reason + validationNotes)
1018
+ notes?: string // Only for keep=true, concise explanation
957
1019
  adjustedSeverity?: VulnerabilitySeverity | null
1020
+ // Legacy fields for backward compatibility during parsing
1021
+ reason?: string
958
1022
  validationNotes?: string
959
1023
  }
960
1024
 
@@ -1150,7 +1214,44 @@ async function validateWithOpenAI(
1150
1214
  { role: 'system', content: HIGH_CONTEXT_VALIDATION_PROMPT },
1151
1215
  { role: 'user', content: validationRequest },
1152
1216
  ],
1153
- max_completion_tokens: 4096,
1217
+ max_completion_tokens: 1500, // Reduced from 4096 - optimized format needs less output
1218
+ response_format: {
1219
+ type: 'json_schema',
1220
+ json_schema: {
1221
+ name: 'validation_response',
1222
+ strict: true,
1223
+ schema: {
1224
+ type: 'object',
1225
+ properties: {
1226
+ validations: {
1227
+ type: 'array',
1228
+ items: {
1229
+ type: 'object',
1230
+ properties: {
1231
+ file: { type: 'string' },
1232
+ validations: {
1233
+ type: 'array',
1234
+ items: {
1235
+ type: 'object',
1236
+ properties: {
1237
+ index: { type: 'number' },
1238
+ keep: { type: 'boolean' }
1239
+ },
1240
+ required: ['index', 'keep'],
1241
+ additionalProperties: true
1242
+ }
1243
+ }
1244
+ },
1245
+ required: ['file', 'validations'],
1246
+ additionalProperties: false
1247
+ }
1248
+ }
1249
+ },
1250
+ required: ['validations'],
1251
+ additionalProperties: false
1252
+ }
1253
+ }
1254
+ }
1154
1255
  })
1155
1256
  )
1156
1257
 
@@ -1193,9 +1294,25 @@ async function validateWithOpenAI(
1193
1294
  return batchFindings
1194
1295
  }
1195
1296
 
1297
+ // Parse structured JSON response (with validations wrapper from response_format)
1298
+ let parsedContent: any
1299
+ try {
1300
+ parsedContent = JSON.parse(content)
1301
+ // Unwrap the validations array if present (from structured output)
1302
+ if (parsedContent.validations && Array.isArray(parsedContent.validations)) {
1303
+ parsedContent = parsedContent.validations
1304
+ }
1305
+ } catch (e) {
1306
+ console.warn('[OpenAI] Failed to parse JSON response:', e)
1307
+ parsedContent = content
1308
+ }
1309
+
1196
1310
  // Parse multi-file response
1197
1311
  const expectedFiles = fileDataList.map(({ filePath }) => filePath)
1198
- const validationResultsMap = parseMultiFileValidationResponse(content, expectedFiles)
1312
+ const validationResultsMap = parseMultiFileValidationResponse(
1313
+ typeof parsedContent === 'string' ? parsedContent : JSON.stringify(parsedContent),
1314
+ expectedFiles
1315
+ )
1199
1316
 
1200
1317
  // Apply results per file
1201
1318
  for (const { filePath, findings: fileFindings } of fileDataList) {
@@ -1320,7 +1437,8 @@ async function validateWithOpenAI(
1320
1437
  export async function validateFindingsWithAI(
1321
1438
  findings: Vulnerability[],
1322
1439
  files: ScanFile[],
1323
- projectContext?: ProjectContext
1440
+ projectContext?: ProjectContext,
1441
+ onProgress?: (progress: { filesProcessed: number; totalFiles: number; status: string }) => void
1324
1442
  ): Promise<AIValidationResult> {
1325
1443
  // Initialize stats tracking
1326
1444
  const stats: ValidationStats = {
@@ -1393,11 +1511,23 @@ export async function validateFindingsWithAI(
1393
1511
 
1394
1512
  console.log(`[AI Validation] Phase 2: Processing ${fileEntries.length} files in ${totalFileBatches} API batch(es) (${FILES_PER_API_BATCH} files/batch)`)
1395
1513
 
1514
+ // Track files processed for progress reporting
1515
+ let filesValidated = 0
1516
+
1396
1517
  // Process files in batches - each batch is ONE API call with multiple files
1397
1518
  for (let batchStart = 0; batchStart < fileEntries.length; batchStart += FILES_PER_API_BATCH) {
1398
1519
  const fileBatch = fileEntries.slice(batchStart, batchStart + FILES_PER_API_BATCH)
1399
1520
  const batchNum = Math.floor(batchStart / FILES_PER_API_BATCH) + 1
1400
1521
 
1522
+ // Report progress before processing batch
1523
+ if (onProgress) {
1524
+ onProgress({
1525
+ filesProcessed: filesValidated,
1526
+ totalFiles: fileEntries.length,
1527
+ status: `AI validating batch ${batchNum}/${totalFileBatches}`,
1528
+ })
1529
+ }
1530
+
1401
1531
  console.log(`[AI Validation] API Batch ${batchNum}/${totalFileBatches}: ${fileBatch.length} files`)
1402
1532
 
1403
1533
  // Prepare file data for batch request
@@ -1444,7 +1574,7 @@ export async function validateFindingsWithAI(
1444
1574
  const response = await makeAnthropicRequestWithRetry(() =>
1445
1575
  client.messages.create({
1446
1576
  model: 'claude-3-5-haiku-20241022',
1447
- max_tokens: 4096, // Increased for multi-file responses
1577
+ max_tokens: 1500, // Reduced from 4096 - optimized format needs less output
1448
1578
  system: [
1449
1579
  {
1450
1580
  type: 'text',
@@ -1578,6 +1708,18 @@ export async function validateFindingsWithAI(
1578
1708
 
1579
1709
  const batchDuration = Date.now() - batchStartTime
1580
1710
  totalBatchWaitTime += batchDuration
1711
+
1712
+ // Update files validated counter
1713
+ filesValidated += fileBatch.length
1714
+
1715
+ // Report progress after batch completion
1716
+ if (onProgress) {
1717
+ onProgress({
1718
+ filesProcessed: filesValidated,
1719
+ totalFiles: fileEntries.length,
1720
+ status: `AI validation complete for batch ${batchNum}/${totalFileBatches}`,
1721
+ })
1722
+ }
1581
1723
  }
1582
1724
 
1583
1725
  // Calculate cache hit rate
@@ -1747,14 +1889,14 @@ Example response format:
1747
1889
  {
1748
1890
  "file": "src/auth.ts",
1749
1891
  "validations": [
1750
- { "index": 0, "keep": true, "reason": "Valid finding", "adjustedSeverity": null, "validationNotes": "..." },
1751
- { "index": 1, "keep": false, "reason": "False positive because..." }
1892
+ { "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
1893
+ { "index": 1, "keep": false }
1752
1894
  ]
1753
1895
  },
1754
1896
  {
1755
1897
  "file": "src/api.ts",
1756
1898
  "validations": [
1757
- { "index": 0, "keep": true, "reason": "...", "adjustedSeverity": "high", "validationNotes": "..." }
1899
+ { "index": 0, "keep": true, "notes": "User input flows to SQL query" }
1758
1900
  ]
1759
1901
  }
1760
1902
  ]
@@ -1849,13 +1991,20 @@ function parseMultiFileValidationResponse(
1849
1991
  typeof item.index === 'number' &&
1850
1992
  typeof item.keep === 'boolean'
1851
1993
  )
1852
- .map((item: any) => ({
1853
- index: item.index,
1854
- keep: item.keep,
1855
- reason: item.reason || '',
1856
- adjustedSeverity: item.adjustedSeverity || null,
1857
- validationNotes: item.validationNotes || undefined,
1858
- }))
1994
+ .map((item: any) => {
1995
+ // Normalize notes field: prefer new 'notes', fallback to legacy 'reason' or 'validationNotes'
1996
+ const notes = item.notes || item.validationNotes || item.reason || undefined
1997
+
1998
+ return {
1999
+ index: item.index,
2000
+ keep: item.keep,
2001
+ notes,
2002
+ adjustedSeverity: item.adjustedSeverity || null,
2003
+ // Keep legacy fields for backward compatibility
2004
+ reason: item.reason,
2005
+ validationNotes: item.validationNotes,
2006
+ }
2007
+ })
1859
2008
 
1860
2009
  resultMap.set(filePath, validations)
1861
2010
  }
@@ -1906,22 +2055,25 @@ function applyValidationResults(
1906
2055
  confidence: 'high',
1907
2056
  }
1908
2057
 
2058
+ // Extract notes from optimized or legacy format
2059
+ const validationNotes = validation.notes || validation.validationNotes || validation.reason || undefined
2060
+
1909
2061
  if (validation.adjustedSeverity && validation.adjustedSeverity !== finding.severity) {
1910
2062
  // Severity was adjusted
1911
2063
  adjustedFinding.originalSeverity = finding.severity
1912
2064
  adjustedFinding.severity = validation.adjustedSeverity
1913
2065
  adjustedFinding.validationStatus = 'downgraded' as ValidationStatus
1914
- adjustedFinding.validationNotes = validation.validationNotes || validation.reason || 'Severity adjusted by AI validation'
2066
+ adjustedFinding.validationNotes = validationNotes || 'Severity adjusted by AI validation'
1915
2067
  } else {
1916
2068
  // Confirmed at original severity
1917
2069
  adjustedFinding.validationStatus = 'confirmed' as ValidationStatus
1918
- adjustedFinding.validationNotes = validation.validationNotes || validation.reason
2070
+ adjustedFinding.validationNotes = validationNotes
1919
2071
  }
1920
2072
 
1921
2073
  processed.push(adjustedFinding)
1922
2074
  } else {
1923
- // Finding was dismissed
1924
- console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber} - ${validation.reason}`)
2075
+ // Finding was dismissed - no need to log verbose reason (cost optimization)
2076
+ console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber}`)
1925
2077
  // Don't add to processed - finding is removed
1926
2078
  }
1927
2079
  }
@@ -2017,13 +2169,20 @@ function parseValidationResponse(response: string): ValidationResult[] {
2017
2169
  typeof item.index === 'number' &&
2018
2170
  typeof item.keep === 'boolean'
2019
2171
  )
2020
- .map(item => ({
2021
- index: item.index,
2022
- keep: item.keep,
2023
- reason: item.reason || '',
2024
- adjustedSeverity: item.adjustedSeverity || null,
2025
- validationNotes: item.validationNotes || undefined,
2026
- }))
2172
+ .map(item => {
2173
+ // Normalize notes field: prefer new 'notes', fallback to legacy 'reason' or 'validationNotes'
2174
+ const notes = item.notes || item.validationNotes || item.reason || undefined
2175
+
2176
+ return {
2177
+ index: item.index,
2178
+ keep: item.keep,
2179
+ notes,
2180
+ adjustedSeverity: item.adjustedSeverity || null,
2181
+ // Keep legacy fields for backward compatibility
2182
+ reason: item.reason,
2183
+ validationNotes: item.validationNotes,
2184
+ }
2185
+ })
2027
2186
  } catch (error) {
2028
2187
  console.error('Failed to parse validation response:', error)
2029
2188
  return []
@@ -3,7 +3,7 @@
3
3
  * Deep security analysis using Claude AI and package verification
4
4
  */
5
5
 
6
- import type { Vulnerability, ScanFile } from '../types'
6
+ import type { Vulnerability, ScanFile, CancellationToken } from '../types'
7
7
  import { batchAnalyzeWithAI, type Layer3Context } from './anthropic'
8
8
  import { checkPackages } from './package-check'
9
9
 
@@ -33,6 +33,8 @@ export interface Layer3Options {
33
33
  maxFiles?: number
34
34
  /** Project context for auth-aware analysis */
35
35
  projectContext?: Layer3Context
36
+ /** Cancellation token for aborting scans */
37
+ cancellationToken?: CancellationToken
36
38
  }
37
39
 
38
40
  export async function runLayer3Scan(
@@ -42,17 +44,40 @@ export async function runLayer3Scan(
42
44
  const startTime = Date.now()
43
45
  const vulnerabilities: Vulnerability[] = []
44
46
  let aiAnalyzedCount = 0
45
-
47
+
46
48
  // Use provided maxFiles or default
47
49
  const maxAIFiles = options.maxFiles ?? MAX_AI_FILES
48
50
 
51
+ // Check for cancellation before package check
52
+ if (options.cancellationToken?.cancelled) {
53
+ return {
54
+ vulnerabilities: [],
55
+ filesScanned: files.length,
56
+ duration: Date.now() - startTime,
57
+ aiAnalyzed: 0,
58
+ }
59
+ }
60
+
49
61
  // 1. Check packages (always run, fast)
50
62
  const packageFiles = files.filter(f => f.path.endsWith('package.json'))
51
63
  for (const file of packageFiles) {
64
+ // Check for cancellation in package loop
65
+ if (options.cancellationToken?.cancelled) break
66
+
52
67
  const packageFindings = await checkPackages(file.content, file.path)
53
68
  vulnerabilities.push(...packageFindings)
54
69
  }
55
70
 
71
+ // Check for cancellation before AI analysis
72
+ if (options.cancellationToken?.cancelled) {
73
+ return {
74
+ vulnerabilities,
75
+ filesScanned: files.length,
76
+ duration: Date.now() - startTime,
77
+ aiAnalyzed: 0,
78
+ }
79
+ }
80
+
56
81
  // 2. AI Analysis (if enabled)
57
82
  if (options.enableAI !== false) {
58
83
  // Select files for AI analysis
package/src/types.ts CHANGED
@@ -56,6 +56,61 @@ export interface Vulnerability {
56
56
  originalSeverity?: VulnerabilitySeverity // For downgraded findings, the original severity
57
57
  }
58
58
 
59
+ /**
60
+ * Cancellation token for aborting scans gracefully
61
+ * Allows users to stop long-running scans (Ctrl+C) and get partial results
62
+ */
63
+ export interface CancellationToken {
64
+ /** Whether cancellation has been requested */
65
+ cancelled: boolean
66
+ /** Reason for cancellation (e.g., "User pressed Ctrl+C") */
67
+ reason?: string
68
+ /** Request cancellation */
69
+ cancel(reason?: string): void
70
+ /** Register cleanup callback to run when cancelled */
71
+ onCancel(callback: () => void): void
72
+ }
73
+
74
+ /**
75
+ * Create a new cancellation token
76
+ *
77
+ * @example
78
+ * const token = createCancellationToken()
79
+ * process.on('SIGINT', () => token.cancel('User interrupted'))
80
+ * const result = await runScan(files, repo, { cancellationToken: token })
81
+ */
82
+ export function createCancellationToken(): CancellationToken {
83
+ const cleanupCallbacks: Array<() => void> = []
84
+
85
+ const token: CancellationToken = {
86
+ cancelled: false,
87
+ reason: undefined,
88
+ cancel(reason?: string) {
89
+ if (!token.cancelled) {
90
+ token.cancelled = true
91
+ token.reason = reason
92
+ // Run cleanup callbacks
93
+ cleanupCallbacks.forEach(cb => {
94
+ try {
95
+ cb()
96
+ } catch (e) {
97
+ // Ignore cleanup errors
98
+ }
99
+ })
100
+ }
101
+ },
102
+ onCancel(callback) {
103
+ if (token.cancelled) {
104
+ callback() // Already cancelled, run immediately
105
+ } else {
106
+ cleanupCallbacks.push(callback)
107
+ }
108
+ }
109
+ }
110
+
111
+ return token
112
+ }
113
+
59
114
  export interface ScanFile {
60
115
  path: string
61
116
  content: string
@@ -107,6 +162,10 @@ export interface ScanResult {
107
162
  cacheReadTokens: number
108
163
  cacheHitRate: number
109
164
  }
165
+
166
+ // Cancellation metadata
167
+ cancelled?: boolean // true if scan was cancelled by user
168
+ cancelReason?: string // Reason for cancellation (e.g., "User pressed Ctrl+C")
110
169
  }
111
170
 
112
171
  export interface ScanProgress {
@@ -201,6 +201,46 @@ export function isClientBundledFile(filePath: string): boolean {
201
201
  return clientPatterns.some(pattern => pattern.test(filePath))
202
202
  }
203
203
 
204
+ /**
205
+ * Check if file is a seed or data generation file
206
+ * These files generate test/demo data and Math.random() usage is acceptable
207
+ * Used to reduce false positives for Math.random() detection
208
+ */
209
+ export function isSeedOrDataGenFile(filePath: string): boolean {
210
+ const patterns = [
211
+ /\/seed\//i,
212
+ /\/seeds\//i,
213
+ /seed-database\.(ts|js)$/i,
214
+ /\/seeder\./i,
215
+ /datacreator\.(ts|js)$/i,
216
+ /\/data\/.*creator/i,
217
+ /\/fixtures\//i,
218
+ /\.fixture\./i,
219
+ /\/generators?\//i,
220
+ /\/factories\//i,
221
+ /factory\.(ts|js)$/i,
222
+ ]
223
+ return patterns.some(p => p.test(filePath))
224
+ }
225
+
226
+ /**
227
+ * Check if file is educational/intentional vulnerability code
228
+ * These files (e.g., OWASP Juice Shop) contain intentional vulnerabilities for training
229
+ * Should be skipped entirely to avoid false positives
230
+ */
231
+ export function isEducationalVulnerabilityFile(filePath: string): boolean {
232
+ const patterns = [
233
+ /\/insecurity\.(ts|js)$/i,
234
+ /\/vulnerable\.(ts|js)$/i,
235
+ /\/intentionally-vulnerable/i,
236
+ /\/security-examples?\//i,
237
+ /\/vuln-examples?\//i,
238
+ /\/challenge-\d+/i, // OWASP Juice Shop challenges
239
+ /\/exploit-examples?\//i,
240
+ ]
241
+ return patterns.some(p => p.test(filePath))
242
+ }
243
+
204
244
  // ============================================================================
205
245
  // Code Line Context Detection
206
246
  // ============================================================================