@oculum/cli 1.0.4 → 1.0.5
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 +519 -24
- 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
|
|
|
@@ -42798,6 +43131,103 @@ async function callBackendAPI(files, depth, apiKey, repoInfo) {
|
|
|
42798
43131
|
}
|
|
42799
43132
|
return data.result;
|
|
42800
43133
|
}
|
|
43134
|
+
async function callBackendAPIStream(files, depth, apiKey, repoInfo, onProgress) {
|
|
43135
|
+
const baseUrl = getApiBaseUrl();
|
|
43136
|
+
const streamUrl = `${baseUrl}/v1/scan/stream`;
|
|
43137
|
+
try {
|
|
43138
|
+
const response = await fetch(streamUrl, {
|
|
43139
|
+
method: "POST",
|
|
43140
|
+
headers: {
|
|
43141
|
+
"Content-Type": "application/json",
|
|
43142
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
43143
|
+
"X-Oculum-Client": "cli",
|
|
43144
|
+
"X-Oculum-Version": "1.0.0"
|
|
43145
|
+
},
|
|
43146
|
+
body: JSON.stringify({
|
|
43147
|
+
files,
|
|
43148
|
+
depth,
|
|
43149
|
+
repoName: repoInfo.name,
|
|
43150
|
+
repoUrl: repoInfo.url,
|
|
43151
|
+
branch: repoInfo.branch
|
|
43152
|
+
})
|
|
43153
|
+
});
|
|
43154
|
+
if (!response.ok) {
|
|
43155
|
+
console.warn(`[API] SSE endpoint failed with ${response.status}, falling back to regular POST`);
|
|
43156
|
+
return await callBackendAPI(files, depth, apiKey, repoInfo);
|
|
43157
|
+
}
|
|
43158
|
+
if (!response.body) {
|
|
43159
|
+
throw new APIError("Response body is empty", 500);
|
|
43160
|
+
}
|
|
43161
|
+
const reader = response.body.getReader();
|
|
43162
|
+
const decoder = new TextDecoder();
|
|
43163
|
+
let buffer = "";
|
|
43164
|
+
let result = null;
|
|
43165
|
+
while (true) {
|
|
43166
|
+
const { done, value } = await reader.read();
|
|
43167
|
+
if (done) break;
|
|
43168
|
+
buffer += decoder.decode(value, { stream: true });
|
|
43169
|
+
const lines = buffer.split("\n");
|
|
43170
|
+
buffer = lines.pop() || "";
|
|
43171
|
+
let currentEvent = null;
|
|
43172
|
+
let currentData = null;
|
|
43173
|
+
for (const line of lines) {
|
|
43174
|
+
if (line.startsWith("event: ")) {
|
|
43175
|
+
currentEvent = line.slice(7).trim();
|
|
43176
|
+
} else if (line.startsWith("data: ")) {
|
|
43177
|
+
currentData = line.slice(6).trim();
|
|
43178
|
+
} else if (line === "" && currentEvent && currentData) {
|
|
43179
|
+
try {
|
|
43180
|
+
const data = JSON.parse(currentData);
|
|
43181
|
+
switch (currentEvent) {
|
|
43182
|
+
case "progress":
|
|
43183
|
+
if (onProgress) {
|
|
43184
|
+
onProgress({
|
|
43185
|
+
status: data.status,
|
|
43186
|
+
message: data.message || "",
|
|
43187
|
+
filesProcessed: data.filesProcessed || 0,
|
|
43188
|
+
totalFiles: data.totalFiles || 0,
|
|
43189
|
+
vulnerabilitiesFound: data.vulnerabilitiesFound || 0
|
|
43190
|
+
});
|
|
43191
|
+
}
|
|
43192
|
+
break;
|
|
43193
|
+
case "complete":
|
|
43194
|
+
if (!data.result) {
|
|
43195
|
+
console.error("[API] Complete event received but no result in data:", data);
|
|
43196
|
+
throw new APIError("Backend scan completed but returned no result data", 500);
|
|
43197
|
+
}
|
|
43198
|
+
result = data.result;
|
|
43199
|
+
break;
|
|
43200
|
+
case "error":
|
|
43201
|
+
console.error("[API] Backend sent error event:", data);
|
|
43202
|
+
throw new APIError(
|
|
43203
|
+
data.message || data.error || "Scan failed",
|
|
43204
|
+
data.status || 500,
|
|
43205
|
+
data.error
|
|
43206
|
+
);
|
|
43207
|
+
}
|
|
43208
|
+
} catch (parseError) {
|
|
43209
|
+
if (parseError instanceof APIError) throw parseError;
|
|
43210
|
+
console.error("[API] Failed to parse SSE event:", parseError);
|
|
43211
|
+
}
|
|
43212
|
+
currentEvent = null;
|
|
43213
|
+
currentData = null;
|
|
43214
|
+
}
|
|
43215
|
+
}
|
|
43216
|
+
}
|
|
43217
|
+
if (!result) {
|
|
43218
|
+
console.error("[API] Stream ended without receiving complete event");
|
|
43219
|
+
throw new APIError("No result received from stream", 500);
|
|
43220
|
+
}
|
|
43221
|
+
console.log("[API] SSE stream completed successfully");
|
|
43222
|
+
return result;
|
|
43223
|
+
} catch (error) {
|
|
43224
|
+
if (error instanceof APIError) {
|
|
43225
|
+
throw error;
|
|
43226
|
+
}
|
|
43227
|
+
console.warn("[API] SSE streaming failed, falling back to regular POST:", error);
|
|
43228
|
+
return await callBackendAPI(files, depth, apiKey, repoInfo);
|
|
43229
|
+
}
|
|
43230
|
+
}
|
|
42801
43231
|
async function verifyApiKey(apiKey) {
|
|
42802
43232
|
const baseUrl = getApiBaseUrl();
|
|
42803
43233
|
const url = `${baseUrl}/v1/verify-key`;
|
|
@@ -43404,6 +43834,23 @@ async function collectFilesForScan(targetPath, options) {
|
|
|
43404
43834
|
}
|
|
43405
43835
|
return collectFiles(targetPath);
|
|
43406
43836
|
}
|
|
43837
|
+
function createEmptyResult(targetPath) {
|
|
43838
|
+
return {
|
|
43839
|
+
repoName: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
|
|
43840
|
+
repoUrl: "",
|
|
43841
|
+
branch: "local",
|
|
43842
|
+
filesScanned: 0,
|
|
43843
|
+
filesSkipped: 0,
|
|
43844
|
+
vulnerabilities: [],
|
|
43845
|
+
severityCounts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
43846
|
+
categoryCounts: {},
|
|
43847
|
+
hasBlockingIssues: false,
|
|
43848
|
+
scanDuration: 0,
|
|
43849
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
43850
|
+
cancelled: true,
|
|
43851
|
+
cancelReason: "User cancelled scan"
|
|
43852
|
+
};
|
|
43853
|
+
}
|
|
43407
43854
|
function formatOutput(result, format, noColor) {
|
|
43408
43855
|
switch (format) {
|
|
43409
43856
|
case "json":
|
|
@@ -43501,6 +43948,10 @@ var quietSpinner = {
|
|
|
43501
43948
|
if (msg) console.error(msg);
|
|
43502
43949
|
return quietSpinner;
|
|
43503
43950
|
},
|
|
43951
|
+
warn: (msg) => {
|
|
43952
|
+
if (msg) console.warn(msg);
|
|
43953
|
+
return quietSpinner;
|
|
43954
|
+
},
|
|
43504
43955
|
stop: () => quietSpinner,
|
|
43505
43956
|
text: ""
|
|
43506
43957
|
};
|
|
@@ -43508,6 +43959,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
43508
43959
|
const spinner = options.quiet ? quietSpinner : ora();
|
|
43509
43960
|
const config = getConfig();
|
|
43510
43961
|
const noColor = options.color === false;
|
|
43962
|
+
const cancellationToken = (0, import_scanner.createCancellationToken)();
|
|
43511
43963
|
if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
|
|
43512
43964
|
if (!options.quiet) {
|
|
43513
43965
|
console.log("");
|
|
@@ -43540,22 +43992,28 @@ async function runScanOnce(targetPath, options) {
|
|
|
43540
43992
|
}
|
|
43541
43993
|
spinner.succeed(`Found ${files.length} files to scan`);
|
|
43542
43994
|
const onProgress = (progress) => {
|
|
43543
|
-
if (options.verbose) {
|
|
43544
|
-
|
|
43995
|
+
if (!options.quiet && !options.verbose) {
|
|
43996
|
+
if (progress.filesProcessed && progress.filesProcessed < progress.totalFiles) {
|
|
43997
|
+
console.log(`[Progress] ${progress.status}: ${progress.filesProcessed}/${progress.totalFiles} files`);
|
|
43998
|
+
}
|
|
43999
|
+
} else if (options.verbose) {
|
|
44000
|
+
if (progress.filesProcessed && progress.filesProcessed < progress.totalFiles) {
|
|
44001
|
+
console.log(`[Progress] ${progress.status}: ${progress.filesProcessed}/${progress.totalFiles} files`);
|
|
44002
|
+
}
|
|
43545
44003
|
}
|
|
43546
44004
|
const fileProgress = progress.filesProcessed && progress.filesProcessed < progress.totalFiles ? ` \u2022 ${progress.filesProcessed}/${progress.totalFiles} files` : ` (${progress.totalFiles} files)`;
|
|
43547
44005
|
switch (progress.status) {
|
|
43548
44006
|
case "layer1":
|
|
43549
|
-
spinner.text = `Layer 1: Pattern matching${fileProgress}
|
|
44007
|
+
spinner.text = `Layer 1: Pattern matching${fileProgress}`;
|
|
43550
44008
|
break;
|
|
43551
44009
|
case "layer2":
|
|
43552
|
-
spinner.text = `Layer 2: Code structure analysis${fileProgress}
|
|
44010
|
+
spinner.text = `Layer 2: Code structure analysis${fileProgress}`;
|
|
43553
44011
|
break;
|
|
43554
44012
|
case "validating":
|
|
43555
|
-
spinner.text = `AI validation${fileProgress}
|
|
44013
|
+
spinner.text = `AI validation${fileProgress}`;
|
|
43556
44014
|
break;
|
|
43557
44015
|
case "layer3":
|
|
43558
|
-
spinner.text = `Layer 3: Deep AI analysis${fileProgress}
|
|
44016
|
+
spinner.text = `Layer 3: Deep AI analysis${fileProgress}`;
|
|
43559
44017
|
break;
|
|
43560
44018
|
case "complete":
|
|
43561
44019
|
const issueText = progress.vulnerabilitiesFound === 1 ? "issue" : "issues";
|
|
@@ -43571,12 +44029,24 @@ async function runScanOnce(targetPath, options) {
|
|
|
43571
44029
|
}
|
|
43572
44030
|
};
|
|
43573
44031
|
let result;
|
|
44032
|
+
let sigintHandler = null;
|
|
44033
|
+
if (!options.quiet) {
|
|
44034
|
+
sigintHandler = () => {
|
|
44035
|
+
spinner.warn(source_default.yellow("\n\u26A0\uFE0F Cancelling scan... (press Ctrl+C again to force quit)"));
|
|
44036
|
+
cancellationToken.cancel("User pressed Ctrl+C");
|
|
44037
|
+
process.once("SIGINT", () => {
|
|
44038
|
+
spinner.fail(source_default.red("Scan aborted forcefully"));
|
|
44039
|
+
process.exit(130);
|
|
44040
|
+
});
|
|
44041
|
+
};
|
|
44042
|
+
process.once("SIGINT", sigintHandler);
|
|
44043
|
+
}
|
|
43574
44044
|
try {
|
|
43575
44045
|
spinner.start("Starting scan...");
|
|
43576
44046
|
const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
|
|
43577
44047
|
if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
|
|
43578
|
-
spinner.text = `Backend ${options.depth} scan
|
|
43579
|
-
result = await
|
|
44048
|
+
spinner.text = `Backend ${options.depth} scan starting...`;
|
|
44049
|
+
result = await callBackendAPIStream(
|
|
43580
44050
|
files,
|
|
43581
44051
|
options.depth,
|
|
43582
44052
|
config.apiKey,
|
|
@@ -43584,7 +44054,9 @@ async function runScanOnce(targetPath, options) {
|
|
|
43584
44054
|
name: (0, import_path3.basename)((0, import_path3.resolve)(targetPath)),
|
|
43585
44055
|
url: "",
|
|
43586
44056
|
branch: "local"
|
|
43587
|
-
}
|
|
44057
|
+
},
|
|
44058
|
+
onProgress
|
|
44059
|
+
// Stream real-time progress from backend
|
|
43588
44060
|
);
|
|
43589
44061
|
spinner.succeed(`Backend ${options.depth} scan complete`);
|
|
43590
44062
|
} else {
|
|
@@ -43600,15 +44072,38 @@ async function runScanOnce(targetPath, options) {
|
|
|
43600
44072
|
{
|
|
43601
44073
|
enableAI,
|
|
43602
44074
|
scanDepth: options.depth,
|
|
43603
|
-
quiet: shouldBeQuiet
|
|
44075
|
+
quiet: shouldBeQuiet,
|
|
44076
|
+
cancellationToken
|
|
43604
44077
|
},
|
|
43605
44078
|
onProgress
|
|
43606
44079
|
);
|
|
43607
44080
|
}
|
|
43608
44081
|
} catch (err) {
|
|
44082
|
+
if (cancellationToken.cancelled) {
|
|
44083
|
+
spinner.warn(source_default.yellow(`
|
|
44084
|
+
\u26A0\uFE0F Scan cancelled after processing some files`));
|
|
44085
|
+
if (result && result.vulnerabilities) {
|
|
44086
|
+
console.log(source_default.dim(`
|
|
44087
|
+
\u{1F4CA} Partial results: ${result.vulnerabilities.length} issues found before cancellation
|
|
44088
|
+
`));
|
|
44089
|
+
}
|
|
44090
|
+
return {
|
|
44091
|
+
result: result || createEmptyResult(targetPath),
|
|
44092
|
+
output: "Scan cancelled",
|
|
44093
|
+
exitCode: 130
|
|
44094
|
+
// Standard SIGINT exit code
|
|
44095
|
+
};
|
|
44096
|
+
}
|
|
43609
44097
|
const enhanced = enhanceError(err);
|
|
43610
44098
|
spinner.fail(enhanced.message);
|
|
43611
44099
|
throw err;
|
|
44100
|
+
} finally {
|
|
44101
|
+
if (sigintHandler) {
|
|
44102
|
+
process.off("SIGINT", sigintHandler);
|
|
44103
|
+
}
|
|
44104
|
+
}
|
|
44105
|
+
if (!result) {
|
|
44106
|
+
throw new Error("Scan failed: no result returned");
|
|
43612
44107
|
}
|
|
43613
44108
|
const output = formatOutput(result, options.format, noColor);
|
|
43614
44109
|
if (options.output) {
|
|
@@ -47872,7 +48367,7 @@ var usageCommand = new Command("usage").description("Show current usage and quot
|
|
|
47872
48367
|
|
|
47873
48368
|
// src/index.ts
|
|
47874
48369
|
var program2 = new Command();
|
|
47875
|
-
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.
|
|
48370
|
+
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.5").addHelpText("after", `
|
|
47876
48371
|
Quick Start:
|
|
47877
48372
|
$ oculum scan . Scan current directory (free)
|
|
47878
48373
|
$ 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.5",
|
|
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.5\"'",
|
|
22
22
|
"dev": "npm run build -- --watch",
|
|
23
23
|
"test": "echo \"No tests configured yet\"",
|
|
24
24
|
"lint": "eslint src/"
|