@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.
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -5
- package/dist/index.js.map +1 -1
- package/dist/layer1/entropy.d.ts.map +1 -1
- package/dist/layer1/entropy.js +6 -4
- package/dist/layer1/entropy.js.map +1 -1
- package/dist/layer1/index.d.ts +3 -2
- package/dist/layer1/index.d.ts.map +1 -1
- package/dist/layer1/index.js +22 -2
- package/dist/layer1/index.js.map +1 -1
- package/dist/layer2/dangerous-functions.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions.js +319 -11
- package/dist/layer2/dangerous-functions.js.map +1 -1
- package/dist/layer2/index.d.ts +3 -2
- package/dist/layer2/index.d.ts.map +1 -1
- package/dist/layer2/index.js +22 -2
- package/dist/layer2/index.js.map +1 -1
- package/dist/layer3/anthropic.d.ts +5 -1
- package/dist/layer3/anthropic.d.ts.map +1 -1
- package/dist/layer3/anthropic.js +50 -1
- package/dist/layer3/anthropic.js.map +1 -1
- package/dist/layer3/index.d.ts +3 -1
- package/dist/layer3/index.d.ts.map +1 -1
- package/dist/layer3/index.js +21 -0
- package/dist/layer3/index.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/context-helpers.d.ts +12 -0
- package/dist/utils/context-helpers.d.ts.map +1 -1
- package/dist/utils/context-helpers.js +40 -0
- package/dist/utils/context-helpers.js.map +1 -1
- package/package.json +4 -2
- package/src/index.ts +75 -5
- package/src/layer1/entropy.ts +6 -4
- package/src/layer1/index.ts +33 -5
- package/src/layer2/__tests__/math-random-enhanced.test.ts +405 -0
- package/src/layer2/dangerous-functions.ts +368 -11
- package/src/layer2/index.ts +31 -5
- package/src/layer3/anthropic.ts +55 -1
- package/src/layer3/index.ts +27 -2
- package/src/types.ts +59 -0
- 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
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
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() -
|
|
1436
|
+
// Special handling for Math.random() - enhanced context-aware severity classification
|
|
1175
1437
|
if (funcPattern.name === 'Math.random for security') {
|
|
1176
|
-
//
|
|
1177
|
-
if (
|
|
1178
|
-
// Skip
|
|
1179
|
-
|
|
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
|
package/src/layer2/index.ts
CHANGED
|
@@ -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
|
package/src/layer3/anthropic.ts
CHANGED
|
@@ -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
|
package/src/layer3/index.ts
CHANGED
|
@@ -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
|