@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.
- 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 +2 -2
- package/dist/layer1/index.d.ts.map +1 -1
- package/dist/layer1/index.js +14 -5
- 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 +2 -2
- package/dist/layer2/index.d.ts.map +1 -1
- package/dist/layer2/index.js +14 -5
- 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 +175 -30
- 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 +23 -8
- package/src/layer2/__tests__/math-random-enhanced.test.ts +405 -0
- package/src/layer2/dangerous-functions.ts +368 -11
- package/src/layer2/index.ts +20 -8
- package/src/layer3/anthropic.ts +190 -31
- package/src/layer3/index.ts +27 -2
- package/src/types.ts +59 -0
- package/src/utils/context-helpers.ts +40 -0
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
|
|
@@ -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
|
-
"
|
|
924
|
-
"
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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, "
|
|
1751
|
-
{ "index": 1, "keep": false
|
|
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, "
|
|
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
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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 =
|
|
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 =
|
|
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}
|
|
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
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
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 []
|
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
|
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
|
// ============================================================================
|