@oculum/cli 1.0.4 → 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.js +417 -21
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -11474,6 +11474,34 @@ var require_types = __commonJS({
|
|
|
11474
11474
|
"use strict";
|
|
11475
11475
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
11476
11476
|
exports2.SCAN_MODE_DEFAULTS = exports2.MAX_FILE_SIZE = exports2.SPECIAL_FILES = exports2.SCANNABLE_EXTENSIONS = void 0;
|
|
11477
|
+
exports2.createCancellationToken = createCancellationToken2;
|
|
11478
|
+
function createCancellationToken2() {
|
|
11479
|
+
const cleanupCallbacks = [];
|
|
11480
|
+
const token = {
|
|
11481
|
+
cancelled: false,
|
|
11482
|
+
reason: void 0,
|
|
11483
|
+
cancel(reason) {
|
|
11484
|
+
if (!token.cancelled) {
|
|
11485
|
+
token.cancelled = true;
|
|
11486
|
+
token.reason = reason;
|
|
11487
|
+
cleanupCallbacks.forEach((cb) => {
|
|
11488
|
+
try {
|
|
11489
|
+
cb();
|
|
11490
|
+
} catch (e2) {
|
|
11491
|
+
}
|
|
11492
|
+
});
|
|
11493
|
+
}
|
|
11494
|
+
},
|
|
11495
|
+
onCancel(callback) {
|
|
11496
|
+
if (token.cancelled) {
|
|
11497
|
+
callback();
|
|
11498
|
+
} else {
|
|
11499
|
+
cleanupCallbacks.push(callback);
|
|
11500
|
+
}
|
|
11501
|
+
}
|
|
11502
|
+
};
|
|
11503
|
+
return token;
|
|
11504
|
+
}
|
|
11477
11505
|
exports2.SCANNABLE_EXTENSIONS = [
|
|
11478
11506
|
".js",
|
|
11479
11507
|
".jsx",
|
|
@@ -14444,7 +14472,8 @@ var require_layer1 = __commonJS({
|
|
|
14444
14472
|
};
|
|
14445
14473
|
}
|
|
14446
14474
|
var LAYER1_PARALLEL_BATCH_SIZE = 50;
|
|
14447
|
-
|
|
14475
|
+
var PROGRESS_UPDATE_INTERVAL = 10;
|
|
14476
|
+
async function runLayer1Scan(files, onProgress, cancellationToken) {
|
|
14448
14477
|
const startTime = Date.now();
|
|
14449
14478
|
const vulnerabilities = [];
|
|
14450
14479
|
const rawStats = {
|
|
@@ -14456,7 +14485,11 @@ var require_layer1 = __commonJS({
|
|
|
14456
14485
|
file_flags: 0,
|
|
14457
14486
|
ai_comments: 0
|
|
14458
14487
|
};
|
|
14488
|
+
let filesProcessed = 0;
|
|
14489
|
+
let lastProgressUpdate = 0;
|
|
14459
14490
|
for (let i = 0; i < files.length; i += LAYER1_PARALLEL_BATCH_SIZE) {
|
|
14491
|
+
if (cancellationToken?.cancelled)
|
|
14492
|
+
break;
|
|
14460
14493
|
const batch = files.slice(i, i + LAYER1_PARALLEL_BATCH_SIZE);
|
|
14461
14494
|
const results = await Promise.all(batch.map((file) => Promise.resolve(processFileLayer1(file))));
|
|
14462
14495
|
for (const result of results) {
|
|
@@ -14465,6 +14498,17 @@ var require_layer1 = __commonJS({
|
|
|
14465
14498
|
rawStats[key] += value;
|
|
14466
14499
|
}
|
|
14467
14500
|
}
|
|
14501
|
+
filesProcessed = Math.min(i + LAYER1_PARALLEL_BATCH_SIZE, files.length);
|
|
14502
|
+
if (onProgress && (filesProcessed - lastProgressUpdate >= PROGRESS_UPDATE_INTERVAL || filesProcessed === files.length)) {
|
|
14503
|
+
onProgress({
|
|
14504
|
+
status: "layer1",
|
|
14505
|
+
message: "Running surface scan (patterns, entropy, config)...",
|
|
14506
|
+
filesProcessed,
|
|
14507
|
+
totalFiles: files.length,
|
|
14508
|
+
vulnerabilitiesFound: vulnerabilities.length
|
|
14509
|
+
});
|
|
14510
|
+
lastProgressUpdate = filesProcessed;
|
|
14511
|
+
}
|
|
14468
14512
|
}
|
|
14469
14513
|
const dedupedVulnerabilities = deduplicateFindings(vulnerabilities);
|
|
14470
14514
|
const { kept: uniqueVulnerabilities, suppressed } = (0, path_exclusions_1.filterFindingsByPath)(dedupedVulnerabilities);
|
|
@@ -15867,6 +15911,139 @@ var require_dangerous_functions = __commonJS({
|
|
|
15867
15911
|
}
|
|
15868
15912
|
return false;
|
|
15869
15913
|
}
|
|
15914
|
+
function extractMathRandomVariableName(lineContent) {
|
|
15915
|
+
const assignmentMatch = lineContent.match(/(?:const|let|var)\s+(\w+)\s*=.*Math\.random/);
|
|
15916
|
+
if (assignmentMatch)
|
|
15917
|
+
return assignmentMatch[1];
|
|
15918
|
+
const propertyMatch = lineContent.match(/(\w+)\s*[:=]\s*Math\.random/);
|
|
15919
|
+
if (propertyMatch)
|
|
15920
|
+
return propertyMatch[1];
|
|
15921
|
+
const paramMatch = lineContent.match(/(\w+)\s*=\s*Math\.random/);
|
|
15922
|
+
if (paramMatch)
|
|
15923
|
+
return paramMatch[1];
|
|
15924
|
+
return null;
|
|
15925
|
+
}
|
|
15926
|
+
function classifyVariableNameRisk(varName) {
|
|
15927
|
+
if (!varName)
|
|
15928
|
+
return "medium";
|
|
15929
|
+
const lower = varName.toLowerCase();
|
|
15930
|
+
const highRiskPatterns = [
|
|
15931
|
+
"token",
|
|
15932
|
+
"secret",
|
|
15933
|
+
"key",
|
|
15934
|
+
"password",
|
|
15935
|
+
"credential",
|
|
15936
|
+
"signature",
|
|
15937
|
+
"salt",
|
|
15938
|
+
"nonce",
|
|
15939
|
+
"session",
|
|
15940
|
+
"csrf",
|
|
15941
|
+
"auth",
|
|
15942
|
+
"apikey",
|
|
15943
|
+
"accesstoken",
|
|
15944
|
+
"refreshtoken",
|
|
15945
|
+
"jwt",
|
|
15946
|
+
"bearer",
|
|
15947
|
+
"oauth",
|
|
15948
|
+
"sessionid"
|
|
15949
|
+
];
|
|
15950
|
+
if (highRiskPatterns.some((p2) => lower.includes(p2))) {
|
|
15951
|
+
return "high";
|
|
15952
|
+
}
|
|
15953
|
+
const lowRiskPatterns = [
|
|
15954
|
+
// Business identifiers
|
|
15955
|
+
"id",
|
|
15956
|
+
"uid",
|
|
15957
|
+
"guid",
|
|
15958
|
+
"business",
|
|
15959
|
+
"order",
|
|
15960
|
+
"invoice",
|
|
15961
|
+
"customer",
|
|
15962
|
+
"user",
|
|
15963
|
+
"product",
|
|
15964
|
+
"item",
|
|
15965
|
+
"transaction",
|
|
15966
|
+
"request",
|
|
15967
|
+
"reference",
|
|
15968
|
+
"tracking",
|
|
15969
|
+
"confirmation",
|
|
15970
|
+
// Test/demo data
|
|
15971
|
+
"test",
|
|
15972
|
+
"mock",
|
|
15973
|
+
"demo",
|
|
15974
|
+
"sample",
|
|
15975
|
+
"example",
|
|
15976
|
+
"fixture",
|
|
15977
|
+
"random",
|
|
15978
|
+
"temp",
|
|
15979
|
+
"temporary",
|
|
15980
|
+
"generated",
|
|
15981
|
+
"dummy",
|
|
15982
|
+
// UI identifiers
|
|
15983
|
+
"toast",
|
|
15984
|
+
"notification",
|
|
15985
|
+
"element",
|
|
15986
|
+
"component",
|
|
15987
|
+
"widget",
|
|
15988
|
+
"modal",
|
|
15989
|
+
"dialog",
|
|
15990
|
+
"popup",
|
|
15991
|
+
"unique",
|
|
15992
|
+
"react"
|
|
15993
|
+
];
|
|
15994
|
+
if (lowRiskPatterns.some((p2) => lower.includes(p2))) {
|
|
15995
|
+
return "low";
|
|
15996
|
+
}
|
|
15997
|
+
return "medium";
|
|
15998
|
+
}
|
|
15999
|
+
function analyzeMathRandomContext(content, filePath, lineNumber) {
|
|
16000
|
+
const lines = content.split("\n");
|
|
16001
|
+
const start = Math.max(0, lineNumber - 10);
|
|
16002
|
+
const end = Math.min(lines.length, lineNumber + 5);
|
|
16003
|
+
const context = lines.slice(start, end).join("\n");
|
|
16004
|
+
const securityPatterns = [
|
|
16005
|
+
/\b(generate|create)(Token|Secret|Key|Password|Nonce|Salt|Session|Signature)/i,
|
|
16006
|
+
/\b(auth|crypto|encrypt|decrypt|hash|sign)\b/i,
|
|
16007
|
+
/function\s+.*(?:token|secret|key|auth|crypto)/i,
|
|
16008
|
+
/\bimport.*(?:crypto|jsonwebtoken|bcrypt|argon2|jose)/i,
|
|
16009
|
+
/\/\*.*(?:security|authentication|cryptograph|authorization)/i,
|
|
16010
|
+
/\/\/.*(?:security|auth|crypto|token|secret)/i
|
|
16011
|
+
];
|
|
16012
|
+
const inSecurityContext = securityPatterns.some((p2) => p2.test(context));
|
|
16013
|
+
const testFilePatterns = /\.(test|spec)\.(ts|tsx|js|jsx)$/i;
|
|
16014
|
+
const testContextPatterns = [
|
|
16015
|
+
/\b(describe|it|test|expect|mock|jest|vitest|mocha|chai)\b/i,
|
|
16016
|
+
/\b(beforeEach|afterEach|beforeAll|afterAll)\b/i,
|
|
16017
|
+
/\b(fixture|stub|spy)\b/i
|
|
16018
|
+
];
|
|
16019
|
+
const inTestContext = testFilePatterns.test(filePath) || testContextPatterns.some((p2) => p2.test(context));
|
|
16020
|
+
const lineContent = lines[lineNumber];
|
|
16021
|
+
const inUIContext = isCosmeticMathRandom(lineContent, content, lineNumber);
|
|
16022
|
+
const businessLogicPatterns = [
|
|
16023
|
+
/\b(business|order|invoice|customer|product|transaction)Id\b/i,
|
|
16024
|
+
/\b(reference|tracking|confirmation)Number\b/i,
|
|
16025
|
+
/\bgenerate.*Id\b/i,
|
|
16026
|
+
/\bcreate.*Id\b/i
|
|
16027
|
+
];
|
|
16028
|
+
const inBusinessLogicContext = businessLogicPatterns.some((p2) => p2.test(context)) && !inSecurityContext;
|
|
16029
|
+
let contextDescription = "unknown context";
|
|
16030
|
+
if (inSecurityContext) {
|
|
16031
|
+
contextDescription = "security-sensitive function";
|
|
16032
|
+
} else if (inTestContext) {
|
|
16033
|
+
contextDescription = "test/mock data generation";
|
|
16034
|
+
} else if (inUIContext) {
|
|
16035
|
+
contextDescription = "UI/cosmetic usage";
|
|
16036
|
+
} else if (inBusinessLogicContext) {
|
|
16037
|
+
contextDescription = "business identifier generation";
|
|
16038
|
+
}
|
|
16039
|
+
return {
|
|
16040
|
+
inSecurityContext,
|
|
16041
|
+
inTestContext,
|
|
16042
|
+
inUIContext,
|
|
16043
|
+
inBusinessLogicContext,
|
|
16044
|
+
contextDescription
|
|
16045
|
+
};
|
|
16046
|
+
}
|
|
15870
16047
|
function detectDangerousFunctions(content, filePath) {
|
|
15871
16048
|
const vulnerabilities = [];
|
|
15872
16049
|
if ((0, context_helpers_1.isScannerOrFixtureFile)(filePath)) {
|
|
@@ -16090,9 +16267,57 @@ var require_dangerous_functions = __commonJS({
|
|
|
16090
16267
|
}
|
|
16091
16268
|
}
|
|
16092
16269
|
if (funcPattern.name === "Math.random for security") {
|
|
16093
|
-
|
|
16270
|
+
const varName = extractMathRandomVariableName(line);
|
|
16271
|
+
const nameRisk = classifyVariableNameRisk(varName);
|
|
16272
|
+
const context = analyzeMathRandomContext(content, filePath, index);
|
|
16273
|
+
if (context.inUIContext) {
|
|
16094
16274
|
break;
|
|
16095
16275
|
}
|
|
16276
|
+
let severity2 = "medium";
|
|
16277
|
+
let confidence2 = "medium";
|
|
16278
|
+
let explanation = "";
|
|
16279
|
+
let description = funcPattern.description;
|
|
16280
|
+
let suggestedFix = funcPattern.suggestedFix;
|
|
16281
|
+
if (context.inTestContext) {
|
|
16282
|
+
severity2 = "info";
|
|
16283
|
+
confidence2 = "low";
|
|
16284
|
+
explanation = " (test data generation)";
|
|
16285
|
+
description = "Math.random() used in test context for generating mock data. Not security-critical, but consider crypto.randomUUID() for better uniqueness in tests.";
|
|
16286
|
+
suggestedFix = "Consider crypto.randomUUID() for test data uniqueness, though Math.random() is acceptable in tests";
|
|
16287
|
+
} else if (nameRisk === "high" || context.inSecurityContext) {
|
|
16288
|
+
severity2 = "high";
|
|
16289
|
+
confidence2 = "high";
|
|
16290
|
+
explanation = " (security-sensitive context)";
|
|
16291
|
+
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.";
|
|
16292
|
+
suggestedFix = "Replace with crypto.randomBytes() or crypto.randomUUID() for security-sensitive operations";
|
|
16293
|
+
} else if (nameRisk === "low" || context.inBusinessLogicContext) {
|
|
16294
|
+
severity2 = "low";
|
|
16295
|
+
confidence2 = "medium";
|
|
16296
|
+
explanation = " (business identifier)";
|
|
16297
|
+
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.";
|
|
16298
|
+
suggestedFix = "Consider crypto.randomUUID() for better uniqueness guarantees and collision resistance";
|
|
16299
|
+
} else {
|
|
16300
|
+
severity2 = "medium";
|
|
16301
|
+
confidence2 = "medium";
|
|
16302
|
+
explanation = " (unclear context)";
|
|
16303
|
+
description = "Math.random() is being used. Verify this is not for security-critical purposes like tokens, session IDs, or cryptographic operations.";
|
|
16304
|
+
suggestedFix = "If used for security, replace with crypto.randomBytes(). For unique IDs, use crypto.randomUUID()";
|
|
16305
|
+
}
|
|
16306
|
+
const title = `Math.random() in ${context.contextDescription}${explanation}`;
|
|
16307
|
+
vulnerabilities.push({
|
|
16308
|
+
id: `dangerous-func-${filePath}-${index + 1}-${funcPattern.name}`,
|
|
16309
|
+
filePath,
|
|
16310
|
+
lineNumber: index + 1,
|
|
16311
|
+
lineContent: line.trim(),
|
|
16312
|
+
severity: severity2,
|
|
16313
|
+
category: "dangerous_function",
|
|
16314
|
+
title,
|
|
16315
|
+
description,
|
|
16316
|
+
suggestedFix,
|
|
16317
|
+
confidence: confidence2,
|
|
16318
|
+
layer: 2
|
|
16319
|
+
});
|
|
16320
|
+
break;
|
|
16096
16321
|
}
|
|
16097
16322
|
let severity = funcPattern.severity;
|
|
16098
16323
|
let confidence = "high";
|
|
@@ -20768,7 +20993,8 @@ var require_layer2 = __commonJS({
|
|
|
20768
20993
|
};
|
|
20769
20994
|
}
|
|
20770
20995
|
var LAYER2_PARALLEL_BATCH_SIZE = 50;
|
|
20771
|
-
|
|
20996
|
+
var PROGRESS_UPDATE_INTERVAL = 10;
|
|
20997
|
+
async function runLayer2Scan(files, options = {}, onProgress, cancellationToken) {
|
|
20772
20998
|
const startTime = Date.now();
|
|
20773
20999
|
const vulnerabilities = [];
|
|
20774
21000
|
const stats = {
|
|
@@ -20789,7 +21015,11 @@ var require_layer2 = __commonJS({
|
|
|
20789
21015
|
schemaValidation: 0
|
|
20790
21016
|
};
|
|
20791
21017
|
const authHelperContext = options.authHelperContext || (0, auth_helper_detector_1.detectAuthHelpers)(files);
|
|
21018
|
+
let filesProcessed = 0;
|
|
21019
|
+
let lastProgressUpdate = 0;
|
|
20792
21020
|
for (let i = 0; i < files.length; i += LAYER2_PARALLEL_BATCH_SIZE) {
|
|
21021
|
+
if (cancellationToken?.cancelled)
|
|
21022
|
+
break;
|
|
20793
21023
|
const batch = files.slice(i, i + LAYER2_PARALLEL_BATCH_SIZE);
|
|
20794
21024
|
const results = await Promise.all(batch.map((file) => Promise.resolve(processFileLayer2(file, options, authHelperContext))));
|
|
20795
21025
|
for (const result of results) {
|
|
@@ -20798,6 +21028,17 @@ var require_layer2 = __commonJS({
|
|
|
20798
21028
|
stats[key] += value;
|
|
20799
21029
|
}
|
|
20800
21030
|
}
|
|
21031
|
+
filesProcessed = Math.min(i + LAYER2_PARALLEL_BATCH_SIZE, files.length);
|
|
21032
|
+
if (onProgress && (filesProcessed - lastProgressUpdate >= PROGRESS_UPDATE_INTERVAL || filesProcessed === files.length)) {
|
|
21033
|
+
onProgress({
|
|
21034
|
+
status: "layer2",
|
|
21035
|
+
message: "Running structural scan (variables, logic gates)...",
|
|
21036
|
+
filesProcessed,
|
|
21037
|
+
totalFiles: files.length,
|
|
21038
|
+
vulnerabilitiesFound: vulnerabilities.length
|
|
21039
|
+
});
|
|
21040
|
+
lastProgressUpdate = filesProcessed;
|
|
21041
|
+
}
|
|
20801
21042
|
}
|
|
20802
21043
|
const dedupedVulnerabilities = deduplicateFindings(vulnerabilities);
|
|
20803
21044
|
const excludeTestFiles = options.excludeTestFiles !== false;
|
|
@@ -37170,7 +37411,7 @@ For each candidate finding, return:
|
|
|
37170
37411
|
console.log(` - Estimated cost: $${stats.estimatedCost.toFixed(4)}`);
|
|
37171
37412
|
return { vulnerabilities: validatedFindings, stats };
|
|
37172
37413
|
}
|
|
37173
|
-
async function validateFindingsWithAI(findings, files, projectContext) {
|
|
37414
|
+
async function validateFindingsWithAI(findings, files, projectContext, onProgress) {
|
|
37174
37415
|
const stats = {
|
|
37175
37416
|
totalFindings: findings.length,
|
|
37176
37417
|
validatedFindings: 0,
|
|
@@ -37220,9 +37461,17 @@ For each candidate finding, return:
|
|
|
37220
37461
|
let totalApiBatches = 0;
|
|
37221
37462
|
const totalFileBatches = Math.ceil(fileEntries.length / FILES_PER_API_BATCH);
|
|
37222
37463
|
console.log(`[AI Validation] Phase 2: Processing ${fileEntries.length} files in ${totalFileBatches} API batch(es) (${FILES_PER_API_BATCH} files/batch)`);
|
|
37464
|
+
let filesValidated = 0;
|
|
37223
37465
|
for (let batchStart = 0; batchStart < fileEntries.length; batchStart += FILES_PER_API_BATCH) {
|
|
37224
37466
|
const fileBatch = fileEntries.slice(batchStart, batchStart + FILES_PER_API_BATCH);
|
|
37225
37467
|
const batchNum = Math.floor(batchStart / FILES_PER_API_BATCH) + 1;
|
|
37468
|
+
if (onProgress) {
|
|
37469
|
+
onProgress({
|
|
37470
|
+
filesProcessed: filesValidated,
|
|
37471
|
+
totalFiles: fileEntries.length,
|
|
37472
|
+
status: `AI validating batch ${batchNum}/${totalFileBatches}`
|
|
37473
|
+
});
|
|
37474
|
+
}
|
|
37226
37475
|
console.log(`[AI Validation] API Batch ${batchNum}/${totalFileBatches}: ${fileBatch.length} files`);
|
|
37227
37476
|
const fileDataList = [];
|
|
37228
37477
|
const filesWithoutContent = [];
|
|
@@ -37358,6 +37607,14 @@ For each candidate finding, return:
|
|
|
37358
37607
|
}
|
|
37359
37608
|
const batchDuration = Date.now() - batchStartTime;
|
|
37360
37609
|
totalBatchWaitTime += batchDuration;
|
|
37610
|
+
filesValidated += fileBatch.length;
|
|
37611
|
+
if (onProgress) {
|
|
37612
|
+
onProgress({
|
|
37613
|
+
filesProcessed: filesValidated,
|
|
37614
|
+
totalFiles: fileEntries.length,
|
|
37615
|
+
status: `AI validation complete for batch ${batchNum}/${totalFileBatches}`
|
|
37616
|
+
});
|
|
37617
|
+
}
|
|
37361
37618
|
}
|
|
37362
37619
|
const totalCacheableTokens = stats.cacheCreationTokens + stats.cacheReadTokens;
|
|
37363
37620
|
stats.cacheHitRate = totalCacheableTokens > 0 ? stats.cacheReadTokens / totalCacheableTokens : 0;
|
|
@@ -38431,11 +38688,29 @@ var require_layer3 = __commonJS({
|
|
|
38431
38688
|
const vulnerabilities = [];
|
|
38432
38689
|
let aiAnalyzedCount = 0;
|
|
38433
38690
|
const maxAIFiles = options.maxFiles ?? MAX_AI_FILES;
|
|
38691
|
+
if (options.cancellationToken?.cancelled) {
|
|
38692
|
+
return {
|
|
38693
|
+
vulnerabilities: [],
|
|
38694
|
+
filesScanned: files.length,
|
|
38695
|
+
duration: Date.now() - startTime,
|
|
38696
|
+
aiAnalyzed: 0
|
|
38697
|
+
};
|
|
38698
|
+
}
|
|
38434
38699
|
const packageFiles = files.filter((f) => f.path.endsWith("package.json"));
|
|
38435
38700
|
for (const file of packageFiles) {
|
|
38701
|
+
if (options.cancellationToken?.cancelled)
|
|
38702
|
+
break;
|
|
38436
38703
|
const packageFindings = await (0, package_check_1.checkPackages)(file.content, file.path);
|
|
38437
38704
|
vulnerabilities.push(...packageFindings);
|
|
38438
38705
|
}
|
|
38706
|
+
if (options.cancellationToken?.cancelled) {
|
|
38707
|
+
return {
|
|
38708
|
+
vulnerabilities,
|
|
38709
|
+
filesScanned: files.length,
|
|
38710
|
+
duration: Date.now() - startTime,
|
|
38711
|
+
aiAnalyzed: 0
|
|
38712
|
+
};
|
|
38713
|
+
}
|
|
38439
38714
|
if (options.enableAI !== false) {
|
|
38440
38715
|
const aiCandidates = files.filter((f) => {
|
|
38441
38716
|
const hasPriorityExt = AI_PRIORITY_EXTENSIONS.some((ext2) => f.path.endsWith(ext2));
|
|
@@ -38722,7 +38997,7 @@ var require_dist = __commonJS({
|
|
|
38722
38997
|
for (var p2 in m2) if (p2 !== "default" && !Object.prototype.hasOwnProperty.call(exports3, p2)) __createBinding(exports3, m2, p2);
|
|
38723
38998
|
};
|
|
38724
38999
|
Object.defineProperty(exports2, "__esModule", { value: true });
|
|
38725
|
-
exports2.validateFindingsWithAI = exports2.buildProjectContext = exports2.runLayer3Scan = exports2.runLayer2Scan = exports2.runLayer1Scan = void 0;
|
|
39000
|
+
exports2.createCancellationToken = exports2.validateFindingsWithAI = exports2.buildProjectContext = exports2.runLayer3Scan = exports2.runLayer2Scan = exports2.runLayer1Scan = void 0;
|
|
38726
39001
|
exports2.runScan = runScan2;
|
|
38727
39002
|
exports2.computeIssueMixFromVulnerabilities = computeIssueMixFromVulnerabilities;
|
|
38728
39003
|
var types_1 = require_types();
|
|
@@ -38799,11 +39074,17 @@ var require_dist = __commonJS({
|
|
|
38799
39074
|
const isIncremental = scanModeConfig.mode === "incremental";
|
|
38800
39075
|
const depth = scanModeConfig.scanDepth || "cheap";
|
|
38801
39076
|
const quiet = options.quiet ?? false;
|
|
39077
|
+
const cancellationToken = options.cancellationToken;
|
|
38802
39078
|
const log = (message) => {
|
|
38803
39079
|
if (!quiet) {
|
|
38804
39080
|
console.log(message);
|
|
38805
39081
|
}
|
|
38806
39082
|
};
|
|
39083
|
+
const checkCancelled = () => {
|
|
39084
|
+
if (cancellationToken?.cancelled) {
|
|
39085
|
+
throw new Error(`Scan cancelled: ${cancellationToken.reason || "user requested"}`);
|
|
39086
|
+
}
|
|
39087
|
+
};
|
|
38807
39088
|
log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`);
|
|
38808
39089
|
if (isIncremental && scanModeConfig.changedFiles) {
|
|
38809
39090
|
log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`);
|
|
@@ -38820,8 +39101,11 @@ var require_dist = __commonJS({
|
|
|
38820
39101
|
}
|
|
38821
39102
|
};
|
|
38822
39103
|
const filesForAI = isIncremental && scanModeConfig.changedFiles ? files.filter((f) => scanModeConfig.changedFiles.some((cf) => f.path.endsWith(cf) || f.path.includes(cf))) : files;
|
|
39104
|
+
let middlewareConfig;
|
|
39105
|
+
let capturedValidationStats;
|
|
38823
39106
|
try {
|
|
38824
|
-
|
|
39107
|
+
checkCancelled();
|
|
39108
|
+
middlewareConfig = (0, middleware_detector_1.detectGlobalAuthMiddleware)(files);
|
|
38825
39109
|
if (middlewareConfig.hasAuthMiddleware) {
|
|
38826
39110
|
log(`[Scanner] repo=${repoInfo.name} auth_middleware=${middlewareConfig.authType || "unknown"} file=${middlewareConfig.middlewareFile}`);
|
|
38827
39111
|
}
|
|
@@ -38830,16 +39114,18 @@ var require_dist = __commonJS({
|
|
|
38830
39114
|
if (filesWithImportedAuth > 0) {
|
|
38831
39115
|
log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`);
|
|
38832
39116
|
}
|
|
39117
|
+
checkCancelled();
|
|
38833
39118
|
reportProgress("layer1", "Running surface scan (patterns, entropy, config)...");
|
|
38834
|
-
let layer1Result = await (0, layer1_1.runLayer1Scan)(files);
|
|
39119
|
+
let layer1Result = await (0, layer1_1.runLayer1Scan)(files, onProgress, cancellationToken);
|
|
38835
39120
|
const layer1RawCount = layer1Result.vulnerabilities.length;
|
|
38836
39121
|
layer1Result = {
|
|
38837
39122
|
...layer1Result,
|
|
38838
39123
|
vulnerabilities: (0, urls_1.aggregateLocalhostFindings)(layer1Result.vulnerabilities)
|
|
38839
39124
|
};
|
|
38840
39125
|
log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`);
|
|
39126
|
+
checkCancelled();
|
|
38841
39127
|
reportProgress("layer2", "Running structural scan (variables, logic gates)...", layer1Result.vulnerabilities.length);
|
|
38842
|
-
const layer2Result = await (0, layer2_1.runLayer2Scan)(files, { middlewareConfig, fileAuthImports });
|
|
39128
|
+
const layer2Result = await (0, layer2_1.runLayer2Scan)(files, { middlewareConfig, fileAuthImports }, onProgress, cancellationToken);
|
|
38843
39129
|
const heuristicBreakdown = Object.entries(layer2Result.stats.raw).filter(([, count]) => count > 0).map(([name, count]) => `${name}:${count}`).join(",");
|
|
38844
39130
|
log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b3) => a + b3, 0)} findings_deduped=${layer2Result.vulnerabilities.length} heuristic_breakdown={${heuristicBreakdown}}`);
|
|
38845
39131
|
const layer12Findings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities];
|
|
@@ -38867,17 +39153,33 @@ var require_dist = __commonJS({
|
|
|
38867
39153
|
}
|
|
38868
39154
|
const maxValidationFiles = scanModeConfig.maxAIValidationFiles || MAX_VALIDATION_CANDIDATES_PER_FILE;
|
|
38869
39155
|
const cappedValidation = capValidationCandidatesPerFile(afterAutoDismiss, maxValidationFiles);
|
|
39156
|
+
checkCancelled();
|
|
38870
39157
|
let validatedFindings = cappedValidation;
|
|
38871
|
-
let
|
|
39158
|
+
let capturedValidationStats2 = void 0;
|
|
38872
39159
|
const shouldValidate = options.enableAI !== false && !scanModeConfig.skipAIValidation && cappedValidation.length > 0;
|
|
38873
39160
|
if (shouldValidate) {
|
|
39161
|
+
checkCancelled();
|
|
38874
39162
|
reportProgress("validating", "AI validating findings (entropy, secrets, AI patterns)...", cappedValidation.length);
|
|
38875
39163
|
const findingsToValidate = isIncremental && scanModeConfig.changedFiles ? cappedValidation.filter((v2) => scanModeConfig.changedFiles.some((cf) => v2.filePath.endsWith(cf) || v2.filePath.includes(cf))) : cappedValidation;
|
|
38876
39164
|
if (findingsToValidate.length > 0) {
|
|
38877
|
-
const validationResult = await (0, anthropic_1.validateFindingsWithAI)(
|
|
39165
|
+
const validationResult = await (0, anthropic_1.validateFindingsWithAI)(
|
|
39166
|
+
findingsToValidate,
|
|
39167
|
+
filesForAI,
|
|
39168
|
+
void 0,
|
|
39169
|
+
// projectContext (uses default)
|
|
39170
|
+
onProgress ? (progress) => {
|
|
39171
|
+
onProgress({
|
|
39172
|
+
status: "validating",
|
|
39173
|
+
message: progress.status,
|
|
39174
|
+
filesProcessed: progress.filesProcessed,
|
|
39175
|
+
totalFiles: progress.totalFiles,
|
|
39176
|
+
vulnerabilitiesFound: allVulnerabilities.length
|
|
39177
|
+
});
|
|
39178
|
+
} : void 0
|
|
39179
|
+
);
|
|
38878
39180
|
validatedFindings = validationResult.vulnerabilities;
|
|
38879
39181
|
const { stats: validationStats } = validationResult;
|
|
38880
|
-
|
|
39182
|
+
capturedValidationStats2 = validationStats;
|
|
38881
39183
|
log(`[AI Validation] repo=${repoInfo.name} depth=${depth} candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`);
|
|
38882
39184
|
log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`);
|
|
38883
39185
|
const notValidated = cappedValidation.filter((v2) => !findingsToValidate.includes(v2));
|
|
@@ -38889,6 +39191,7 @@ var require_dist = __commonJS({
|
|
|
38889
39191
|
allVulnerabilities.push(...validatedFindings, ...noValidationNeeded);
|
|
38890
39192
|
const shouldRunLayer3 = options.enableAI !== false && !scanModeConfig.skipLayer3;
|
|
38891
39193
|
if (shouldRunLayer3) {
|
|
39194
|
+
checkCancelled();
|
|
38892
39195
|
reportProgress("layer3", "Running AI semantic analysis...", allVulnerabilities.length);
|
|
38893
39196
|
const filesToAnalyze = isIncremental ? filesForAI : files;
|
|
38894
39197
|
const maxLayer3Files = scanModeConfig.maxLayer3Files || 10;
|
|
@@ -38906,7 +39209,8 @@ var require_dist = __commonJS({
|
|
|
38906
39209
|
hasThrowingHelpers: true,
|
|
38907
39210
|
summary: authHelperContext.summary
|
|
38908
39211
|
} : void 0
|
|
38909
|
-
}
|
|
39212
|
+
},
|
|
39213
|
+
cancellationToken
|
|
38910
39214
|
});
|
|
38911
39215
|
allVulnerabilities.push(...layer3Result.vulnerabilities);
|
|
38912
39216
|
log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
|
|
@@ -38933,9 +39237,34 @@ var require_dist = __commonJS({
|
|
|
38933
39237
|
hasBlockingIssues,
|
|
38934
39238
|
scanDuration: Date.now() - startTime,
|
|
38935
39239
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38936
|
-
validationStats:
|
|
39240
|
+
validationStats: capturedValidationStats2
|
|
38937
39241
|
};
|
|
38938
39242
|
} catch (error) {
|
|
39243
|
+
if (cancellationToken?.cancelled) {
|
|
39244
|
+
reportProgress("failed", "Scan cancelled");
|
|
39245
|
+
const uniqueVulnerabilities = deduplicateVulnerabilities(allVulnerabilities);
|
|
39246
|
+
const resolvedVulnerabilities = resolveContradictions(uniqueVulnerabilities, middlewareConfig);
|
|
39247
|
+
const sortedVulnerabilities = sortBySeverity(resolvedVulnerabilities);
|
|
39248
|
+
const severityCounts = computeSeverityCounts(sortedVulnerabilities);
|
|
39249
|
+
const categoryCounts = computeCategoryCounts(sortedVulnerabilities);
|
|
39250
|
+
return {
|
|
39251
|
+
repoName: repoInfo.name,
|
|
39252
|
+
repoUrl: repoInfo.url,
|
|
39253
|
+
branch: repoInfo.branch,
|
|
39254
|
+
filesScanned: files.length,
|
|
39255
|
+
filesSkipped: 0,
|
|
39256
|
+
vulnerabilities: sortedVulnerabilities,
|
|
39257
|
+
severityCounts,
|
|
39258
|
+
categoryCounts,
|
|
39259
|
+
hasBlockingIssues: false,
|
|
39260
|
+
// Don't block on partial results
|
|
39261
|
+
scanDuration: Date.now() - startTime,
|
|
39262
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
39263
|
+
validationStats: capturedValidationStats,
|
|
39264
|
+
cancelled: true,
|
|
39265
|
+
cancelReason: cancellationToken.reason
|
|
39266
|
+
};
|
|
39267
|
+
}
|
|
38939
39268
|
reportProgress("failed", `Scan failed: ${error}`);
|
|
38940
39269
|
throw error;
|
|
38941
39270
|
}
|
|
@@ -39173,6 +39502,10 @@ Found ${group.length} occurrences at lines: ${lineDisplay}`,
|
|
|
39173
39502
|
Object.defineProperty(exports2, "validateFindingsWithAI", { enumerable: true, get: function() {
|
|
39174
39503
|
return anthropic_2.validateFindingsWithAI;
|
|
39175
39504
|
} });
|
|
39505
|
+
var types_2 = require_types();
|
|
39506
|
+
Object.defineProperty(exports2, "createCancellationToken", { enumerable: true, get: function() {
|
|
39507
|
+
return types_2.createCancellationToken;
|
|
39508
|
+
} });
|
|
39176
39509
|
}
|
|
39177
39510
|
});
|
|
39178
39511
|
|
|
@@ -43404,6 +43737,23 @@ async function collectFilesForScan(targetPath, options) {
|
|
|
43404
43737
|
}
|
|
43405
43738
|
return collectFiles(targetPath);
|
|
43406
43739
|
}
|
|
43740
|
+
function createEmptyResult(targetPath) {
|
|
43741
|
+
return {
|
|
43742
|
+
repoName: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
|
|
43743
|
+
repoUrl: "",
|
|
43744
|
+
branch: "local",
|
|
43745
|
+
filesScanned: 0,
|
|
43746
|
+
filesSkipped: 0,
|
|
43747
|
+
vulnerabilities: [],
|
|
43748
|
+
severityCounts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
43749
|
+
categoryCounts: {},
|
|
43750
|
+
hasBlockingIssues: false,
|
|
43751
|
+
scanDuration: 0,
|
|
43752
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43753
|
+
cancelled: true,
|
|
43754
|
+
cancelReason: "User cancelled scan"
|
|
43755
|
+
};
|
|
43756
|
+
}
|
|
43407
43757
|
function formatOutput(result, format, noColor) {
|
|
43408
43758
|
switch (format) {
|
|
43409
43759
|
case "json":
|
|
@@ -43501,6 +43851,10 @@ var quietSpinner = {
|
|
|
43501
43851
|
if (msg) console.error(msg);
|
|
43502
43852
|
return quietSpinner;
|
|
43503
43853
|
},
|
|
43854
|
+
warn: (msg) => {
|
|
43855
|
+
if (msg) console.warn(msg);
|
|
43856
|
+
return quietSpinner;
|
|
43857
|
+
},
|
|
43504
43858
|
stop: () => quietSpinner,
|
|
43505
43859
|
text: ""
|
|
43506
43860
|
};
|
|
@@ -43508,6 +43862,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
43508
43862
|
const spinner = options.quiet ? quietSpinner : ora();
|
|
43509
43863
|
const config = getConfig();
|
|
43510
43864
|
const noColor = options.color === false;
|
|
43865
|
+
const cancellationToken = (0, import_scanner.createCancellationToken)();
|
|
43511
43866
|
if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
|
|
43512
43867
|
if (!options.quiet) {
|
|
43513
43868
|
console.log("");
|
|
@@ -43540,22 +43895,28 @@ async function runScanOnce(targetPath, options) {
|
|
|
43540
43895
|
}
|
|
43541
43896
|
spinner.succeed(`Found ${files.length} files to scan`);
|
|
43542
43897
|
const onProgress = (progress) => {
|
|
43543
|
-
if (options.verbose) {
|
|
43544
|
-
|
|
43898
|
+
if (!options.quiet && !options.verbose) {
|
|
43899
|
+
if (progress.filesProcessed && progress.filesProcessed < progress.totalFiles) {
|
|
43900
|
+
console.log(`[Progress] ${progress.status}: ${progress.filesProcessed}/${progress.totalFiles} files`);
|
|
43901
|
+
}
|
|
43902
|
+
} else if (options.verbose) {
|
|
43903
|
+
if (progress.filesProcessed && progress.filesProcessed < progress.totalFiles) {
|
|
43904
|
+
console.log(`[Progress] ${progress.status}: ${progress.filesProcessed}/${progress.totalFiles} files`);
|
|
43905
|
+
}
|
|
43545
43906
|
}
|
|
43546
43907
|
const fileProgress = progress.filesProcessed && progress.filesProcessed < progress.totalFiles ? ` \u2022 ${progress.filesProcessed}/${progress.totalFiles} files` : ` (${progress.totalFiles} files)`;
|
|
43547
43908
|
switch (progress.status) {
|
|
43548
43909
|
case "layer1":
|
|
43549
|
-
spinner.text = `Layer 1: Pattern matching${fileProgress}
|
|
43910
|
+
spinner.text = `Layer 1: Pattern matching${fileProgress}`;
|
|
43550
43911
|
break;
|
|
43551
43912
|
case "layer2":
|
|
43552
|
-
spinner.text = `Layer 2: Code structure analysis${fileProgress}
|
|
43913
|
+
spinner.text = `Layer 2: Code structure analysis${fileProgress}`;
|
|
43553
43914
|
break;
|
|
43554
43915
|
case "validating":
|
|
43555
|
-
spinner.text = `AI validation${fileProgress}
|
|
43916
|
+
spinner.text = `AI validation${fileProgress}`;
|
|
43556
43917
|
break;
|
|
43557
43918
|
case "layer3":
|
|
43558
|
-
spinner.text = `Layer 3: Deep AI analysis${fileProgress}
|
|
43919
|
+
spinner.text = `Layer 3: Deep AI analysis${fileProgress}`;
|
|
43559
43920
|
break;
|
|
43560
43921
|
case "complete":
|
|
43561
43922
|
const issueText = progress.vulnerabilitiesFound === 1 ? "issue" : "issues";
|
|
@@ -43571,6 +43932,18 @@ async function runScanOnce(targetPath, options) {
|
|
|
43571
43932
|
}
|
|
43572
43933
|
};
|
|
43573
43934
|
let result;
|
|
43935
|
+
let sigintHandler = null;
|
|
43936
|
+
if (!options.quiet) {
|
|
43937
|
+
sigintHandler = () => {
|
|
43938
|
+
spinner.warn(source_default.yellow("\n\u26A0\uFE0F Cancelling scan... (press Ctrl+C again to force quit)"));
|
|
43939
|
+
cancellationToken.cancel("User pressed Ctrl+C");
|
|
43940
|
+
process.once("SIGINT", () => {
|
|
43941
|
+
spinner.fail(source_default.red("Scan aborted forcefully"));
|
|
43942
|
+
process.exit(130);
|
|
43943
|
+
});
|
|
43944
|
+
};
|
|
43945
|
+
process.once("SIGINT", sigintHandler);
|
|
43946
|
+
}
|
|
43574
43947
|
try {
|
|
43575
43948
|
spinner.start("Starting scan...");
|
|
43576
43949
|
const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
|
|
@@ -43600,15 +43973,38 @@ async function runScanOnce(targetPath, options) {
|
|
|
43600
43973
|
{
|
|
43601
43974
|
enableAI,
|
|
43602
43975
|
scanDepth: options.depth,
|
|
43603
|
-
quiet: shouldBeQuiet
|
|
43976
|
+
quiet: shouldBeQuiet,
|
|
43977
|
+
cancellationToken
|
|
43604
43978
|
},
|
|
43605
43979
|
onProgress
|
|
43606
43980
|
);
|
|
43607
43981
|
}
|
|
43608
43982
|
} catch (err) {
|
|
43983
|
+
if (cancellationToken.cancelled) {
|
|
43984
|
+
spinner.warn(source_default.yellow(`
|
|
43985
|
+
\u26A0\uFE0F Scan cancelled after processing some files`));
|
|
43986
|
+
if (result && result.vulnerabilities) {
|
|
43987
|
+
console.log(source_default.dim(`
|
|
43988
|
+
\u{1F4CA} Partial results: ${result.vulnerabilities.length} issues found before cancellation
|
|
43989
|
+
`));
|
|
43990
|
+
}
|
|
43991
|
+
return {
|
|
43992
|
+
result: result || createEmptyResult(targetPath),
|
|
43993
|
+
output: "Scan cancelled",
|
|
43994
|
+
exitCode: 130
|
|
43995
|
+
// Standard SIGINT exit code
|
|
43996
|
+
};
|
|
43997
|
+
}
|
|
43609
43998
|
const enhanced = enhanceError(err);
|
|
43610
43999
|
spinner.fail(enhanced.message);
|
|
43611
44000
|
throw err;
|
|
44001
|
+
} finally {
|
|
44002
|
+
if (sigintHandler) {
|
|
44003
|
+
process.off("SIGINT", sigintHandler);
|
|
44004
|
+
}
|
|
44005
|
+
}
|
|
44006
|
+
if (!result) {
|
|
44007
|
+
throw new Error("Scan failed: no result returned");
|
|
43612
44008
|
}
|
|
43613
44009
|
const output = formatOutput(result, options.format, noColor);
|
|
43614
44010
|
if (options.output) {
|
|
@@ -47872,7 +48268,7 @@ var usageCommand = new Command("usage").description("Show current usage and quot
|
|
|
47872
48268
|
|
|
47873
48269
|
// src/index.ts
|
|
47874
48270
|
var program2 = new Command();
|
|
47875
|
-
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.
|
|
48271
|
+
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.7").addHelpText("after", `
|
|
47876
48272
|
Quick Start:
|
|
47877
48273
|
$ oculum scan . Scan current directory (free)
|
|
47878
48274
|
$ oculum ui Interactive mode with guided setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oculum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "AI-native security scanner CLI for detecting vulnerabilities in AI-generated code, BYOK patterns, and modern web applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"url": "https://github.com/flexipie/oculum/issues"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.
|
|
21
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined' --define:VERSION='\"1.0.7\"'",
|
|
22
22
|
"dev": "npm run build -- --watch",
|
|
23
23
|
"test": "echo \"No tests configured yet\"",
|
|
24
24
|
"lint": "eslint src/"
|