@oculum/scanner 1.0.1 → 1.0.3

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 +3 -2
  9. package/dist/layer1/index.d.ts.map +1 -1
  10. package/dist/layer1/index.js +22 -2
  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 +3 -2
  16. package/dist/layer2/index.d.ts.map +1 -1
  17. package/dist/layer2/index.js +22 -2
  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 +50 -1
  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 +33 -5
  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 +31 -5
  42. package/src/layer3/anthropic.ts +55 -1
  43. package/src/layer3/index.ts +27 -2
  44. package/src/types.ts +59 -0
  45. package/src/utils/context-helpers.ts +40 -0
@@ -8,6 +8,8 @@ import {
8
8
  isComment,
9
9
  isTestOrMockFile,
10
10
  isScannerOrFixtureFile,
11
+ isSeedOrDataGenFile,
12
+ isEducationalVulnerabilityFile,
11
13
  } from '../utils/context-helpers'
12
14
 
13
15
  /**
@@ -807,12 +809,9 @@ function isCosmeticMathRandom(lineContent: string, content: string, lineNumber:
807
809
  /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bpx\b/i, // Math.random() * 100 + 50 + 'px'
808
810
  /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bms\b/i, // Math.random() * 1000 + 500 + 'ms'
809
811
  /Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i, // Math.random() * 5 + 2 + 's'
810
- // UI identifier generation (short strings for element IDs, keys, etc.)
811
- /Math\.random\(\)\.toString\(36\)\.substring\(/, // .toString(36).substring(2, 9) - short UI IDs
812
- /Math\.random\(\)\.toString\(36\)\.substr\(/, // .substr() variant
813
- /Math\.random\(\)\.toString\(36\)\.slice\(/, // .slice() variant
814
- /Math\.random\(\)\.toString\(16\)\.substring\(/, // .toString(16).substring() - hex UI IDs
815
- /Math\.random\(\)\.toString\(16\)\.slice\(/, // hex slice variant
812
+ // NOTE: toString patterns removed - now handled by analyzeToStringPattern()
813
+ // which provides more granular severity classification (info/low/medium/high)
814
+ // based on truncation length and context
816
815
  ]
817
816
 
818
817
  if (cosmeticLinePatterns.some(p => p.test(lineContent))) {
@@ -879,6 +878,269 @@ function isCosmeticMathRandom(lineContent: string, content: string, lineNumber:
879
878
  return false // Default to flagging if unclear
880
879
  }
881
880
 
881
+ /**
882
+ * Extract function context where Math.random() is being called
883
+ * Looks backwards from the current line to find enclosing function name
884
+ * Returns lowercase function name or null if not found
885
+ */
886
+ function extractFunctionContext(content: string, lineNumber: number): string | null {
887
+ const lines = content.split('\n')
888
+ const start = Math.max(0, lineNumber - 10)
889
+
890
+ // Look backwards for function declaration
891
+ for (let i = lineNumber; i >= start; i--) {
892
+ const line = lines[i]
893
+
894
+ // Match various function declaration patterns
895
+ // 1. function functionName
896
+ // 2. export function functionName
897
+ // 3. const/let functionName = function
898
+ // 4. const/let functionName = (arrow function)
899
+ // 5. export const functionName =
900
+
901
+ // Traditional function declaration
902
+ const funcDeclMatch = line.match(/(?:export\s+)?function\s+(\w+)/i)
903
+ if (funcDeclMatch) {
904
+ return funcDeclMatch[1].toLowerCase()
905
+ }
906
+
907
+ // Arrow function or function expression assignment
908
+ // Only match if there's an equals sign and function-like syntax
909
+ const arrowFuncMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\(|async)/i)
910
+ if (arrowFuncMatch) {
911
+ return arrowFuncMatch[1].toLowerCase()
912
+ }
913
+ }
914
+ return null
915
+ }
916
+
917
+ /**
918
+ * Classify function intent based on function name
919
+ * Used to determine if Math.random() usage is legitimate
920
+ */
921
+ function classifyFunctionIntent(functionName: string | null): 'uuid' | 'captcha' | 'demo' | 'security' | 'unknown' {
922
+ if (!functionName) return 'unknown'
923
+
924
+ const lower = functionName.toLowerCase()
925
+
926
+ // UUID/ID generation (UI correlation, not security)
927
+ // Check for specific UUID patterns and generic ID generation functions
928
+ const uuidPatterns = ['uuid', 'guid', 'uniqueid', 'correlationid']
929
+ const idGenerationPatterns = /^(generate|create|make|build)(id|identifier)$/i
930
+ if (uuidPatterns.some(p => lower.includes(p)) || idGenerationPatterns.test(lower)) {
931
+ return 'uuid'
932
+ }
933
+
934
+ // CAPTCHA/puzzle generation (legitimate non-security)
935
+ const captchaPatterns = ['captcha', 'puzzle', 'mathproblem']
936
+ // Also check for 'challenge' but only if not in security context
937
+ if (captchaPatterns.some(p => lower.includes(p))) return 'captcha'
938
+ if (lower.includes('challenge') && !lower.includes('auth')) return 'captcha'
939
+
940
+ // Demo/seed/fixture data
941
+ const demoPatterns = ['seed', 'fixture', 'demo', 'mock', 'fake']
942
+ if (demoPatterns.some(p => lower.includes(p))) return 'demo'
943
+
944
+ // Security-sensitive (check this after id generation to avoid false positives)
945
+ const securityPatterns = ['token', 'secret', 'key', 'password', 'credential', 'signature']
946
+ // Also match generate/create + security term combinations
947
+ const securityFunctionPattern = /^(generate|create|make)(token|secret|key|session|password|credential)/i
948
+ if (securityPatterns.some(p => lower.includes(p)) || securityFunctionPattern.test(lower)) {
949
+ return 'security'
950
+ }
951
+
952
+ return 'unknown'
953
+ }
954
+
955
+ /**
956
+ * Analyze toString() pattern in Math.random() usage
957
+ * Determines intent based on base and truncation length
958
+ */
959
+ function analyzeToStringPattern(lineContent: string): {
960
+ hasToString: boolean
961
+ base: number | null
962
+ isTruncated: boolean
963
+ truncationLength: number | null
964
+ intent: 'short-ui-id' | 'business-id' | 'full-token' | 'unknown'
965
+ } {
966
+ const toString36Match = lineContent.match(/Math\.random\(\)\.toString\(36\)/)
967
+ const toString16Match = lineContent.match(/Math\.random\(\)\.toString\(16\)/)
968
+
969
+ if (!toString36Match && !toString16Match) {
970
+ return { hasToString: false, base: null, isTruncated: false, truncationLength: null, intent: 'unknown' }
971
+ }
972
+
973
+ const base = toString36Match ? 36 : 16
974
+
975
+ // Check for truncation methods
976
+ const substringMatch = lineContent.match(/\.substring\((\d+)(?:,\s*(\d+))?\)/)
977
+ const sliceMatch = lineContent.match(/\.slice\((\d+)(?:,\s*(\d+))?\)/)
978
+ const substrMatch = lineContent.match(/\.substr\((\d+)(?:,\s*(\d+))?\)/)
979
+
980
+ const truncMatch = substringMatch || sliceMatch || substrMatch
981
+
982
+ if (!truncMatch) {
983
+ return { hasToString: true, base, isTruncated: false, truncationLength: null, intent: 'full-token' }
984
+ }
985
+
986
+ // Calculate truncation length
987
+ const start = parseInt(truncMatch[1])
988
+ const end = truncMatch[2] ? parseInt(truncMatch[2]) : null
989
+ const length = end ? (end - start) : null
990
+
991
+ // Classify intent by length
992
+ // Short (2-9 chars): UI correlation IDs, React keys
993
+ // Medium (10-15 chars): Business IDs, order numbers
994
+ if (length && length <= 9) {
995
+ return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: 'short-ui-id' }
996
+ } else if (length && length <= 15) {
997
+ return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: 'business-id' }
998
+ } else {
999
+ return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: 'business-id' }
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * Extract variable name from Math.random() assignment
1005
+ * Examples:
1006
+ * const token = Math.random() -> "token"
1007
+ * const businessId = Math.random().toString(36) -> "businessId"
1008
+ * return Math.random() -> null (no variable)
1009
+ */
1010
+ function extractMathRandomVariableName(lineContent: string): string | null {
1011
+ // const/let/var variableName = Math.random...
1012
+ const assignmentMatch = lineContent.match(/(?:const|let|var)\s+(\w+)\s*=.*Math\.random/)
1013
+ if (assignmentMatch) return assignmentMatch[1]
1014
+
1015
+ // object.property = Math.random...
1016
+ const propertyMatch = lineContent.match(/(\w+)\s*[:=]\s*Math\.random/)
1017
+ if (propertyMatch) return propertyMatch[1]
1018
+
1019
+ // function parameter default: functionName(param = Math.random())
1020
+ const paramMatch = lineContent.match(/(\w+)\s*=\s*Math\.random/)
1021
+ if (paramMatch) return paramMatch[1]
1022
+
1023
+ return null // No variable name found
1024
+ }
1025
+
1026
+ /**
1027
+ * Classify variable name security risk based on naming patterns
1028
+ *
1029
+ * High risk: Security-sensitive names (token, secret, key, etc.)
1030
+ * Medium risk: Unclear context
1031
+ * Low risk: Non-security names (id, businessId, orderId, etc.)
1032
+ */
1033
+ function classifyVariableNameRisk(varName: string | null): 'high' | 'medium' | 'low' {
1034
+ if (!varName) return 'medium' // Unknown usage, moderate risk
1035
+
1036
+ const lower = varName.toLowerCase()
1037
+
1038
+ // High risk: security-sensitive variable names
1039
+ const highRiskPatterns = [
1040
+ 'token', 'secret', 'key', 'password', 'credential',
1041
+ 'signature', 'salt', 'nonce', 'session', 'csrf',
1042
+ 'auth', 'apikey', 'accesstoken', 'refreshtoken',
1043
+ 'jwt', 'bearer', 'oauth', 'sessionid'
1044
+ ]
1045
+ if (highRiskPatterns.some(p => lower.includes(p))) {
1046
+ return 'high'
1047
+ }
1048
+
1049
+ // Low risk: clearly non-security contexts
1050
+ const lowRiskPatterns = [
1051
+ // Business identifiers
1052
+ 'id', 'uid', 'guid', 'business', 'order', 'invoice',
1053
+ 'customer', 'user', 'product', 'item', 'transaction',
1054
+ 'request', 'reference', 'tracking', 'confirmation',
1055
+ // Test/demo data
1056
+ 'test', 'mock', 'demo', 'sample', 'example', 'fixture',
1057
+ 'random', 'temp', 'temporary', 'generated', 'dummy',
1058
+ // UI identifiers
1059
+ 'toast', 'notification', 'element', 'component', 'widget',
1060
+ 'modal', 'dialog', 'popup', 'unique', 'react'
1061
+ ]
1062
+ if (lowRiskPatterns.some(p => lower.includes(p))) {
1063
+ return 'low'
1064
+ }
1065
+
1066
+ return 'medium' // Unclear context, moderate risk
1067
+ }
1068
+
1069
+ /**
1070
+ * Analyze surrounding code context for security signals
1071
+ * Returns context type and description for severity classification
1072
+ */
1073
+ function analyzeMathRandomContext(
1074
+ content: string,
1075
+ filePath: string,
1076
+ lineNumber: number
1077
+ ): {
1078
+ inSecurityContext: boolean
1079
+ inTestContext: boolean
1080
+ inUIContext: boolean
1081
+ inBusinessLogicContext: boolean
1082
+ contextDescription: string
1083
+ } {
1084
+ const lines = content.split('\n')
1085
+ const start = Math.max(0, lineNumber - 10)
1086
+ const end = Math.min(lines.length, lineNumber + 5)
1087
+ const context = lines.slice(start, end).join('\n')
1088
+
1089
+ // Security context indicators (functions, imports, comments)
1090
+ const securityPatterns = [
1091
+ /\b(generate|create)(Token|Secret|Key|Password|Nonce|Salt|Session|Signature)/i,
1092
+ /\b(auth|crypto|encrypt|decrypt|hash|sign)\b/i,
1093
+ /function\s+.*(?:token|secret|key|auth|crypto)/i,
1094
+ /\bimport.*(?:crypto|jsonwebtoken|bcrypt|argon2|jose)/i,
1095
+ /\/\*.*(?:security|authentication|cryptograph|authorization)/i,
1096
+ /\/\/.*(?:security|auth|crypto|token|secret)/i,
1097
+ ]
1098
+ const inSecurityContext = securityPatterns.some(p => p.test(context))
1099
+
1100
+ // Test context
1101
+ const testFilePatterns = /\.(test|spec)\.(ts|tsx|js|jsx)$/i
1102
+ const testContextPatterns = [
1103
+ /\b(describe|it|test|expect|mock|jest|vitest|mocha|chai)\b/i,
1104
+ /\b(beforeEach|afterEach|beforeAll|afterAll)\b/i,
1105
+ /\b(fixture|stub|spy)\b/i,
1106
+ ]
1107
+ const inTestContext = testFilePatterns.test(filePath) ||
1108
+ testContextPatterns.some(p => p.test(context))
1109
+
1110
+ // UI/cosmetic context (reuse existing logic)
1111
+ const lineContent = lines[lineNumber]
1112
+ const inUIContext = isCosmeticMathRandom(lineContent, content, lineNumber)
1113
+
1114
+ // Business logic context (non-security ID generation)
1115
+ // Note: UUID/CAPTCHA patterns excluded - handled by functionIntent classification
1116
+ const businessLogicPatterns = [
1117
+ /\b(business|order|invoice|customer|product|transaction)Id\b/i,
1118
+ /\b(reference|tracking|confirmation)Number\b/i,
1119
+ ]
1120
+ const inBusinessLogicContext = businessLogicPatterns.some(p => p.test(context)) &&
1121
+ !inSecurityContext
1122
+
1123
+ // Determine context description
1124
+ let contextDescription = 'unknown context'
1125
+ if (inSecurityContext) {
1126
+ contextDescription = 'security-sensitive function'
1127
+ } else if (inTestContext) {
1128
+ contextDescription = 'test/mock data generation'
1129
+ } else if (inUIContext) {
1130
+ contextDescription = 'UI/cosmetic usage'
1131
+ } else if (inBusinessLogicContext) {
1132
+ contextDescription = 'business identifier generation'
1133
+ }
1134
+
1135
+ return {
1136
+ inSecurityContext,
1137
+ inTestContext,
1138
+ inUIContext,
1139
+ inBusinessLogicContext,
1140
+ contextDescription,
1141
+ }
1142
+ }
1143
+
882
1144
  export function detectDangerousFunctions(
883
1145
  content: string,
884
1146
  filePath: string
@@ -1171,13 +1433,108 @@ export function detectDangerousFunctions(
1171
1433
  }
1172
1434
  }
1173
1435
 
1174
- // Special handling for Math.random() - skip cosmetic/UI uses
1436
+ // Special handling for Math.random() - enhanced context-aware severity classification
1175
1437
  if (funcPattern.name === 'Math.random for security') {
1176
- // Check if this is cosmetic use (CSS, animations, UI variations)
1177
- if (isCosmeticMathRandom(line, content, index)) {
1178
- // Skip entirely - this is not a security concern
1179
- break
1438
+ // Phase 1: File-level exclusions (skip entirely)
1439
+ if (isSeedOrDataGenFile(filePath)) {
1440
+ break // Skip seed/data generation files entirely
1441
+ }
1442
+
1443
+ if (isEducationalVulnerabilityFile(filePath)) {
1444
+ break // Skip intentional vulnerability examples
1445
+ }
1446
+
1447
+ // Phase 2: Context analysis
1448
+ const varName = extractMathRandomVariableName(line)
1449
+ const nameRisk = classifyVariableNameRisk(varName)
1450
+ const context = analyzeMathRandomContext(content, filePath, index)
1451
+ const functionName = extractFunctionContext(content, index)
1452
+ const functionIntent = classifyFunctionIntent(functionName)
1453
+ const toStringPattern = analyzeToStringPattern(line)
1454
+
1455
+ // Phase 3: Skip cosmetic/UI uses
1456
+ if (context.inUIContext) {
1457
+ break // Already working
1458
+ }
1459
+
1460
+ // Phase 4: Skip UUID/CAPTCHA generation functions
1461
+ if (functionIntent === 'uuid' || functionIntent === 'captcha') {
1462
+ break // Legitimate non-security uses
1463
+ }
1464
+
1465
+ // Phase 5: Determine severity
1466
+ let severity: VulnerabilitySeverity = 'medium'
1467
+ let confidence: 'high' | 'medium' | 'low' = 'medium'
1468
+ let explanation = ''
1469
+ let description = funcPattern.description
1470
+ let suggestedFix = funcPattern.suggestedFix
1471
+
1472
+ // Test context - INFO
1473
+ if (context.inTestContext) {
1474
+ severity = 'info'
1475
+ confidence = 'low'
1476
+ explanation = ' (test data generation)'
1477
+ description = 'Math.random() used in test context for generating mock data. Not security-critical, but consider crypto.randomUUID() for better uniqueness in tests.'
1478
+ suggestedFix = 'Consider crypto.randomUUID() for test data uniqueness, though Math.random() is acceptable in tests'
1479
+ }
1480
+ // Seed/demo function context - INFO
1481
+ else if (functionIntent === 'demo') {
1482
+ severity = 'info'
1483
+ confidence = 'low'
1484
+ explanation = ' (seed/demo data generation)'
1485
+ description = 'Math.random() used for generating fixture/seed data. Not security-critical in development contexts.'
1486
+ suggestedFix = 'Acceptable for seed data. Use crypto.randomUUID() if uniqueness guarantees needed.'
1487
+ }
1488
+ // Security context - HIGH
1489
+ else if (nameRisk === 'high' || context.inSecurityContext || functionIntent === 'security') {
1490
+ severity = 'high'
1491
+ confidence = 'high'
1492
+ explanation = ' (security-sensitive context)'
1493
+ description = 'Math.random() is NOT cryptographically secure and MUST NOT be used for tokens, keys, passwords, or session IDs. This can lead to predictable values that attackers can exploit.'
1494
+ suggestedFix = 'Replace with crypto.randomBytes() or crypto.randomUUID() for security-sensitive operations'
1495
+ }
1496
+ // Short UI ID pattern - INFO
1497
+ else if (toStringPattern.intent === 'short-ui-id') {
1498
+ severity = 'info'
1499
+ confidence = 'low'
1500
+ explanation = ' (UI correlation ID)'
1501
+ description = 'Math.random() used for short UI correlation IDs. Not security-critical, but collisions possible in high-volume scenarios.'
1502
+ suggestedFix = 'For UI correlation, crypto.randomUUID() provides better uniqueness guarantees'
1503
+ }
1504
+ // Business ID pattern - LOW
1505
+ else if (nameRisk === 'low' || context.inBusinessLogicContext || toStringPattern.intent === 'business-id') {
1506
+ severity = 'low'
1507
+ confidence = 'low'
1508
+ explanation = ' (business identifier)'
1509
+ description = 'Math.random() is being used for non-security purposes (business IDs, tracking numbers). While not critical, Math.random() can produce collisions in high-volume scenarios.'
1510
+ suggestedFix = 'Consider crypto.randomUUID() for better uniqueness guarantees and collision resistance'
1511
+ }
1512
+ // Unknown context - MEDIUM
1513
+ else {
1514
+ severity = 'medium'
1515
+ confidence = 'medium'
1516
+ explanation = ' (unclear context)'
1517
+ description = 'Math.random() is being used. Verify this is not for security-critical purposes like tokens, session IDs, or cryptographic operations.'
1518
+ suggestedFix = 'If used for security, replace with crypto.randomBytes(). For unique IDs, use crypto.randomUUID()'
1180
1519
  }
1520
+
1521
+ // Update title with context
1522
+ const title = `Math.random() in ${context.contextDescription}${explanation}`
1523
+
1524
+ vulnerabilities.push({
1525
+ id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
1526
+ filePath,
1527
+ lineNumber: index + 1,
1528
+ lineContent: line.trim(),
1529
+ severity,
1530
+ category: 'dangerous_function',
1531
+ title,
1532
+ description,
1533
+ suggestedFix,
1534
+ confidence,
1535
+ layer: 2,
1536
+ })
1537
+ break // Only report once per line
1181
1538
  }
1182
1539
 
1183
1540
  // Standard handling for all other patterns
@@ -5,7 +5,8 @@
5
5
  * and AI code fingerprinting
6
6
  */
7
7
 
8
- import type { Vulnerability, ScanFile } from '../types'
8
+ import type { Vulnerability, ScanFile, CancellationToken } from '../types'
9
+ import type { ProgressCallback } from '../index'
9
10
  import type { MiddlewareAuthConfig } from '../utils/middleware-detector'
10
11
  import { detectAuthHelpers, type AuthHelperContext } from '../utils/auth-helper-detector'
11
12
  import type { FileAuthImports } from '../utils/imported-auth-detector'
@@ -182,12 +183,16 @@ function processFileLayer2(
182
183
  }
183
184
  }
184
185
 
185
- // Parallel batch size for Layer 2 processing
186
+ // Parallel batch size for Layer 2 processing (larger batches for performance)
186
187
  const LAYER2_PARALLEL_BATCH_SIZE = 50
188
+ // Progress update interval (report every N files for better UX)
189
+ const PROGRESS_UPDATE_INTERVAL = 10
187
190
 
188
191
  export async function runLayer2Scan(
189
192
  files: ScanFile[],
190
- options: Layer2Options = {}
193
+ options: Layer2Options = {},
194
+ onProgress?: ProgressCallback,
195
+ cancellationToken?: CancellationToken
191
196
  ): Promise<Layer2Result> {
192
197
  const startTime = Date.now()
193
198
  const vulnerabilities: Vulnerability[] = []
@@ -208,17 +213,24 @@ export async function runLayer2Scan(
208
213
  endpointProtection: 0,
209
214
  schemaValidation: 0,
210
215
  }
211
-
216
+
212
217
  // Detect auth helpers once for all files (if not already provided)
213
218
  const authHelperContext = options.authHelperContext || detectAuthHelpers(files)
214
219
 
220
+ // Track progress for frequent updates
221
+ let filesProcessed = 0
222
+ let lastProgressUpdate = 0
223
+
215
224
  // Process files in parallel batches for better performance on large codebases
216
225
  for (let i = 0; i < files.length; i += LAYER2_PARALLEL_BATCH_SIZE) {
226
+ // Check for cancellation before processing batch
227
+ if (cancellationToken?.cancelled) break
228
+
217
229
  const batch = files.slice(i, i + LAYER2_PARALLEL_BATCH_SIZE)
218
230
  const results = await Promise.all(
219
231
  batch.map(file => Promise.resolve(processFileLayer2(file, options, authHelperContext)))
220
232
  )
221
-
233
+
222
234
  for (const result of results) {
223
235
  vulnerabilities.push(...result.findings)
224
236
  // Accumulate stats
@@ -226,6 +238,20 @@ export async function runLayer2Scan(
226
238
  stats[key as keyof Layer2DetectorStats] += value
227
239
  }
228
240
  }
241
+
242
+ filesProcessed = Math.min(i + LAYER2_PARALLEL_BATCH_SIZE, files.length)
243
+
244
+ // Report progress every PROGRESS_UPDATE_INTERVAL files for better UX
245
+ if (onProgress && (filesProcessed - lastProgressUpdate >= PROGRESS_UPDATE_INTERVAL || filesProcessed === files.length)) {
246
+ onProgress({
247
+ status: 'layer2',
248
+ message: 'Running structural scan (variables, logic gates)...',
249
+ filesProcessed,
250
+ totalFiles: files.length,
251
+ vulnerabilitiesFound: vulnerabilities.length,
252
+ })
253
+ lastProgressUpdate = filesProcessed
254
+ }
229
255
  }
230
256
 
231
257
  // Deduplicate findings
@@ -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
@@ -1320,7 +1349,8 @@ async function validateWithOpenAI(
1320
1349
  export async function validateFindingsWithAI(
1321
1350
  findings: Vulnerability[],
1322
1351
  files: ScanFile[],
1323
- projectContext?: ProjectContext
1352
+ projectContext?: ProjectContext,
1353
+ onProgress?: (progress: { filesProcessed: number; totalFiles: number; status: string }) => void
1324
1354
  ): Promise<AIValidationResult> {
1325
1355
  // Initialize stats tracking
1326
1356
  const stats: ValidationStats = {
@@ -1393,11 +1423,23 @@ export async function validateFindingsWithAI(
1393
1423
 
1394
1424
  console.log(`[AI Validation] Phase 2: Processing ${fileEntries.length} files in ${totalFileBatches} API batch(es) (${FILES_PER_API_BATCH} files/batch)`)
1395
1425
 
1426
+ // Track files processed for progress reporting
1427
+ let filesValidated = 0
1428
+
1396
1429
  // Process files in batches - each batch is ONE API call with multiple files
1397
1430
  for (let batchStart = 0; batchStart < fileEntries.length; batchStart += FILES_PER_API_BATCH) {
1398
1431
  const fileBatch = fileEntries.slice(batchStart, batchStart + FILES_PER_API_BATCH)
1399
1432
  const batchNum = Math.floor(batchStart / FILES_PER_API_BATCH) + 1
1400
1433
 
1434
+ // Report progress before processing batch
1435
+ if (onProgress) {
1436
+ onProgress({
1437
+ filesProcessed: filesValidated,
1438
+ totalFiles: fileEntries.length,
1439
+ status: `AI validating batch ${batchNum}/${totalFileBatches}`,
1440
+ })
1441
+ }
1442
+
1401
1443
  console.log(`[AI Validation] API Batch ${batchNum}/${totalFileBatches}: ${fileBatch.length} files`)
1402
1444
 
1403
1445
  // Prepare file data for batch request
@@ -1578,6 +1620,18 @@ export async function validateFindingsWithAI(
1578
1620
 
1579
1621
  const batchDuration = Date.now() - batchStartTime
1580
1622
  totalBatchWaitTime += batchDuration
1623
+
1624
+ // Update files validated counter
1625
+ filesValidated += fileBatch.length
1626
+
1627
+ // Report progress after batch completion
1628
+ if (onProgress) {
1629
+ onProgress({
1630
+ filesProcessed: filesValidated,
1631
+ totalFiles: fileEntries.length,
1632
+ status: `AI validation complete for batch ${batchNum}/${totalFileBatches}`,
1633
+ })
1634
+ }
1581
1635
  }
1582
1636
 
1583
1637
  // Calculate cache hit rate
@@ -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