@oculum/scanner 1.0.5 → 1.0.7
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.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/layer2/dangerous-functions.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions.js +12 -5
- package/dist/layer2/dangerous-functions.js.map +1 -1
- package/dist/layer3/anthropic.d.ts.map +1 -1
- package/dist/layer3/anthropic.js +120 -63
- package/dist/layer3/anthropic.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +7 -1
- package/src/layer2/dangerous-functions.ts +13 -5
- package/src/layer3/anthropic.ts +129 -62
package/src/layer3/anthropic.ts
CHANGED
|
@@ -24,6 +24,49 @@ import { buildProjectContext, getFileValidationContext, type ProjectContext } fr
|
|
|
24
24
|
// Import tier system for tier-aware auto-dismiss
|
|
25
25
|
import { getTierForCategory, type DetectorTier } from '../tiers'
|
|
26
26
|
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Path Normalization Helpers (for AI response path matching)
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a file path for comparison purposes.
|
|
33
|
+
* Handles common variations: ./src/file.ts, src/file.ts, /src/file.ts
|
|
34
|
+
*/
|
|
35
|
+
function normalizePathForComparison(path: string): string {
|
|
36
|
+
return path
|
|
37
|
+
.replace(/^\.\//, '') // Remove leading ./
|
|
38
|
+
.replace(/^\//, '') // Remove leading /
|
|
39
|
+
.replace(/\\/g, '/') // Normalize Windows backslashes
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Find a matching file path from expected paths, handling path format variations.
|
|
44
|
+
* AI responses may use different path formats than what we sent.
|
|
45
|
+
*/
|
|
46
|
+
function findMatchingFilePath(responsePath: string, expectedPaths: string[]): string | null {
|
|
47
|
+
// Exact match first
|
|
48
|
+
if (expectedPaths.includes(responsePath)) return responsePath
|
|
49
|
+
|
|
50
|
+
// Normalized match
|
|
51
|
+
const normalized = normalizePathForComparison(responsePath)
|
|
52
|
+
for (const expected of expectedPaths) {
|
|
53
|
+
if (normalizePathForComparison(expected) === normalized) {
|
|
54
|
+
console.log(`[AI Validation] Path fuzzy matched: "${responsePath}" -> "${expected}"`)
|
|
55
|
+
return expected
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Basename match (only if unique) - handles cases like "file.ts" matching "src/api/file.ts"
|
|
60
|
+
const basename = responsePath.split('/').pop() || responsePath
|
|
61
|
+
const matches = expectedPaths.filter(p => (p.split('/').pop() || p) === basename)
|
|
62
|
+
if (matches.length === 1) {
|
|
63
|
+
console.log(`[AI Validation] Path basename matched: "${responsePath}" -> "${matches[0]}"`)
|
|
64
|
+
return matches[0]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
27
70
|
// ============================================================================
|
|
28
71
|
// Cost Monitoring Types
|
|
29
72
|
// ============================================================================
|
|
@@ -949,15 +992,14 @@ For each candidate finding, return:
|
|
|
949
992
|
{
|
|
950
993
|
"index": <number>,
|
|
951
994
|
"keep": true | false,
|
|
952
|
-
"
|
|
953
|
-
"
|
|
995
|
+
"notes": "<concise context>" | null,
|
|
996
|
+
"adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null
|
|
954
997
|
}
|
|
955
998
|
\`\`\`
|
|
956
999
|
|
|
957
1000
|
**CRITICAL**: To minimize costs:
|
|
958
|
-
- For \`keep: false\` (rejected):
|
|
959
|
-
- For \`keep: true\` (accepted): Include \`notes\` field with brief context (10-30 words).
|
|
960
|
-
- Omit \`adjustedSeverity\` if keeping original severity (null is wasteful).
|
|
1001
|
+
- For \`keep: false\` (rejected): Set \`notes: null\` and \`adjustedSeverity: null\`. NO explanation needed.
|
|
1002
|
+
- For \`keep: true\` (accepted): Include \`notes\` field with brief context (10-30 words). Set \`adjustedSeverity: null\` if keeping original severity.
|
|
961
1003
|
|
|
962
1004
|
## Severity Guidelines
|
|
963
1005
|
- **critical/high**: Realistically exploitable, should block deploys - ONLY for clear vulnerabilities
|
|
@@ -1235,9 +1277,18 @@ async function validateWithOpenAI(
|
|
|
1235
1277
|
type: 'object',
|
|
1236
1278
|
properties: {
|
|
1237
1279
|
index: { type: 'number' },
|
|
1238
|
-
keep: { type: 'boolean' }
|
|
1280
|
+
keep: { type: 'boolean' },
|
|
1281
|
+
notes: {
|
|
1282
|
+
type: ['string', 'null'],
|
|
1283
|
+
default: null
|
|
1284
|
+
},
|
|
1285
|
+
adjustedSeverity: {
|
|
1286
|
+
type: ['string', 'null'],
|
|
1287
|
+
enum: ['critical', 'high', 'medium', 'low', 'info', null],
|
|
1288
|
+
default: null
|
|
1289
|
+
}
|
|
1239
1290
|
},
|
|
1240
|
-
required: ['index', 'keep'],
|
|
1291
|
+
required: ['index', 'keep', 'notes', 'adjustedSeverity'],
|
|
1241
1292
|
additionalProperties: false
|
|
1242
1293
|
}
|
|
1243
1294
|
}
|
|
@@ -1298,9 +1349,15 @@ async function validateWithOpenAI(
|
|
|
1298
1349
|
let parsedContent: any
|
|
1299
1350
|
try {
|
|
1300
1351
|
parsedContent = JSON.parse(content)
|
|
1352
|
+
console.log(`[OpenAI Debug] Raw parsed content keys:`, Object.keys(parsedContent))
|
|
1301
1353
|
// Unwrap the validations array if present (from structured output)
|
|
1302
1354
|
if (parsedContent.validations && Array.isArray(parsedContent.validations)) {
|
|
1355
|
+
console.log(`[OpenAI Debug] Unwrapping 'validations' array with ${parsedContent.validations.length} items`)
|
|
1303
1356
|
parsedContent = parsedContent.validations
|
|
1357
|
+
} else if (Array.isArray(parsedContent)) {
|
|
1358
|
+
console.log(`[OpenAI Debug] Content is already an array with ${parsedContent.length} items`)
|
|
1359
|
+
} else {
|
|
1360
|
+
console.log(`[OpenAI Debug] Content structure:`, typeof parsedContent, Array.isArray(parsedContent))
|
|
1304
1361
|
}
|
|
1305
1362
|
} catch (e) {
|
|
1306
1363
|
console.warn('[OpenAI] Failed to parse JSON response:', e)
|
|
@@ -1314,39 +1371,53 @@ async function validateWithOpenAI(
|
|
|
1314
1371
|
expectedFiles
|
|
1315
1372
|
)
|
|
1316
1373
|
|
|
1374
|
+
console.log(`[OpenAI] Batch ${batchNum} parsed ${validationResultsMap.size} file results from ${fileDataList.length} files`)
|
|
1375
|
+
if (validationResultsMap.size === 0) {
|
|
1376
|
+
console.warn(`[OpenAI] WARNING: No file results parsed! Content type: ${typeof parsedContent}, isArray: ${Array.isArray(parsedContent)}`)
|
|
1377
|
+
if (Array.isArray(parsedContent) && parsedContent.length > 0) {
|
|
1378
|
+
console.log(`[OpenAI] First item structure:`, Object.keys(parsedContent[0]))
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Log any missing files from the response (these will be REJECTED)
|
|
1383
|
+
if (validationResultsMap.size !== fileDataList.length) {
|
|
1384
|
+
const missing = fileDataList
|
|
1385
|
+
.filter(({ filePath }) => !validationResultsMap.has(filePath))
|
|
1386
|
+
.map(({ filePath }) => filePath)
|
|
1387
|
+
if (missing.length > 0) {
|
|
1388
|
+
console.warn(`[OpenAI] Missing ${missing.length} files from response (will be REJECTED): ${missing.join(', ')}`)
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1317
1392
|
// Apply results per file
|
|
1318
1393
|
for (const { filePath, findings: fileFindings } of fileDataList) {
|
|
1319
1394
|
const fileResults = validationResultsMap.get(filePath)
|
|
1395
|
+
console.log(`[OpenAI] File ${filePath}: ${fileResults?.length || 0} validation results for ${fileFindings.length} findings`)
|
|
1320
1396
|
|
|
1321
1397
|
if (!fileResults || fileResults.length === 0) {
|
|
1322
1398
|
const singleFileResults = parseValidationResponse(content)
|
|
1323
1399
|
if (singleFileResults.length > 0 && fileDataList.length === 1) {
|
|
1324
|
-
const processedFindings = applyValidationResults(fileFindings, singleFileResults)
|
|
1400
|
+
const { processed: processedFindings, dismissedCount } = applyValidationResults(fileFindings, singleFileResults)
|
|
1401
|
+
statsLock.validatedFindings += processedFindings.length + dismissedCount
|
|
1402
|
+
statsLock.dismissedFindings += dismissedCount
|
|
1325
1403
|
for (const processed of processedFindings) {
|
|
1326
|
-
statsLock.validatedFindings++
|
|
1327
1404
|
if (processed.validationStatus === 'confirmed') statsLock.confirmedFindings++
|
|
1328
|
-
else if (processed.validationStatus === 'dismissed') statsLock.dismissedFindings++
|
|
1329
1405
|
else if (processed.validationStatus === 'downgraded') statsLock.downgradedFindings++
|
|
1330
1406
|
batchFindings.push(processed)
|
|
1331
1407
|
}
|
|
1332
1408
|
} else {
|
|
1333
|
-
for
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
validatedByAI: true,
|
|
1339
|
-
validationStatus: 'confirmed' as ValidationStatus,
|
|
1340
|
-
validationNotes: 'Kept by default - no explicit validation result',
|
|
1341
|
-
})
|
|
1342
|
-
}
|
|
1409
|
+
// No validation results - REJECT all findings for this file (conservative approach)
|
|
1410
|
+
console.warn(`[OpenAI] No validation results for ${filePath} - REJECTING ${fileFindings.length} findings`)
|
|
1411
|
+
statsLock.validatedFindings += fileFindings.length
|
|
1412
|
+
statsLock.dismissedFindings += fileFindings.length
|
|
1413
|
+
// Don't add to batchFindings - findings are rejected
|
|
1343
1414
|
}
|
|
1344
1415
|
} else {
|
|
1345
|
-
const processedFindings = applyValidationResults(fileFindings, fileResults)
|
|
1416
|
+
const { processed: processedFindings, dismissedCount } = applyValidationResults(fileFindings, fileResults)
|
|
1417
|
+
statsLock.validatedFindings += processedFindings.length + dismissedCount
|
|
1418
|
+
statsLock.dismissedFindings += dismissedCount
|
|
1346
1419
|
for (const processed of processedFindings) {
|
|
1347
|
-
statsLock.validatedFindings++
|
|
1348
1420
|
if (processed.validationStatus === 'confirmed') statsLock.confirmedFindings++
|
|
1349
|
-
else if (processed.validationStatus === 'dismissed') statsLock.dismissedFindings++
|
|
1350
1421
|
else if (processed.validationStatus === 'downgraded') statsLock.downgradedFindings++
|
|
1351
1422
|
batchFindings.push(processed)
|
|
1352
1423
|
}
|
|
@@ -1647,42 +1718,32 @@ export async function validateFindingsWithAI(
|
|
|
1647
1718
|
|
|
1648
1719
|
if (singleFileResults.length > 0 && fileDataList.length === 1) {
|
|
1649
1720
|
// Single file in batch, use single-file parsing
|
|
1650
|
-
const processedFindings = applyValidationResults(findings, singleFileResults)
|
|
1721
|
+
const { processed: processedFindings, dismissedCount } = applyValidationResults(findings, singleFileResults)
|
|
1722
|
+
stats.validatedFindings += processedFindings.length + dismissedCount
|
|
1723
|
+
stats.dismissedFindings += dismissedCount
|
|
1651
1724
|
for (const processed of processedFindings) {
|
|
1652
|
-
stats.validatedFindings++
|
|
1653
1725
|
if (processed.validationStatus === 'confirmed') {
|
|
1654
1726
|
stats.confirmedFindings++
|
|
1655
|
-
} else if (processed.validationStatus === 'dismissed') {
|
|
1656
|
-
stats.dismissedFindings++
|
|
1657
1727
|
} else if (processed.validationStatus === 'downgraded') {
|
|
1658
1728
|
stats.downgradedFindings++
|
|
1659
1729
|
}
|
|
1660
1730
|
validatedFindings.push(processed)
|
|
1661
1731
|
}
|
|
1662
1732
|
} else {
|
|
1663
|
-
//
|
|
1664
|
-
console.warn(`[AI Validation] No results for ${filePath}
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
validatedFindings.push({
|
|
1669
|
-
...f,
|
|
1670
|
-
validatedByAI: true,
|
|
1671
|
-
validationStatus: 'confirmed' as ValidationStatus,
|
|
1672
|
-
validationNotes: 'Kept by default - no explicit validation result',
|
|
1673
|
-
})
|
|
1674
|
-
}
|
|
1733
|
+
// No validation results - REJECT all findings for this file (conservative approach)
|
|
1734
|
+
console.warn(`[AI Validation] No results for ${filePath} - REJECTING ${findings.length} findings`)
|
|
1735
|
+
stats.validatedFindings += findings.length
|
|
1736
|
+
stats.dismissedFindings += findings.length
|
|
1737
|
+
// Don't add to validatedFindings - findings are rejected
|
|
1675
1738
|
}
|
|
1676
1739
|
} else {
|
|
1677
1740
|
// Apply validation results for this file
|
|
1678
|
-
const processedFindings = applyValidationResults(findings, fileResults)
|
|
1679
|
-
|
|
1741
|
+
const { processed: processedFindings, dismissedCount } = applyValidationResults(findings, fileResults)
|
|
1742
|
+
stats.validatedFindings += processedFindings.length + dismissedCount
|
|
1743
|
+
stats.dismissedFindings += dismissedCount
|
|
1680
1744
|
for (const processed of processedFindings) {
|
|
1681
|
-
stats.validatedFindings++
|
|
1682
1745
|
if (processed.validationStatus === 'confirmed') {
|
|
1683
1746
|
stats.confirmedFindings++
|
|
1684
|
-
} else if (processed.validationStatus === 'dismissed') {
|
|
1685
|
-
stats.dismissedFindings++
|
|
1686
1747
|
} else if (processed.validationStatus === 'downgraded') {
|
|
1687
1748
|
stats.downgradedFindings++
|
|
1688
1749
|
}
|
|
@@ -1985,7 +2046,15 @@ function parseMultiFileValidationResponse(
|
|
|
1985
2046
|
continue
|
|
1986
2047
|
}
|
|
1987
2048
|
|
|
1988
|
-
|
|
2049
|
+
// Use path normalization to match AI response paths to expected paths
|
|
2050
|
+
const responsePath = fileResult.file
|
|
2051
|
+
const matchedPath = findMatchingFilePath(responsePath, expectedFiles)
|
|
2052
|
+
|
|
2053
|
+
if (!matchedPath) {
|
|
2054
|
+
console.warn(`[AI Validation] Multi-file: Could not match path "${responsePath}" to any expected file`)
|
|
2055
|
+
continue
|
|
2056
|
+
}
|
|
2057
|
+
|
|
1989
2058
|
const validations: ValidationResult[] = fileResult.validations
|
|
1990
2059
|
.filter((item: any) =>
|
|
1991
2060
|
typeof item.index === 'number' &&
|
|
@@ -1994,7 +2063,7 @@ function parseMultiFileValidationResponse(
|
|
|
1994
2063
|
.map((item: any) => {
|
|
1995
2064
|
// Normalize notes field: prefer new 'notes', fallback to legacy 'reason' or 'validationNotes'
|
|
1996
2065
|
const notes = item.notes || item.validationNotes || item.reason || undefined
|
|
1997
|
-
|
|
2066
|
+
|
|
1998
2067
|
return {
|
|
1999
2068
|
index: item.index,
|
|
2000
2069
|
keep: item.keep,
|
|
@@ -2006,14 +2075,13 @@ function parseMultiFileValidationResponse(
|
|
|
2006
2075
|
}
|
|
2007
2076
|
})
|
|
2008
2077
|
|
|
2009
|
-
resultMap.set(
|
|
2078
|
+
resultMap.set(matchedPath, validations)
|
|
2010
2079
|
}
|
|
2011
2080
|
|
|
2012
|
-
// Log any files that weren't in the response
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
}
|
|
2081
|
+
// Log any files that weren't in the response (these will be REJECTED by default)
|
|
2082
|
+
const missingFiles = expectedFiles.filter(f => !resultMap.has(f))
|
|
2083
|
+
if (missingFiles.length > 0) {
|
|
2084
|
+
console.warn(`[AI Validation] Multi-file: Missing ${missingFiles.length} files from response: ${missingFiles.join(', ')}`)
|
|
2017
2085
|
}
|
|
2018
2086
|
|
|
2019
2087
|
} catch (error) {
|
|
@@ -2029,22 +2097,20 @@ function parseMultiFileValidationResponse(
|
|
|
2029
2097
|
function applyValidationResults(
|
|
2030
2098
|
findings: Vulnerability[],
|
|
2031
2099
|
validationResults: ValidationResult[]
|
|
2032
|
-
): Vulnerability[] {
|
|
2100
|
+
): { processed: Vulnerability[]; dismissedCount: number } {
|
|
2033
2101
|
const processed: Vulnerability[] = []
|
|
2102
|
+
let dismissedCount = 0
|
|
2034
2103
|
|
|
2035
2104
|
for (let i = 0; i < findings.length; i++) {
|
|
2036
2105
|
const finding = findings[i]
|
|
2037
2106
|
const validation = validationResults.find(v => v.index === i)
|
|
2038
2107
|
|
|
2039
2108
|
if (!validation) {
|
|
2040
|
-
// No validation result -
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
validationNotes: 'No explicit validation result - kept by default',
|
|
2046
|
-
})
|
|
2047
|
-
continue
|
|
2109
|
+
// No validation result - REJECT by default (conservative approach)
|
|
2110
|
+
// If AI doesn't explicitly validate a finding, assume it's a false positive
|
|
2111
|
+
console.warn(`[AI Validation] No result for finding ${i}: ${finding.title} - REJECTING`)
|
|
2112
|
+
dismissedCount++
|
|
2113
|
+
continue // Don't add to processed - finding is removed
|
|
2048
2114
|
}
|
|
2049
2115
|
|
|
2050
2116
|
if (validation.keep) {
|
|
@@ -2074,11 +2140,12 @@ function applyValidationResults(
|
|
|
2074
2140
|
} else {
|
|
2075
2141
|
// Finding was dismissed - no need to log verbose reason (cost optimization)
|
|
2076
2142
|
console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber}`)
|
|
2143
|
+
dismissedCount++
|
|
2077
2144
|
// Don't add to processed - finding is removed
|
|
2078
2145
|
}
|
|
2079
2146
|
}
|
|
2080
2147
|
|
|
2081
|
-
return processed
|
|
2148
|
+
return { processed, dismissedCount }
|
|
2082
2149
|
}
|
|
2083
2150
|
|
|
2084
2151
|
/**
|