@oculum/cli 1.0.7 → 1.0.9
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 +859 -133
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -214,9 +214,9 @@ var require_help = __commonJS({
|
|
|
214
214
|
*/
|
|
215
215
|
visibleCommands(cmd) {
|
|
216
216
|
const visibleCommands = cmd.commands.filter((cmd2) => !cmd2._hidden);
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
visibleCommands.push(
|
|
217
|
+
const helpCommand2 = cmd._getHelpCommand();
|
|
218
|
+
if (helpCommand2 && !helpCommand2._hidden) {
|
|
219
|
+
visibleCommands.push(helpCommand2);
|
|
220
220
|
}
|
|
221
221
|
if (this.sortSubcommands) {
|
|
222
222
|
visibleCommands.sort((a, b3) => {
|
|
@@ -1304,12 +1304,12 @@ var require_command = __commonJS({
|
|
|
1304
1304
|
enableOrNameAndArgs = enableOrNameAndArgs ?? "help [command]";
|
|
1305
1305
|
const [, helpName, helpArgs] = enableOrNameAndArgs.match(/([^ ]+) *(.*)/);
|
|
1306
1306
|
const helpDescription = description ?? "display help for command";
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
if (helpArgs)
|
|
1310
|
-
if (helpDescription)
|
|
1307
|
+
const helpCommand2 = this.createCommand(helpName);
|
|
1308
|
+
helpCommand2.helpOption(false);
|
|
1309
|
+
if (helpArgs) helpCommand2.arguments(helpArgs);
|
|
1310
|
+
if (helpDescription) helpCommand2.description(helpDescription);
|
|
1311
1311
|
this._addImplicitHelpCommand = true;
|
|
1312
|
-
this._helpCommand =
|
|
1312
|
+
this._helpCommand = helpCommand2;
|
|
1313
1313
|
return this;
|
|
1314
1314
|
}
|
|
1315
1315
|
/**
|
|
@@ -1319,13 +1319,13 @@ var require_command = __commonJS({
|
|
|
1319
1319
|
* @param {string} [deprecatedDescription] - deprecated custom description used with custom name only
|
|
1320
1320
|
* @return {Command} `this` command for chaining
|
|
1321
1321
|
*/
|
|
1322
|
-
addHelpCommand(
|
|
1323
|
-
if (typeof
|
|
1324
|
-
this.helpCommand(
|
|
1322
|
+
addHelpCommand(helpCommand2, deprecatedDescription) {
|
|
1323
|
+
if (typeof helpCommand2 !== "object") {
|
|
1324
|
+
this.helpCommand(helpCommand2, deprecatedDescription);
|
|
1325
1325
|
return this;
|
|
1326
1326
|
}
|
|
1327
1327
|
this._addImplicitHelpCommand = true;
|
|
1328
|
-
this._helpCommand =
|
|
1328
|
+
this._helpCommand = helpCommand2;
|
|
1329
1329
|
return this;
|
|
1330
1330
|
}
|
|
1331
1331
|
/**
|
|
@@ -11572,6 +11572,8 @@ var require_context_helpers = __commonJS({
|
|
|
11572
11572
|
exports2.isDocumentationFile = isDocumentationFile;
|
|
11573
11573
|
exports2.isScannerOrFixtureFile = isScannerOrFixtureFile;
|
|
11574
11574
|
exports2.isClientBundledFile = isClientBundledFile;
|
|
11575
|
+
exports2.isSeedOrDataGenFile = isSeedOrDataGenFile;
|
|
11576
|
+
exports2.isEducationalVulnerabilityFile = isEducationalVulnerabilityFile;
|
|
11575
11577
|
exports2.isEnvVarReference = isEnvVarReference;
|
|
11576
11578
|
exports2.isNextPublicEnvVar = isNextPublicEnvVar;
|
|
11577
11579
|
exports2.isComment = isComment;
|
|
@@ -11735,6 +11737,35 @@ var require_context_helpers = __commonJS({
|
|
|
11735
11737
|
}
|
|
11736
11738
|
return clientPatterns.some((pattern) => pattern.test(filePath));
|
|
11737
11739
|
}
|
|
11740
|
+
function isSeedOrDataGenFile(filePath) {
|
|
11741
|
+
const patterns = [
|
|
11742
|
+
/\/seed\//i,
|
|
11743
|
+
/\/seeds\//i,
|
|
11744
|
+
/seed-database\.(ts|js)$/i,
|
|
11745
|
+
/\/seeder\./i,
|
|
11746
|
+
/datacreator\.(ts|js)$/i,
|
|
11747
|
+
/\/data\/.*creator/i,
|
|
11748
|
+
/\/fixtures\//i,
|
|
11749
|
+
/\.fixture\./i,
|
|
11750
|
+
/\/generators?\//i,
|
|
11751
|
+
/\/factories\//i,
|
|
11752
|
+
/factory\.(ts|js)$/i
|
|
11753
|
+
];
|
|
11754
|
+
return patterns.some((p2) => p2.test(filePath));
|
|
11755
|
+
}
|
|
11756
|
+
function isEducationalVulnerabilityFile(filePath) {
|
|
11757
|
+
const patterns = [
|
|
11758
|
+
/\/insecurity\.(ts|js)$/i,
|
|
11759
|
+
/\/vulnerable\.(ts|js)$/i,
|
|
11760
|
+
/\/intentionally-vulnerable/i,
|
|
11761
|
+
/\/security-examples?\//i,
|
|
11762
|
+
/\/vuln-examples?\//i,
|
|
11763
|
+
/\/challenge-\d+/i,
|
|
11764
|
+
// OWASP Juice Shop challenges
|
|
11765
|
+
/\/exploit-examples?\//i
|
|
11766
|
+
];
|
|
11767
|
+
return patterns.some((p2) => p2.test(filePath));
|
|
11768
|
+
}
|
|
11738
11769
|
function isEnvVarReference(line) {
|
|
11739
11770
|
return /process\.env\.[A-Z_]+/.test(line) || /\$\{?[A-Z_]+\}?/.test(line) || /import\.meta\.env\.[A-Z_]+/.test(line) || /Deno\.env\.get\(/.test(line) || /os\.environ\[/.test(line) || // Python
|
|
11740
11771
|
/os\.getenv\(/.test(line) || // Python
|
|
@@ -11977,12 +12008,12 @@ var require_entropy = __commonJS({
|
|
|
11977
12008
|
const strings = [];
|
|
11978
12009
|
const lines = content.split("\n");
|
|
11979
12010
|
const patterns = [
|
|
11980
|
-
/"
|
|
11981
|
-
// Double-quoted strings 20+ chars
|
|
11982
|
-
/'
|
|
11983
|
-
// Single-quoted strings 20+ chars
|
|
11984
|
-
/`
|
|
11985
|
-
// Template literals 20+ chars
|
|
12011
|
+
/"[^"\\]{20,}(?:\\.[^"\\]*)*"/g,
|
|
12012
|
+
// Double-quoted strings 20+ chars (unrolled loop)
|
|
12013
|
+
/'[^'\\]{20,}(?:\\.[^'\\]*)*'/g,
|
|
12014
|
+
// Single-quoted strings 20+ chars (unrolled loop)
|
|
12015
|
+
/`[^`\\]{20,}(?:\\.[^`\\]*)*`/g
|
|
12016
|
+
// Template literals 20+ chars (unrolled loop)
|
|
11986
12017
|
];
|
|
11987
12018
|
lines.forEach((line, index) => {
|
|
11988
12019
|
for (const pattern of patterns) {
|
|
@@ -15848,19 +15879,11 @@ var require_dangerous_functions = __commonJS({
|
|
|
15848
15879
|
// Math.random() * 100 + 50 + 'px'
|
|
15849
15880
|
/Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bms\b/i,
|
|
15850
15881
|
// Math.random() * 1000 + 500 + 'ms'
|
|
15851
|
-
/Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i
|
|
15882
|
+
/Math\.random.*\*\s*\d+\s*\+\s*\d+.*\bs\b/i
|
|
15852
15883
|
// Math.random() * 5 + 2 + 's'
|
|
15853
|
-
//
|
|
15854
|
-
/
|
|
15855
|
-
//
|
|
15856
|
-
/Math\.random\(\)\.toString\(36\)\.substr\(/,
|
|
15857
|
-
// .substr() variant
|
|
15858
|
-
/Math\.random\(\)\.toString\(36\)\.slice\(/,
|
|
15859
|
-
// .slice() variant
|
|
15860
|
-
/Math\.random\(\)\.toString\(16\)\.substring\(/,
|
|
15861
|
-
// .toString(16).substring() - hex UI IDs
|
|
15862
|
-
/Math\.random\(\)\.toString\(16\)\.slice\(/
|
|
15863
|
-
// hex slice variant
|
|
15884
|
+
// NOTE: toString patterns removed - now handled by analyzeToStringPattern()
|
|
15885
|
+
// which provides more granular severity classification (info/low/medium/high)
|
|
15886
|
+
// based on truncation length and context
|
|
15864
15887
|
];
|
|
15865
15888
|
if (cosmeticLinePatterns.some((p2) => p2.test(lineContent))) {
|
|
15866
15889
|
return true;
|
|
@@ -15911,6 +15934,71 @@ var require_dangerous_functions = __commonJS({
|
|
|
15911
15934
|
}
|
|
15912
15935
|
return false;
|
|
15913
15936
|
}
|
|
15937
|
+
function extractFunctionContext(content, lineNumber) {
|
|
15938
|
+
const lines = content.split("\n");
|
|
15939
|
+
const start = Math.max(0, lineNumber - 10);
|
|
15940
|
+
for (let i = lineNumber; i >= start; i--) {
|
|
15941
|
+
const line = lines[i];
|
|
15942
|
+
const funcDeclMatch = line.match(/(?:export\s+)?function\s+(\w+)/i);
|
|
15943
|
+
if (funcDeclMatch) {
|
|
15944
|
+
return funcDeclMatch[1].toLowerCase();
|
|
15945
|
+
}
|
|
15946
|
+
const arrowFuncMatch = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\(|async)/i);
|
|
15947
|
+
if (arrowFuncMatch) {
|
|
15948
|
+
return arrowFuncMatch[1].toLowerCase();
|
|
15949
|
+
}
|
|
15950
|
+
}
|
|
15951
|
+
return null;
|
|
15952
|
+
}
|
|
15953
|
+
function classifyFunctionIntent(functionName) {
|
|
15954
|
+
if (!functionName)
|
|
15955
|
+
return "unknown";
|
|
15956
|
+
const lower = functionName.toLowerCase();
|
|
15957
|
+
const uuidPatterns = ["uuid", "guid", "uniqueid", "correlationid"];
|
|
15958
|
+
const idGenerationPatterns = /^(generate|create|make|build)(id|identifier)$/i;
|
|
15959
|
+
if (uuidPatterns.some((p2) => lower.includes(p2)) || idGenerationPatterns.test(lower)) {
|
|
15960
|
+
return "uuid";
|
|
15961
|
+
}
|
|
15962
|
+
const captchaPatterns = ["captcha", "puzzle", "mathproblem"];
|
|
15963
|
+
if (captchaPatterns.some((p2) => lower.includes(p2)))
|
|
15964
|
+
return "captcha";
|
|
15965
|
+
if (lower.includes("challenge") && !lower.includes("auth"))
|
|
15966
|
+
return "captcha";
|
|
15967
|
+
const demoPatterns = ["seed", "fixture", "demo", "mock", "fake"];
|
|
15968
|
+
if (demoPatterns.some((p2) => lower.includes(p2)))
|
|
15969
|
+
return "demo";
|
|
15970
|
+
const securityPatterns = ["token", "secret", "key", "password", "credential", "signature"];
|
|
15971
|
+
const securityFunctionPattern = /^(generate|create|make)(token|secret|key|session|password|credential)/i;
|
|
15972
|
+
if (securityPatterns.some((p2) => lower.includes(p2)) || securityFunctionPattern.test(lower)) {
|
|
15973
|
+
return "security";
|
|
15974
|
+
}
|
|
15975
|
+
return "unknown";
|
|
15976
|
+
}
|
|
15977
|
+
function analyzeToStringPattern(lineContent) {
|
|
15978
|
+
const toString36Match = lineContent.match(/Math\.random\(\)\.toString\(36\)/);
|
|
15979
|
+
const toString16Match = lineContent.match(/Math\.random\(\)\.toString\(16\)/);
|
|
15980
|
+
if (!toString36Match && !toString16Match) {
|
|
15981
|
+
return { hasToString: false, base: null, isTruncated: false, truncationLength: null, intent: "unknown" };
|
|
15982
|
+
}
|
|
15983
|
+
const base = toString36Match ? 36 : 16;
|
|
15984
|
+
const substringMatch = lineContent.match(/\.substring\((\d+)(?:,\s*(\d+))?\)/);
|
|
15985
|
+
const sliceMatch = lineContent.match(/\.slice\((\d+)(?:,\s*(\d+))?\)/);
|
|
15986
|
+
const substrMatch = lineContent.match(/\.substr\((\d+)(?:,\s*(\d+))?\)/);
|
|
15987
|
+
const truncMatch = substringMatch || sliceMatch || substrMatch;
|
|
15988
|
+
if (!truncMatch) {
|
|
15989
|
+
return { hasToString: true, base, isTruncated: false, truncationLength: null, intent: "full-token" };
|
|
15990
|
+
}
|
|
15991
|
+
const start = parseInt(truncMatch[1]);
|
|
15992
|
+
const end = truncMatch[2] ? parseInt(truncMatch[2]) : null;
|
|
15993
|
+
const length = end ? end - start : null;
|
|
15994
|
+
if (length && length <= 9) {
|
|
15995
|
+
return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: "short-ui-id" };
|
|
15996
|
+
} else if (length && length <= 15) {
|
|
15997
|
+
return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: "business-id" };
|
|
15998
|
+
} else {
|
|
15999
|
+
return { hasToString: true, base, isTruncated: true, truncationLength: length, intent: "business-id" };
|
|
16000
|
+
}
|
|
16001
|
+
}
|
|
15914
16002
|
function extractMathRandomVariableName(lineContent) {
|
|
15915
16003
|
const assignmentMatch = lineContent.match(/(?:const|let|var)\s+(\w+)\s*=.*Math\.random/);
|
|
15916
16004
|
if (assignmentMatch)
|
|
@@ -16021,9 +16109,7 @@ var require_dangerous_functions = __commonJS({
|
|
|
16021
16109
|
const inUIContext = isCosmeticMathRandom(lineContent, content, lineNumber);
|
|
16022
16110
|
const businessLogicPatterns = [
|
|
16023
16111
|
/\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
|
|
16112
|
+
/\b(reference|tracking|confirmation)Number\b/i
|
|
16027
16113
|
];
|
|
16028
16114
|
const inBusinessLogicContext = businessLogicPatterns.some((p2) => p2.test(context)) && !inSecurityContext;
|
|
16029
16115
|
let contextDescription = "unknown context";
|
|
@@ -16267,12 +16353,24 @@ var require_dangerous_functions = __commonJS({
|
|
|
16267
16353
|
}
|
|
16268
16354
|
}
|
|
16269
16355
|
if (funcPattern.name === "Math.random for security") {
|
|
16356
|
+
if ((0, context_helpers_1.isSeedOrDataGenFile)(filePath)) {
|
|
16357
|
+
break;
|
|
16358
|
+
}
|
|
16359
|
+
if ((0, context_helpers_1.isEducationalVulnerabilityFile)(filePath)) {
|
|
16360
|
+
break;
|
|
16361
|
+
}
|
|
16270
16362
|
const varName = extractMathRandomVariableName(line);
|
|
16271
16363
|
const nameRisk = classifyVariableNameRisk(varName);
|
|
16272
16364
|
const context = analyzeMathRandomContext(content, filePath, index);
|
|
16365
|
+
const functionName = extractFunctionContext(content, index);
|
|
16366
|
+
const functionIntent = classifyFunctionIntent(functionName);
|
|
16367
|
+
const toStringPattern = analyzeToStringPattern(line);
|
|
16273
16368
|
if (context.inUIContext) {
|
|
16274
16369
|
break;
|
|
16275
16370
|
}
|
|
16371
|
+
if (functionIntent === "uuid" || functionIntent === "captcha") {
|
|
16372
|
+
break;
|
|
16373
|
+
}
|
|
16276
16374
|
let severity2 = "medium";
|
|
16277
16375
|
let confidence2 = "medium";
|
|
16278
16376
|
let explanation = "";
|
|
@@ -16284,15 +16382,27 @@ var require_dangerous_functions = __commonJS({
|
|
|
16284
16382
|
explanation = " (test data generation)";
|
|
16285
16383
|
description = "Math.random() used in test context for generating mock data. Not security-critical, but consider crypto.randomUUID() for better uniqueness in tests.";
|
|
16286
16384
|
suggestedFix = "Consider crypto.randomUUID() for test data uniqueness, though Math.random() is acceptable in tests";
|
|
16287
|
-
} else if (
|
|
16385
|
+
} else if (functionIntent === "demo") {
|
|
16386
|
+
severity2 = "info";
|
|
16387
|
+
confidence2 = "low";
|
|
16388
|
+
explanation = " (seed/demo data generation)";
|
|
16389
|
+
description = "Math.random() used for generating fixture/seed data. Not security-critical in development contexts.";
|
|
16390
|
+
suggestedFix = "Acceptable for seed data. Use crypto.randomUUID() if uniqueness guarantees needed.";
|
|
16391
|
+
} else if (nameRisk === "high" || context.inSecurityContext || functionIntent === "security") {
|
|
16288
16392
|
severity2 = "high";
|
|
16289
16393
|
confidence2 = "high";
|
|
16290
16394
|
explanation = " (security-sensitive context)";
|
|
16291
16395
|
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
16396
|
suggestedFix = "Replace with crypto.randomBytes() or crypto.randomUUID() for security-sensitive operations";
|
|
16293
|
-
} else if (
|
|
16397
|
+
} else if (toStringPattern.intent === "short-ui-id") {
|
|
16398
|
+
severity2 = "info";
|
|
16399
|
+
confidence2 = "low";
|
|
16400
|
+
explanation = " (UI correlation ID)";
|
|
16401
|
+
description = "Math.random() used for short UI correlation IDs. Not security-critical, but collisions possible in high-volume scenarios.";
|
|
16402
|
+
suggestedFix = "For UI correlation, crypto.randomUUID() provides better uniqueness guarantees";
|
|
16403
|
+
} else if (nameRisk === "low" || context.inBusinessLogicContext || toStringPattern.intent === "business-id") {
|
|
16294
16404
|
severity2 = "low";
|
|
16295
|
-
confidence2 = "
|
|
16405
|
+
confidence2 = "low";
|
|
16296
16406
|
explanation = " (business identifier)";
|
|
16297
16407
|
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
16408
|
suggestedFix = "Consider crypto.randomUUID() for better uniqueness guarantees and collision resistance";
|
|
@@ -36950,6 +37060,35 @@ Distinguish these patterns:
|
|
|
36950
37060
|
- Cross-tenant storage: medium (data isolation risk)
|
|
36951
37061
|
- Do NOT describe transient BYOK keys as "stored without encryption" - they are NOT stored
|
|
36952
37062
|
|
|
37063
|
+
**Math.random() for Security:**
|
|
37064
|
+
Distinguish legitimate uses from security-critical misuse:
|
|
37065
|
+
- **Seed/Data Generation Files**: Files in /seed/, /fixtures/, /factories/, datacreator.ts, *.fixture.* are for test data generation
|
|
37066
|
+
- Math.random() in seed files is acceptable - these are never production security code
|
|
37067
|
+
- REJECT findings from seed/data generation files entirely
|
|
37068
|
+
- **Educational Vulnerability Files**: Files named insecurity.ts, vulnerable.ts, or in /intentionally-vulnerable/ paths
|
|
37069
|
+
- These are OWASP Juice Shop challenges or security training examples
|
|
37070
|
+
- REJECT entirely - they're intentionally vulnerable for educational purposes
|
|
37071
|
+
- **UUID/Identifier Generation**: Functions named generateUUID(), createId(), correlationId(), etc.
|
|
37072
|
+
- Use Math.random() for UI correlation, React keys, element IDs
|
|
37073
|
+
- Short toString(36).substring(2, 9) patterns are for UI correlation, NOT security tokens
|
|
37074
|
+
- REJECT unless function name explicitly indicates security (generateToken, createSessionId, generateSecret)
|
|
37075
|
+
- **CAPTCHA/Puzzle Generation**: Math.random() for CAPTCHA questions, puzzle difficulty, game mechanics
|
|
37076
|
+
- These don't need cryptographic randomness - legitimate non-security use
|
|
37077
|
+
- REJECT findings in CAPTCHA/puzzle generation functions
|
|
37078
|
+
- **Security-Sensitive Context**: Only keep as HIGH/CRITICAL when:
|
|
37079
|
+
- Variable names indicate security: token, secret, key, auth, session, password
|
|
37080
|
+
- Function names indicate security: generateToken, createSession, makeSecret
|
|
37081
|
+
- Used in security-critical files: auth.ts, crypto.ts, session.ts
|
|
37082
|
+
- Long toString() patterns without truncation (potential token generation)
|
|
37083
|
+
|
|
37084
|
+
**Severity Ladder for Math.random():**
|
|
37085
|
+
- Seed/educational files: REJECT (not production code)
|
|
37086
|
+
- UUID/CAPTCHA functions: REJECT (legitimate use)
|
|
37087
|
+
- Short UI IDs (toString(36).substring(2, 9)): INFO (UI correlation, suggest crypto.randomUUID())
|
|
37088
|
+
- Business IDs: LOW (suggest crypto.randomUUID() for collision resistance)
|
|
37089
|
+
- Security contexts (tokens/secrets/keys): HIGH (cryptographic weakness)
|
|
37090
|
+
- Unknown context: MEDIUM (needs manual review)
|
|
37091
|
+
|
|
36953
37092
|
### 3.6 DOM Sinks and Bootstrap Scripts
|
|
36954
37093
|
Recognise LOW-RISK patterns:
|
|
36955
37094
|
- Static scripts reading localStorage for theme/preferences
|
|
@@ -37130,19 +37269,23 @@ AI-generated structured outputs need validation before use in security-sensitive
|
|
|
37130
37269
|
- Generic success messages
|
|
37131
37270
|
- Placeholder comments in non-security code
|
|
37132
37271
|
|
|
37133
|
-
## Response Format
|
|
37272
|
+
## Response Format (OPTIMIZED FOR MINIMAL OUTPUT)
|
|
37134
37273
|
|
|
37135
37274
|
For each candidate finding, return:
|
|
37136
37275
|
\`\`\`json
|
|
37137
37276
|
{
|
|
37138
37277
|
"index": <number>,
|
|
37139
37278
|
"keep": true | false,
|
|
37140
|
-
"
|
|
37141
|
-
"
|
|
37142
|
-
"validationNotes": "<optional: additional context for the developer>"
|
|
37279
|
+
"adjustedSeverity": "critical" | "high" | "medium" | "low" | "info" | null, // Only if keep=true
|
|
37280
|
+
"notes": "<concise context for developer>" // Only if keep=true, 1-2 sentences max
|
|
37143
37281
|
}
|
|
37144
37282
|
\`\`\`
|
|
37145
37283
|
|
|
37284
|
+
**CRITICAL**: To minimize costs:
|
|
37285
|
+
- For \`keep: false\` (rejected): ONLY include \`index\` and \`keep\` fields. NO explanation needed.
|
|
37286
|
+
- For \`keep: true\` (accepted): Include \`notes\` field with brief context (10-30 words). Be concise.
|
|
37287
|
+
- Omit \`adjustedSeverity\` if keeping original severity (null is wasteful).
|
|
37288
|
+
|
|
37146
37289
|
## Severity Guidelines
|
|
37147
37290
|
- **critical/high**: Realistically exploitable, should block deploys - ONLY for clear vulnerabilities
|
|
37148
37291
|
- **medium/low**: Important but non-blocking, hardening opportunities - use sparingly
|
|
@@ -37165,7 +37308,35 @@ For each candidate finding, return:
|
|
|
37165
37308
|
- No visible mitigating factors in context
|
|
37166
37309
|
- Real-world attack scenario is plausible
|
|
37167
37310
|
|
|
37168
|
-
**REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT
|
|
37311
|
+
**REMEMBER**: You are the last line of defense against noise. A finding that reaches the user should be CLEARLY worth their time. When in doubt, REJECT.
|
|
37312
|
+
|
|
37313
|
+
## Response Format
|
|
37314
|
+
|
|
37315
|
+
For EACH file, provide a JSON object with the file path and validation results.
|
|
37316
|
+
Return a JSON array where each element has:
|
|
37317
|
+
- "file": the file path (e.g., "src/routes/api.ts")
|
|
37318
|
+
- "validations": array of validation results for that file's candidates
|
|
37319
|
+
|
|
37320
|
+
Example response format (OPTIMIZED):
|
|
37321
|
+
\`\`\`json
|
|
37322
|
+
[
|
|
37323
|
+
{
|
|
37324
|
+
"file": "src/auth.ts",
|
|
37325
|
+
"validations": [
|
|
37326
|
+
{ "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
|
|
37327
|
+
{ "index": 1, "keep": false }
|
|
37328
|
+
]
|
|
37329
|
+
},
|
|
37330
|
+
{
|
|
37331
|
+
"file": "src/api.ts",
|
|
37332
|
+
"validations": [
|
|
37333
|
+
{ "index": 0, "keep": true, "notes": "User input flows to SQL query" }
|
|
37334
|
+
]
|
|
37335
|
+
}
|
|
37336
|
+
]
|
|
37337
|
+
\`\`\`
|
|
37338
|
+
|
|
37339
|
+
**REMEMBER**: Rejected findings (keep: false) need NO explanation. Keep notes brief (10-30 words).`;
|
|
37169
37340
|
var cachedProjectContext = null;
|
|
37170
37341
|
async function makeAnthropicRequestWithRetry(requestFn, maxRetries = 3, initialDelayMs = 1e3) {
|
|
37171
37342
|
let lastError = null;
|
|
@@ -37279,7 +37450,45 @@ For each candidate finding, return:
|
|
|
37279
37450
|
{ role: "system", content: HIGH_CONTEXT_VALIDATION_PROMPT },
|
|
37280
37451
|
{ role: "user", content: validationRequest }
|
|
37281
37452
|
],
|
|
37282
|
-
max_completion_tokens:
|
|
37453
|
+
max_completion_tokens: 1500,
|
|
37454
|
+
// Reduced from 4096 - optimized format needs less output
|
|
37455
|
+
response_format: {
|
|
37456
|
+
type: "json_schema",
|
|
37457
|
+
json_schema: {
|
|
37458
|
+
name: "validation_response",
|
|
37459
|
+
strict: true,
|
|
37460
|
+
schema: {
|
|
37461
|
+
type: "object",
|
|
37462
|
+
properties: {
|
|
37463
|
+
validations: {
|
|
37464
|
+
type: "array",
|
|
37465
|
+
items: {
|
|
37466
|
+
type: "object",
|
|
37467
|
+
properties: {
|
|
37468
|
+
file: { type: "string" },
|
|
37469
|
+
validations: {
|
|
37470
|
+
type: "array",
|
|
37471
|
+
items: {
|
|
37472
|
+
type: "object",
|
|
37473
|
+
properties: {
|
|
37474
|
+
index: { type: "number" },
|
|
37475
|
+
keep: { type: "boolean" }
|
|
37476
|
+
},
|
|
37477
|
+
required: ["index", "keep"],
|
|
37478
|
+
additionalProperties: true
|
|
37479
|
+
}
|
|
37480
|
+
}
|
|
37481
|
+
},
|
|
37482
|
+
required: ["file", "validations"],
|
|
37483
|
+
additionalProperties: false
|
|
37484
|
+
}
|
|
37485
|
+
}
|
|
37486
|
+
},
|
|
37487
|
+
required: ["validations"],
|
|
37488
|
+
additionalProperties: false
|
|
37489
|
+
}
|
|
37490
|
+
}
|
|
37491
|
+
}
|
|
37283
37492
|
}));
|
|
37284
37493
|
statsLock.apiCalls++;
|
|
37285
37494
|
const usage2 = response.usage;
|
|
@@ -37311,8 +37520,18 @@ For each candidate finding, return:
|
|
|
37311
37520
|
}
|
|
37312
37521
|
return batchFindings;
|
|
37313
37522
|
}
|
|
37523
|
+
let parsedContent;
|
|
37524
|
+
try {
|
|
37525
|
+
parsedContent = JSON.parse(content);
|
|
37526
|
+
if (parsedContent.validations && Array.isArray(parsedContent.validations)) {
|
|
37527
|
+
parsedContent = parsedContent.validations;
|
|
37528
|
+
}
|
|
37529
|
+
} catch (e2) {
|
|
37530
|
+
console.warn("[OpenAI] Failed to parse JSON response:", e2);
|
|
37531
|
+
parsedContent = content;
|
|
37532
|
+
}
|
|
37314
37533
|
const expectedFiles = fileDataList.map(({ filePath }) => filePath);
|
|
37315
|
-
const validationResultsMap = parseMultiFileValidationResponse(
|
|
37534
|
+
const validationResultsMap = parseMultiFileValidationResponse(typeof parsedContent === "string" ? parsedContent : JSON.stringify(parsedContent), expectedFiles);
|
|
37316
37535
|
for (const { filePath, findings: fileFindings } of fileDataList) {
|
|
37317
37536
|
const fileResults = validationResultsMap.get(filePath);
|
|
37318
37537
|
if (!fileResults || fileResults.length === 0) {
|
|
@@ -37501,8 +37720,8 @@ For each candidate finding, return:
|
|
|
37501
37720
|
const validationRequest = buildMultiFileValidationRequest(fileDataList.map(({ file, findings: findings2 }) => ({ file, findings: findings2 })), context);
|
|
37502
37721
|
const response = await makeAnthropicRequestWithRetry(() => client.messages.create({
|
|
37503
37722
|
model: "claude-3-5-haiku-20241022",
|
|
37504
|
-
max_tokens:
|
|
37505
|
-
//
|
|
37723
|
+
max_tokens: 1500,
|
|
37724
|
+
// Reduced from 4096 - optimized format needs less output
|
|
37506
37725
|
system: [
|
|
37507
37726
|
{
|
|
37508
37727
|
type: "text",
|
|
@@ -37696,14 +37915,14 @@ Example response format:
|
|
|
37696
37915
|
{
|
|
37697
37916
|
"file": "src/auth.ts",
|
|
37698
37917
|
"validations": [
|
|
37699
|
-
{ "index": 0, "keep": true, "
|
|
37700
|
-
{ "index": 1, "keep": false
|
|
37918
|
+
{ "index": 0, "keep": true, "adjustedSeverity": "medium", "notes": "Protected by middleware" },
|
|
37919
|
+
{ "index": 1, "keep": false }
|
|
37701
37920
|
]
|
|
37702
37921
|
},
|
|
37703
37922
|
{
|
|
37704
37923
|
"file": "src/api.ts",
|
|
37705
37924
|
"validations": [
|
|
37706
|
-
{ "index": 0, "keep": true, "
|
|
37925
|
+
{ "index": 0, "keep": true, "notes": "User input flows to SQL query" }
|
|
37707
37926
|
]
|
|
37708
37927
|
}
|
|
37709
37928
|
]
|
|
@@ -37771,13 +37990,18 @@ Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context
|
|
|
37771
37990
|
continue;
|
|
37772
37991
|
}
|
|
37773
37992
|
const filePath = fileResult.file;
|
|
37774
|
-
const validations = fileResult.validations.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) =>
|
|
37775
|
-
|
|
37776
|
-
|
|
37777
|
-
|
|
37778
|
-
|
|
37779
|
-
|
|
37780
|
-
|
|
37993
|
+
const validations = fileResult.validations.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) => {
|
|
37994
|
+
const notes = item.notes || item.validationNotes || item.reason || void 0;
|
|
37995
|
+
return {
|
|
37996
|
+
index: item.index,
|
|
37997
|
+
keep: item.keep,
|
|
37998
|
+
notes,
|
|
37999
|
+
adjustedSeverity: item.adjustedSeverity || null,
|
|
38000
|
+
// Keep legacy fields for backward compatibility
|
|
38001
|
+
reason: item.reason,
|
|
38002
|
+
validationNotes: item.validationNotes
|
|
38003
|
+
};
|
|
38004
|
+
});
|
|
37781
38005
|
resultMap.set(filePath, validations);
|
|
37782
38006
|
}
|
|
37783
38007
|
for (const expectedFile of expectedFiles) {
|
|
@@ -37810,18 +38034,19 @@ Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context
|
|
|
37810
38034
|
validatedByAI: true,
|
|
37811
38035
|
confidence: "high"
|
|
37812
38036
|
};
|
|
38037
|
+
const validationNotes = validation.notes || validation.validationNotes || validation.reason || void 0;
|
|
37813
38038
|
if (validation.adjustedSeverity && validation.adjustedSeverity !== finding.severity) {
|
|
37814
38039
|
adjustedFinding.originalSeverity = finding.severity;
|
|
37815
38040
|
adjustedFinding.severity = validation.adjustedSeverity;
|
|
37816
38041
|
adjustedFinding.validationStatus = "downgraded";
|
|
37817
|
-
adjustedFinding.validationNotes =
|
|
38042
|
+
adjustedFinding.validationNotes = validationNotes || "Severity adjusted by AI validation";
|
|
37818
38043
|
} else {
|
|
37819
38044
|
adjustedFinding.validationStatus = "confirmed";
|
|
37820
|
-
adjustedFinding.validationNotes =
|
|
38045
|
+
adjustedFinding.validationNotes = validationNotes;
|
|
37821
38046
|
}
|
|
37822
38047
|
processed.push(adjustedFinding);
|
|
37823
38048
|
} else {
|
|
37824
|
-
console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber}
|
|
38049
|
+
console.log(`[AI Validation] Rejected: ${finding.title} at ${finding.filePath}:${finding.lineNumber}`);
|
|
37825
38050
|
}
|
|
37826
38051
|
}
|
|
37827
38052
|
return processed;
|
|
@@ -37894,13 +38119,18 @@ Remember: Be AGGRESSIVE in rejecting false positives. Use the full file context
|
|
|
37894
38119
|
const parsed = JSON.parse(jsonSlice);
|
|
37895
38120
|
if (!Array.isArray(parsed))
|
|
37896
38121
|
return [];
|
|
37897
|
-
return parsed.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) =>
|
|
37898
|
-
|
|
37899
|
-
|
|
37900
|
-
|
|
37901
|
-
|
|
37902
|
-
|
|
37903
|
-
|
|
38122
|
+
return parsed.filter((item) => typeof item.index === "number" && typeof item.keep === "boolean").map((item) => {
|
|
38123
|
+
const notes = item.notes || item.validationNotes || item.reason || void 0;
|
|
38124
|
+
return {
|
|
38125
|
+
index: item.index,
|
|
38126
|
+
keep: item.keep,
|
|
38127
|
+
notes,
|
|
38128
|
+
adjustedSeverity: item.adjustedSeverity || null,
|
|
38129
|
+
// Keep legacy fields for backward compatibility
|
|
38130
|
+
reason: item.reason,
|
|
38131
|
+
validationNotes: item.validationNotes
|
|
38132
|
+
};
|
|
38133
|
+
});
|
|
37904
38134
|
} catch (error) {
|
|
37905
38135
|
console.error("Failed to parse validation response:", error);
|
|
37906
38136
|
return [];
|
|
@@ -43239,8 +43469,11 @@ function enhanceAPIError(error) {
|
|
|
43239
43469
|
case 401:
|
|
43240
43470
|
return {
|
|
43241
43471
|
message: "Authentication required",
|
|
43242
|
-
suggestion: "
|
|
43472
|
+
suggestion: "You need to login to use AI-powered scans.",
|
|
43243
43473
|
category: "auth",
|
|
43474
|
+
errorCode: "OCU-E401",
|
|
43475
|
+
quickFix: "oculum login",
|
|
43476
|
+
learnMoreUrl: "https://oculum.dev/docs/authentication",
|
|
43244
43477
|
recoveryActions: [
|
|
43245
43478
|
{ label: "Login", command: "oculum login", action: "login" },
|
|
43246
43479
|
{ label: "Use free scan", action: "fallback" }
|
|
@@ -43250,8 +43483,11 @@ function enhanceAPIError(error) {
|
|
|
43250
43483
|
if (error.reason === "insufficient_tier") {
|
|
43251
43484
|
return {
|
|
43252
43485
|
message: "This feature requires a Pro subscription",
|
|
43253
|
-
suggestion: "
|
|
43486
|
+
suggestion: "Validated and deep scans require a Pro plan.",
|
|
43254
43487
|
category: "auth",
|
|
43488
|
+
errorCode: "OCU-E403-TIER",
|
|
43489
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43490
|
+
learnMoreUrl: "https://oculum.dev/billing",
|
|
43255
43491
|
recoveryActions: [
|
|
43256
43492
|
{ label: "View pricing", action: "upgrade" },
|
|
43257
43493
|
{ label: "Use free scan", action: "fallback" }
|
|
@@ -43261,8 +43497,11 @@ function enhanceAPIError(error) {
|
|
|
43261
43497
|
if (error.reason === "expired") {
|
|
43262
43498
|
return {
|
|
43263
43499
|
message: "Your API key has expired",
|
|
43264
|
-
suggestion: "Generate a new key
|
|
43500
|
+
suggestion: "Generate a new key to continue using AI-powered scans.",
|
|
43265
43501
|
category: "auth",
|
|
43502
|
+
errorCode: "OCU-E403-EXP",
|
|
43503
|
+
quickFix: "oculum login",
|
|
43504
|
+
learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
|
|
43266
43505
|
recoveryActions: [
|
|
43267
43506
|
{ label: "Login again", command: "oculum login", action: "login" }
|
|
43268
43507
|
]
|
|
@@ -43271,8 +43510,11 @@ function enhanceAPIError(error) {
|
|
|
43271
43510
|
if (error.reason === "invalid_key") {
|
|
43272
43511
|
return {
|
|
43273
43512
|
message: "Invalid API key",
|
|
43274
|
-
suggestion: "Your API key is not recognized.
|
|
43513
|
+
suggestion: "Your API key is not recognized.",
|
|
43275
43514
|
category: "auth",
|
|
43515
|
+
errorCode: "OCU-E403-KEY",
|
|
43516
|
+
quickFix: "oculum login",
|
|
43517
|
+
learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
|
|
43276
43518
|
recoveryActions: [
|
|
43277
43519
|
{ label: "Login", command: "oculum login", action: "login" },
|
|
43278
43520
|
{ label: "Use free scan", action: "fallback" }
|
|
@@ -43281,19 +43523,24 @@ function enhanceAPIError(error) {
|
|
|
43281
43523
|
}
|
|
43282
43524
|
return {
|
|
43283
43525
|
message: "Access denied",
|
|
43284
|
-
suggestion: "Check your API key permissions
|
|
43526
|
+
suggestion: "Check your API key permissions.",
|
|
43285
43527
|
category: "auth",
|
|
43528
|
+
errorCode: "OCU-E403",
|
|
43529
|
+
quickFix: "oculum login",
|
|
43286
43530
|
recoveryActions: [
|
|
43287
43531
|
{ label: "Login", command: "oculum login", action: "login" }
|
|
43288
43532
|
]
|
|
43289
43533
|
};
|
|
43290
43534
|
case 429:
|
|
43291
43535
|
const rateLimitInfo = error.reason === "quota_exceeded" ? "You've reached your monthly scan quota." : "Too many requests in a short period.";
|
|
43292
|
-
const rateLimitSuggestion = error.reason === "quota_exceeded" ? "Your quota resets at the start of next month.
|
|
43536
|
+
const rateLimitSuggestion = error.reason === "quota_exceeded" ? "Your quota resets at the start of next month." : "Wait a moment before trying again.";
|
|
43293
43537
|
return {
|
|
43294
43538
|
message: rateLimitInfo,
|
|
43295
43539
|
suggestion: rateLimitSuggestion,
|
|
43296
43540
|
category: "server",
|
|
43541
|
+
errorCode: error.reason === "quota_exceeded" ? "OCU-E429-QUOTA" : "OCU-E429-RATE",
|
|
43542
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43543
|
+
learnMoreUrl: "https://oculum.dev/dashboard/usage",
|
|
43297
43544
|
recoveryActions: [
|
|
43298
43545
|
{ label: "Use free local scan", command: "oculum scan . --depth cheap", action: "fallback" },
|
|
43299
43546
|
{ label: "View usage & upgrade", action: "upgrade" },
|
|
@@ -43306,8 +43553,11 @@ function enhanceAPIError(error) {
|
|
|
43306
43553
|
case 503:
|
|
43307
43554
|
return {
|
|
43308
43555
|
message: "Oculum servers are experiencing issues",
|
|
43309
|
-
suggestion: "Try again in a few minutes.
|
|
43556
|
+
suggestion: "This is temporary. Try again in a few minutes.",
|
|
43310
43557
|
category: "server",
|
|
43558
|
+
errorCode: `OCU-E${error.statusCode}`,
|
|
43559
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43560
|
+
learnMoreUrl: "https://status.oculum.dev",
|
|
43311
43561
|
recoveryActions: [
|
|
43312
43562
|
{ label: "Retry", action: "retry" },
|
|
43313
43563
|
{ label: "Use free scan (offline)", action: "fallback" }
|
|
@@ -43316,15 +43566,22 @@ function enhanceAPIError(error) {
|
|
|
43316
43566
|
case 504:
|
|
43317
43567
|
return {
|
|
43318
43568
|
message: "Request timed out",
|
|
43319
|
-
suggestion: "The scan may be too large
|
|
43569
|
+
suggestion: "The scan may be too large for the server.",
|
|
43320
43570
|
category: "server",
|
|
43571
|
+
errorCode: "OCU-E504",
|
|
43572
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43573
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#timeout",
|
|
43321
43574
|
recoveryActions: [
|
|
43322
43575
|
{ label: "Retry", action: "retry" },
|
|
43323
43576
|
{ label: "Use quick scan", action: "fallback" }
|
|
43324
43577
|
]
|
|
43325
43578
|
};
|
|
43326
43579
|
default:
|
|
43327
|
-
return {
|
|
43580
|
+
return {
|
|
43581
|
+
message: error.message,
|
|
43582
|
+
category: "unknown",
|
|
43583
|
+
errorCode: "OCU-E000"
|
|
43584
|
+
};
|
|
43328
43585
|
}
|
|
43329
43586
|
}
|
|
43330
43587
|
function detectNetworkErrorType(msg) {
|
|
@@ -43372,6 +43629,7 @@ function enhanceStandardError(error) {
|
|
|
43372
43629
|
message: `Path not found: ${path2}`,
|
|
43373
43630
|
suggestion: "Check that the path exists and is spelled correctly.",
|
|
43374
43631
|
category: "file",
|
|
43632
|
+
errorCode: "OCU-E001",
|
|
43375
43633
|
details: "The file or directory does not exist at the specified location."
|
|
43376
43634
|
};
|
|
43377
43635
|
}
|
|
@@ -43380,15 +43638,33 @@ function enhanceStandardError(error) {
|
|
|
43380
43638
|
message: "Permission denied",
|
|
43381
43639
|
suggestion: "Check file permissions or try running with appropriate access.",
|
|
43382
43640
|
category: "file",
|
|
43641
|
+
errorCode: "OCU-E002",
|
|
43383
43642
|
details: "You do not have permission to access this file or directory."
|
|
43384
43643
|
};
|
|
43385
43644
|
}
|
|
43645
|
+
if (msg.includes("self-signed") || msg.includes("unable to verify") || msg.includes("certificate") && (msg.includes("invalid") || msg.includes("expired"))) {
|
|
43646
|
+
return {
|
|
43647
|
+
message: "SSL certificate error",
|
|
43648
|
+
suggestion: "Your network may be intercepting HTTPS traffic (corporate proxy).",
|
|
43649
|
+
category: "network",
|
|
43650
|
+
errorCode: "OCU-E003",
|
|
43651
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43652
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#ssl",
|
|
43653
|
+
recoveryActions: [
|
|
43654
|
+
{ label: "Use offline scan", command: "oculum scan . --depth cheap", action: "fallback" },
|
|
43655
|
+
{ label: "View help", action: "help" }
|
|
43656
|
+
]
|
|
43657
|
+
};
|
|
43658
|
+
}
|
|
43386
43659
|
if (msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("etimedout") || msg.includes("econnreset") || msg.includes("socket")) {
|
|
43387
43660
|
const { type, suggestion } = detectNetworkErrorType(msg);
|
|
43388
43661
|
return {
|
|
43389
43662
|
message: type,
|
|
43390
43663
|
suggestion,
|
|
43391
43664
|
category: "network",
|
|
43665
|
+
errorCode: "OCU-E004",
|
|
43666
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43667
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#network",
|
|
43392
43668
|
recoveryActions: [
|
|
43393
43669
|
{ label: "Retry", action: "retry" },
|
|
43394
43670
|
{ label: "Use offline scan", action: "fallback" }
|
|
@@ -43400,52 +43676,105 @@ function enhanceStandardError(error) {
|
|
|
43400
43676
|
message: "Invalid configuration file",
|
|
43401
43677
|
suggestion: "Check your config file for valid JSON syntax.",
|
|
43402
43678
|
category: "file",
|
|
43679
|
+
errorCode: "OCU-E005",
|
|
43680
|
+
learnMoreUrl: "https://oculum.dev/docs/configuration",
|
|
43403
43681
|
details: "The configuration file contains invalid JSON. Use a JSON validator to find the error."
|
|
43404
43682
|
};
|
|
43405
43683
|
}
|
|
43406
43684
|
if (msg.includes("no scannable files")) {
|
|
43407
43685
|
return {
|
|
43408
43686
|
message: "No scannable files found",
|
|
43409
|
-
suggestion: "Check that the path contains supported file types
|
|
43687
|
+
suggestion: "Check that the path contains supported file types.",
|
|
43410
43688
|
category: "scan",
|
|
43411
|
-
|
|
43689
|
+
errorCode: "OCU-E006",
|
|
43690
|
+
details: "Supported: .js, .jsx, .ts, .tsx, .py, .go, .java, .rb, .php, .yaml, .json"
|
|
43412
43691
|
};
|
|
43413
43692
|
}
|
|
43414
43693
|
if (msg.includes("symlink") || msg.includes("eloop")) {
|
|
43415
43694
|
return {
|
|
43416
43695
|
message: "Symbolic link error",
|
|
43417
43696
|
suggestion: "There may be a circular symlink. Try scanning a specific directory instead.",
|
|
43418
|
-
category: "file"
|
|
43697
|
+
category: "file",
|
|
43698
|
+
errorCode: "OCU-E007"
|
|
43419
43699
|
};
|
|
43420
43700
|
}
|
|
43421
43701
|
if (msg.includes("heap") || msg.includes("memory") || msg.includes("enomem")) {
|
|
43422
43702
|
return {
|
|
43423
43703
|
message: "Out of memory",
|
|
43424
|
-
suggestion: "The scan ran out of memory. Try scanning fewer files
|
|
43425
|
-
category: "scan"
|
|
43704
|
+
suggestion: "The scan ran out of memory. Try scanning fewer files.",
|
|
43705
|
+
category: "scan",
|
|
43706
|
+
errorCode: "OCU-E008",
|
|
43707
|
+
quickFix: 'NODE_OPTIONS="--max-old-space-size=4096" oculum scan .',
|
|
43708
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#memory"
|
|
43426
43709
|
};
|
|
43427
43710
|
}
|
|
43428
|
-
|
|
43711
|
+
if (msg.includes("enospc") || msg.includes("no space left")) {
|
|
43712
|
+
return {
|
|
43713
|
+
message: "Disk full",
|
|
43714
|
+
suggestion: "Free up disk space and try again.",
|
|
43715
|
+
category: "file",
|
|
43716
|
+
errorCode: "OCU-E009"
|
|
43717
|
+
};
|
|
43718
|
+
}
|
|
43719
|
+
return {
|
|
43720
|
+
message: error.message,
|
|
43721
|
+
category: "unknown",
|
|
43722
|
+
errorCode: "OCU-E000",
|
|
43723
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting"
|
|
43724
|
+
};
|
|
43429
43725
|
}
|
|
43430
43726
|
function formatError(enhanced) {
|
|
43431
|
-
|
|
43727
|
+
const icon = getErrorIcon(enhanced.category);
|
|
43728
|
+
const lines = [];
|
|
43729
|
+
if (enhanced.errorCode) {
|
|
43730
|
+
lines.push(source_default.red.bold(`${icon} ${enhanced.errorCode} ${enhanced.message}`));
|
|
43731
|
+
} else {
|
|
43732
|
+
lines.push(source_default.red.bold(`${icon} ${enhanced.message}`));
|
|
43733
|
+
}
|
|
43734
|
+
lines.push("");
|
|
43432
43735
|
if (enhanced.suggestion) {
|
|
43433
|
-
|
|
43736
|
+
lines.push(source_default.white(enhanced.suggestion));
|
|
43434
43737
|
}
|
|
43435
43738
|
if (enhanced.details) {
|
|
43436
|
-
|
|
43739
|
+
lines.push(source_default.dim(enhanced.details));
|
|
43740
|
+
}
|
|
43741
|
+
if (enhanced.quickFix) {
|
|
43742
|
+
lines.push("");
|
|
43743
|
+
lines.push(source_default.bold("Quick fix:"));
|
|
43744
|
+
lines.push(source_default.cyan(` $ ${enhanced.quickFix}`));
|
|
43437
43745
|
}
|
|
43438
43746
|
if (enhanced.recoveryActions && enhanced.recoveryActions.length > 0) {
|
|
43439
|
-
|
|
43747
|
+
lines.push("");
|
|
43748
|
+
lines.push(source_default.dim("Or try:"));
|
|
43440
43749
|
for (const action of enhanced.recoveryActions) {
|
|
43441
43750
|
if (action.command) {
|
|
43442
|
-
|
|
43751
|
+
lines.push(source_default.dim(" - ") + source_default.white(action.label + ": ") + source_default.cyan(action.command));
|
|
43443
43752
|
} else {
|
|
43444
|
-
|
|
43753
|
+
lines.push(source_default.dim(" - ") + source_default.white(action.label));
|
|
43445
43754
|
}
|
|
43446
43755
|
}
|
|
43447
43756
|
}
|
|
43448
|
-
|
|
43757
|
+
if (enhanced.learnMoreUrl) {
|
|
43758
|
+
lines.push("");
|
|
43759
|
+
lines.push(source_default.dim("Need help? ") + source_default.cyan.underline(enhanced.learnMoreUrl));
|
|
43760
|
+
}
|
|
43761
|
+
return lines.join("\n");
|
|
43762
|
+
}
|
|
43763
|
+
function getErrorIcon(category) {
|
|
43764
|
+
switch (category) {
|
|
43765
|
+
case "network":
|
|
43766
|
+
return "\u{1F310}";
|
|
43767
|
+
case "auth":
|
|
43768
|
+
return "\u{1F510}";
|
|
43769
|
+
case "file":
|
|
43770
|
+
return "\u{1F4C1}";
|
|
43771
|
+
case "scan":
|
|
43772
|
+
return "\u{1F50D}";
|
|
43773
|
+
case "server":
|
|
43774
|
+
return "\u{1F5A5}\uFE0F";
|
|
43775
|
+
default:
|
|
43776
|
+
return "\u274C";
|
|
43777
|
+
}
|
|
43449
43778
|
}
|
|
43450
43779
|
|
|
43451
43780
|
// src/utils/project-config.ts
|
|
@@ -43946,7 +44275,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
43946
44275
|
}
|
|
43947
44276
|
try {
|
|
43948
44277
|
spinner.start("Starting scan...");
|
|
43949
|
-
const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
|
|
44278
|
+
const hasLocalAI = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
43950
44279
|
if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
|
|
43951
44280
|
spinner.text = `Backend ${options.depth} scan analyzing ${files.length} files...`;
|
|
43952
44281
|
result = await callBackendAPI(
|
|
@@ -44066,26 +44395,37 @@ async function runScan(targetPath, cliOptions) {
|
|
|
44066
44395
|
}
|
|
44067
44396
|
}
|
|
44068
44397
|
var scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: cheap (free), validated, deep", "cheap").option("-m, --mode <mode>", "alias for --depth (quick, validated, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown", "terminal").option("--fail-on <severity>", "exit with error code if findings at severity", "high").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show detailed scanner logs (debug mode)").option("-o, --output <file>", "write output to file").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").addHelpText("after", `
|
|
44398
|
+
Scan Modes:
|
|
44399
|
+
cheap Free Fast pattern matching, runs locally
|
|
44400
|
+
Best for: Quick checks, CI/CD pipelines
|
|
44401
|
+
|
|
44402
|
+
validated ~$0.03 AI validates findings, ~70% fewer false positives
|
|
44403
|
+
Best for: Pre-commit checks, PR reviews
|
|
44404
|
+
|
|
44405
|
+
deep ~$0.10 Full semantic analysis with AI reasoning
|
|
44406
|
+
Best for: Security audits, release checks
|
|
44407
|
+
|
|
44069
44408
|
Examples:
|
|
44070
|
-
$ oculum scan .
|
|
44071
|
-
$ oculum scan
|
|
44072
|
-
$ oculum scan
|
|
44073
|
-
$ oculum scan . -
|
|
44074
|
-
$ oculum scan . --incremental
|
|
44075
|
-
$ oculum scan . --
|
|
44076
|
-
$ oculum scan .
|
|
44077
|
-
|
|
44078
|
-
|
|
44079
|
-
|
|
44080
|
-
|
|
44081
|
-
|
|
44082
|
-
|
|
44083
|
-
|
|
44084
|
-
|
|
44085
|
-
|
|
44086
|
-
|
|
44087
|
-
|
|
44088
|
-
|
|
44409
|
+
$ oculum scan . Scan current directory (free)
|
|
44410
|
+
$ oculum scan . -d validated AI-validated scan
|
|
44411
|
+
$ oculum scan ./src --fail-on high Fail CI on high severity findings
|
|
44412
|
+
$ oculum scan . -f sarif -o report Export for GitHub Code Scanning
|
|
44413
|
+
$ oculum scan . --incremental Only scan changed files (git)
|
|
44414
|
+
$ oculum scan . --diff origin/main Compare against base branch
|
|
44415
|
+
$ oculum scan . -q Quiet mode for CI/CD
|
|
44416
|
+
|
|
44417
|
+
Configuration:
|
|
44418
|
+
Create oculum.config.json in your project:
|
|
44419
|
+
{
|
|
44420
|
+
"depth": "validated",
|
|
44421
|
+
"failOn": "high",
|
|
44422
|
+
"ignore": ["**/test/**"]
|
|
44423
|
+
}
|
|
44424
|
+
|
|
44425
|
+
More Help:
|
|
44426
|
+
$ oculum help scan-modes Detailed mode comparison
|
|
44427
|
+
$ oculum help ci-setup CI/CD integration examples
|
|
44428
|
+
$ oculum help config Full configuration options
|
|
44089
44429
|
`).action(runScan);
|
|
44090
44430
|
|
|
44091
44431
|
// src/commands/auth.ts
|
|
@@ -44788,10 +45128,10 @@ var foreach = (val, fn) => {
|
|
|
44788
45128
|
fn(val);
|
|
44789
45129
|
}
|
|
44790
45130
|
};
|
|
44791
|
-
var addAndConvert = (
|
|
44792
|
-
let container =
|
|
45131
|
+
var addAndConvert = (main3, prop, item) => {
|
|
45132
|
+
let container = main3[prop];
|
|
44793
45133
|
if (!(container instanceof Set)) {
|
|
44794
|
-
|
|
45134
|
+
main3[prop] = container = /* @__PURE__ */ new Set([container]);
|
|
44795
45135
|
}
|
|
44796
45136
|
container.add(item);
|
|
44797
45137
|
};
|
|
@@ -44803,12 +45143,12 @@ var clearItem = (cont) => (key) => {
|
|
|
44803
45143
|
delete cont[key];
|
|
44804
45144
|
}
|
|
44805
45145
|
};
|
|
44806
|
-
var delFromSet = (
|
|
44807
|
-
const container =
|
|
45146
|
+
var delFromSet = (main3, prop, item) => {
|
|
45147
|
+
const container = main3[prop];
|
|
44808
45148
|
if (container instanceof Set) {
|
|
44809
45149
|
container.delete(item);
|
|
44810
45150
|
} else if (container === item) {
|
|
44811
|
-
delete
|
|
45151
|
+
delete main3[prop];
|
|
44812
45152
|
}
|
|
44813
45153
|
};
|
|
44814
45154
|
var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
|
|
@@ -46853,6 +47193,7 @@ var CONFIG_DIR3 = (0, import_path6.join)((0, import_os4.homedir)(), ".oculum");
|
|
|
46853
47193
|
var STATE_FILE = (0, import_path6.join)(CONFIG_DIR3, "state.json");
|
|
46854
47194
|
var DEFAULT_STATE = {
|
|
46855
47195
|
onboardingComplete: false,
|
|
47196
|
+
setupWizardComplete: false,
|
|
46856
47197
|
totalScans: 0,
|
|
46857
47198
|
welcomeShown: false
|
|
46858
47199
|
};
|
|
@@ -46881,7 +47222,7 @@ function updateUserState(updates) {
|
|
|
46881
47222
|
}
|
|
46882
47223
|
function isFirstTimeUser() {
|
|
46883
47224
|
const state = getUserState();
|
|
46884
|
-
return !state.onboardingComplete && state.totalScans === 0;
|
|
47225
|
+
return !state.setupWizardComplete && !state.onboardingComplete && state.totalScans === 0;
|
|
46885
47226
|
}
|
|
46886
47227
|
function shouldShowWelcome() {
|
|
46887
47228
|
const state = getUserState();
|
|
@@ -46906,8 +47247,7 @@ function recordScan() {
|
|
|
46906
47247
|
}
|
|
46907
47248
|
|
|
46908
47249
|
// src/ui/onboarding.ts
|
|
46909
|
-
|
|
46910
|
-
console.clear();
|
|
47250
|
+
function showLogo() {
|
|
46911
47251
|
console.log(source_default.cyan(`
|
|
46912
47252
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557
|
|
46913
47253
|
\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551
|
|
@@ -46916,6 +47256,10 @@ async function showWelcomeScreen() {
|
|
|
46916
47256
|
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551
|
|
46917
47257
|
\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D
|
|
46918
47258
|
`));
|
|
47259
|
+
}
|
|
47260
|
+
async function showWelcomeScreen() {
|
|
47261
|
+
console.clear();
|
|
47262
|
+
showLogo();
|
|
46919
47263
|
console.log(source_default.bold.white(" AI-Native Security Scanner for Modern Codebases\n"));
|
|
46920
47264
|
console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
46921
47265
|
console.log(source_default.white(" Oculum detects security vulnerabilities in AI-generated"));
|
|
@@ -47972,11 +48316,136 @@ async function runHistoryFlow() {
|
|
|
47972
48316
|
}
|
|
47973
48317
|
}
|
|
47974
48318
|
}
|
|
48319
|
+
function formatNumber(num) {
|
|
48320
|
+
if (num === -1) return "unlimited";
|
|
48321
|
+
return num.toLocaleString();
|
|
48322
|
+
}
|
|
48323
|
+
function createProgressBar(percentage, width = 20) {
|
|
48324
|
+
const filled = Math.round(percentage / 100 * width);
|
|
48325
|
+
const empty = width - filled;
|
|
48326
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
48327
|
+
if (percentage >= 90) return source_default.red(bar);
|
|
48328
|
+
if (percentage >= 70) return source_default.yellow(bar);
|
|
48329
|
+
return source_default.green(bar);
|
|
48330
|
+
}
|
|
48331
|
+
function formatDate(dateStr) {
|
|
48332
|
+
const date = new Date(dateStr);
|
|
48333
|
+
return date.toLocaleDateString("en-US", {
|
|
48334
|
+
month: "short",
|
|
48335
|
+
day: "numeric",
|
|
48336
|
+
year: "numeric"
|
|
48337
|
+
});
|
|
48338
|
+
}
|
|
48339
|
+
function getTimeAgo(date) {
|
|
48340
|
+
const now = /* @__PURE__ */ new Date();
|
|
48341
|
+
const diffMs = now.getTime() - date.getTime();
|
|
48342
|
+
const diffMins = Math.floor(diffMs / 6e4);
|
|
48343
|
+
const diffHours = Math.floor(diffMs / 36e5);
|
|
48344
|
+
const diffDays = Math.floor(diffMs / 864e5);
|
|
48345
|
+
if (diffMins < 1) return "just now";
|
|
48346
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
48347
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
48348
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
48349
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
48350
|
+
}
|
|
48351
|
+
async function runUsageFlow() {
|
|
48352
|
+
const config = getConfig();
|
|
48353
|
+
if (!isAuthenticated()) {
|
|
48354
|
+
console.log("");
|
|
48355
|
+
M2.warn("Not logged in");
|
|
48356
|
+
console.log("");
|
|
48357
|
+
console.log(source_default.dim(" Login to view your usage and quota:"));
|
|
48358
|
+
console.log(source_default.cyan(" oculum login"));
|
|
48359
|
+
console.log("");
|
|
48360
|
+
await ve({
|
|
48361
|
+
message: "Press Enter to continue",
|
|
48362
|
+
options: [{ value: "back", label: "\u2190 Back to menu" }]
|
|
48363
|
+
});
|
|
48364
|
+
return;
|
|
48365
|
+
}
|
|
48366
|
+
const spinner = Y2();
|
|
48367
|
+
spinner.start("Fetching usage data...");
|
|
48368
|
+
try {
|
|
48369
|
+
const result = await getUsage(config.apiKey);
|
|
48370
|
+
if (!result.success || !result.usage || !result.plan) {
|
|
48371
|
+
spinner.stop("Failed to fetch usage data");
|
|
48372
|
+
console.log("");
|
|
48373
|
+
M2.error(result.error || "Unknown error");
|
|
48374
|
+
console.log("");
|
|
48375
|
+
await ve({
|
|
48376
|
+
message: "Press Enter to continue",
|
|
48377
|
+
options: [{ value: "back", label: "\u2190 Back to menu" }]
|
|
48378
|
+
});
|
|
48379
|
+
return;
|
|
48380
|
+
}
|
|
48381
|
+
spinner.stop("Usage data loaded");
|
|
48382
|
+
const { plan, usage: usageData } = result;
|
|
48383
|
+
console.clear();
|
|
48384
|
+
console.log("");
|
|
48385
|
+
console.log(source_default.bold(" \u{1F4CA} Oculum Usage"));
|
|
48386
|
+
console.log(source_default.dim(" " + "\u2500".repeat(38)));
|
|
48387
|
+
console.log("");
|
|
48388
|
+
const planBadge = plan.name === "pro" ? source_default.bgBlue.white(" PRO ") : plan.name === "enterprise" || plan.name === "max" ? source_default.bgMagenta.white(" MAX ") : plan.name === "starter" ? source_default.bgCyan.white(" STARTER ") : source_default.bgGray.white(" FREE ");
|
|
48389
|
+
console.log(source_default.dim(" Plan: ") + planBadge + source_default.white(` ${plan.displayName}`));
|
|
48390
|
+
console.log(source_default.dim(" Month: ") + source_default.white(usageData.month));
|
|
48391
|
+
console.log("");
|
|
48392
|
+
console.log(source_default.bold(" Credits Usage"));
|
|
48393
|
+
console.log("");
|
|
48394
|
+
const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber(usageData.creditsUsed)} / unlimited` : `${formatNumber(usageData.creditsUsed)} / ${formatNumber(usageData.creditsLimit)}`;
|
|
48395
|
+
console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
|
|
48396
|
+
if (usageData.creditsLimit !== -1) {
|
|
48397
|
+
console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber(usageData.creditsRemaining)));
|
|
48398
|
+
console.log("");
|
|
48399
|
+
console.log(source_default.dim(" ") + createProgressBar(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
|
|
48400
|
+
}
|
|
48401
|
+
console.log("");
|
|
48402
|
+
console.log(source_default.bold(" This Month"));
|
|
48403
|
+
console.log("");
|
|
48404
|
+
console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
|
|
48405
|
+
console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
|
|
48406
|
+
console.log("");
|
|
48407
|
+
console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
|
|
48408
|
+
console.log("");
|
|
48409
|
+
if (result.recentScans && result.recentScans.length > 0) {
|
|
48410
|
+
console.log(source_default.bold(" Recent Scans"));
|
|
48411
|
+
console.log("");
|
|
48412
|
+
const recentToShow = result.recentScans.slice(0, 5);
|
|
48413
|
+
for (const scan of recentToShow) {
|
|
48414
|
+
const date = new Date(scan.createdAt);
|
|
48415
|
+
const timeAgo = getTimeAgo(date);
|
|
48416
|
+
console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
|
|
48417
|
+
console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
|
|
48418
|
+
}
|
|
48419
|
+
console.log("");
|
|
48420
|
+
}
|
|
48421
|
+
if (plan.name === "free" || plan.name === "starter") {
|
|
48422
|
+
console.log(source_default.dim(" " + "\u2500".repeat(38)));
|
|
48423
|
+
console.log("");
|
|
48424
|
+
console.log(source_default.dim(" Need more credits? Upgrade your plan"));
|
|
48425
|
+
console.log("");
|
|
48426
|
+
}
|
|
48427
|
+
await ve({
|
|
48428
|
+
message: "Press Enter to continue",
|
|
48429
|
+
options: [{ value: "back", label: "\u2190 Back to menu" }]
|
|
48430
|
+
});
|
|
48431
|
+
} catch (error) {
|
|
48432
|
+
spinner.stop("Failed to fetch usage data");
|
|
48433
|
+
console.log("");
|
|
48434
|
+
M2.error(String(error));
|
|
48435
|
+
console.log("");
|
|
48436
|
+
await ve({
|
|
48437
|
+
message: "Press Enter to continue",
|
|
48438
|
+
options: [{ value: "back", label: "\u2190 Back to menu" }]
|
|
48439
|
+
});
|
|
48440
|
+
}
|
|
48441
|
+
}
|
|
47975
48442
|
async function runUI() {
|
|
48443
|
+
console.clear();
|
|
48444
|
+
showLogo();
|
|
48445
|
+
console.log();
|
|
47976
48446
|
const onboardingResult = await handleOnboarding();
|
|
47977
48447
|
if (onboardingResult.quickStartResult) {
|
|
47978
48448
|
const { path: path2, depth } = onboardingResult.quickStartResult;
|
|
47979
|
-
Ie(source_default.bold("Oculum"));
|
|
47980
48449
|
try {
|
|
47981
48450
|
const { output, exitCode, result } = await runScanOnce(path2, {
|
|
47982
48451
|
depth,
|
|
@@ -48004,8 +48473,6 @@ async function runUI() {
|
|
|
48004
48473
|
} catch (err) {
|
|
48005
48474
|
M2.error(`Scan failed: ${String(err)}`);
|
|
48006
48475
|
}
|
|
48007
|
-
} else {
|
|
48008
|
-
Ie(source_default.bold("Oculum"));
|
|
48009
48476
|
}
|
|
48010
48477
|
let lastScanEntry;
|
|
48011
48478
|
const userState = getUserState();
|
|
@@ -48021,6 +48488,7 @@ async function runUI() {
|
|
|
48021
48488
|
{ value: "scan", label: "\u{1F50D} Custom Scan", hint: "Configure scan options" },
|
|
48022
48489
|
{ value: "history", label: "\u{1F4DC} Scan History", hint: `${listScanHistory().length} past scans` },
|
|
48023
48490
|
{ value: "auth", label: "\u{1F510} Auth", hint: isAuthenticated() ? `Logged in (${getConfig().tier || "free"})` : "Not logged in" },
|
|
48491
|
+
{ value: "usage", label: "\u{1F4CA} Usage", hint: isAuthenticated() ? "View credits and quota" : "Requires login" },
|
|
48024
48492
|
{ value: "help", label: "\u2753 Help", hint: "Commands and tips" },
|
|
48025
48493
|
{ value: "exit", label: "\u{1F44B} Exit" }
|
|
48026
48494
|
];
|
|
@@ -48102,6 +48570,10 @@ async function runUI() {
|
|
|
48102
48570
|
await runHistoryFlow();
|
|
48103
48571
|
continue;
|
|
48104
48572
|
}
|
|
48573
|
+
if (action === "usage") {
|
|
48574
|
+
await runUsageFlow();
|
|
48575
|
+
continue;
|
|
48576
|
+
}
|
|
48105
48577
|
if (action === "help") {
|
|
48106
48578
|
await showHelpScreen();
|
|
48107
48579
|
continue;
|
|
@@ -48154,11 +48626,11 @@ var uiCommand = new Command("ui").description("Interactive terminal UI").action(
|
|
|
48154
48626
|
});
|
|
48155
48627
|
|
|
48156
48628
|
// src/commands/usage.ts
|
|
48157
|
-
function
|
|
48629
|
+
function formatNumber2(num) {
|
|
48158
48630
|
if (num === -1) return "unlimited";
|
|
48159
48631
|
return num.toLocaleString();
|
|
48160
48632
|
}
|
|
48161
|
-
function
|
|
48633
|
+
function createProgressBar2(percentage, width = 20) {
|
|
48162
48634
|
const filled = Math.round(percentage / 100 * width);
|
|
48163
48635
|
const empty = width - filled;
|
|
48164
48636
|
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
@@ -48166,7 +48638,7 @@ function createProgressBar(percentage, width = 20) {
|
|
|
48166
48638
|
if (percentage >= 70) return source_default.yellow(bar);
|
|
48167
48639
|
return source_default.green(bar);
|
|
48168
48640
|
}
|
|
48169
|
-
function
|
|
48641
|
+
function formatDate2(dateStr) {
|
|
48170
48642
|
const date = new Date(dateStr);
|
|
48171
48643
|
return date.toLocaleDateString("en-US", {
|
|
48172
48644
|
month: "short",
|
|
@@ -48211,20 +48683,20 @@ async function usage(options) {
|
|
|
48211
48683
|
console.log("");
|
|
48212
48684
|
console.log(source_default.bold(" Credits Usage"));
|
|
48213
48685
|
console.log("");
|
|
48214
|
-
const creditsDisplay = usageData.creditsLimit === -1 ? `${
|
|
48686
|
+
const creditsDisplay = usageData.creditsLimit === -1 ? `${formatNumber2(usageData.creditsUsed)} / unlimited` : `${formatNumber2(usageData.creditsUsed)} / ${formatNumber2(usageData.creditsLimit)}`;
|
|
48215
48687
|
console.log(source_default.dim(" Used: ") + source_default.white(creditsDisplay));
|
|
48216
48688
|
if (usageData.creditsLimit !== -1) {
|
|
48217
|
-
console.log(source_default.dim(" Remaining: ") + source_default.white(
|
|
48689
|
+
console.log(source_default.dim(" Remaining: ") + source_default.white(formatNumber2(usageData.creditsRemaining)));
|
|
48218
48690
|
console.log("");
|
|
48219
|
-
console.log(source_default.dim(" ") +
|
|
48691
|
+
console.log(source_default.dim(" ") + createProgressBar2(usageData.usagePercentage) + source_default.dim(` ${usageData.usagePercentage.toFixed(1)}%`));
|
|
48220
48692
|
}
|
|
48221
48693
|
console.log("");
|
|
48222
48694
|
console.log(source_default.bold(" This Month"));
|
|
48223
48695
|
console.log("");
|
|
48224
|
-
console.log(source_default.dim(" Scans: ") + source_default.white(
|
|
48225
|
-
console.log(source_default.dim(" Files: ") + source_default.white(
|
|
48696
|
+
console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber2(usageData.totalScans)));
|
|
48697
|
+
console.log(source_default.dim(" Files: ") + source_default.white(formatNumber2(usageData.totalFiles)));
|
|
48226
48698
|
console.log("");
|
|
48227
|
-
console.log(source_default.dim(" Resets on: ") + source_default.white(
|
|
48699
|
+
console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate2(usageData.resetDate)));
|
|
48228
48700
|
console.log("");
|
|
48229
48701
|
if (result.recentScans && result.recentScans.length > 0) {
|
|
48230
48702
|
console.log(source_default.bold(" Recent Scans"));
|
|
@@ -48232,7 +48704,7 @@ async function usage(options) {
|
|
|
48232
48704
|
const recentToShow = result.recentScans.slice(0, 5);
|
|
48233
48705
|
for (const scan of recentToShow) {
|
|
48234
48706
|
const date = new Date(scan.createdAt);
|
|
48235
|
-
const timeAgo =
|
|
48707
|
+
const timeAgo = getTimeAgo2(date);
|
|
48236
48708
|
console.log(source_default.dim(" \u2022 ") + source_default.white(scan.repoName || "unknown") + source_default.dim(` (${timeAgo})`));
|
|
48237
48709
|
console.log(source_default.dim(" ") + source_default.dim(`${scan.filesScanned} files, ${scan.findingsCount} findings, ${scan.creditsUsed} credits`));
|
|
48238
48710
|
}
|
|
@@ -48252,7 +48724,7 @@ async function usage(options) {
|
|
|
48252
48724
|
process.exit(1);
|
|
48253
48725
|
}
|
|
48254
48726
|
}
|
|
48255
|
-
function
|
|
48727
|
+
function getTimeAgo2(date) {
|
|
48256
48728
|
const now = /* @__PURE__ */ new Date();
|
|
48257
48729
|
const diffMs = now.getTime() - date.getTime();
|
|
48258
48730
|
const diffMins = Math.floor(diffMs / 6e4);
|
|
@@ -48266,9 +48738,251 @@ function getTimeAgo(date) {
|
|
|
48266
48738
|
}
|
|
48267
48739
|
var usageCommand = new Command("usage").description("Show current usage and quota").option("--json", "Output as JSON").action(usage);
|
|
48268
48740
|
|
|
48741
|
+
// src/commands/help.ts
|
|
48742
|
+
var TOPICS = {
|
|
48743
|
+
"scan-modes": showScanModes,
|
|
48744
|
+
"ci-setup": showCISetup,
|
|
48745
|
+
"config": showConfig,
|
|
48746
|
+
"troubleshooting": showTroubleshooting
|
|
48747
|
+
};
|
|
48748
|
+
var helpCommand = new Command("help").description("Get detailed help on specific topics").argument("[topic]", "Help topic (scan-modes, ci-setup, config, troubleshooting)").action(async (topic) => {
|
|
48749
|
+
if (!topic) {
|
|
48750
|
+
showTopicList();
|
|
48751
|
+
return;
|
|
48752
|
+
}
|
|
48753
|
+
const normalizedTopic = topic.toLowerCase().replace(/_/g, "-");
|
|
48754
|
+
if (normalizedTopic in TOPICS) {
|
|
48755
|
+
TOPICS[normalizedTopic]();
|
|
48756
|
+
} else {
|
|
48757
|
+
console.log(source_default.red(`Unknown topic: ${topic}
|
|
48758
|
+
`));
|
|
48759
|
+
showTopicList();
|
|
48760
|
+
}
|
|
48761
|
+
});
|
|
48762
|
+
function showTopicList() {
|
|
48763
|
+
console.log(source_default.bold("\nOculum Help Topics\n"));
|
|
48764
|
+
console.log(source_default.dim("\u2500".repeat(50)));
|
|
48765
|
+
console.log();
|
|
48766
|
+
console.log(source_default.cyan(" scan-modes ") + source_default.white("Compare cheap, validated, and deep scans"));
|
|
48767
|
+
console.log(source_default.cyan(" ci-setup ") + source_default.white("GitHub Actions and GitLab CI examples"));
|
|
48768
|
+
console.log(source_default.cyan(" config ") + source_default.white("Configuration file documentation"));
|
|
48769
|
+
console.log(source_default.cyan(" troubleshooting ") + source_default.white("Common issues and solutions"));
|
|
48770
|
+
console.log();
|
|
48771
|
+
console.log(source_default.dim("\u2500".repeat(50)));
|
|
48772
|
+
console.log(source_default.dim("\nUsage: oculum help <topic>"));
|
|
48773
|
+
console.log(source_default.dim("Example: oculum help scan-modes\n"));
|
|
48774
|
+
}
|
|
48775
|
+
function showScanModes() {
|
|
48776
|
+
console.log(source_default.bold("\nScan Modes Comparison\n"));
|
|
48777
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48778
|
+
console.log(source_default.green.bold(" CHEAP (Quick Scan)"));
|
|
48779
|
+
console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
48780
|
+
console.log(source_default.white(" Cost: ") + source_default.green("Free"));
|
|
48781
|
+
console.log(source_default.white(" Speed: ") + source_default.white("~1000 files/second"));
|
|
48782
|
+
console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + entropy analysis"));
|
|
48783
|
+
console.log(source_default.white(" Best for: ") + source_default.dim("Quick checks, CI/CD pipelines"));
|
|
48784
|
+
console.log(source_default.white(" Limitation: ") + source_default.dim("May have more false positives\n"));
|
|
48785
|
+
console.log(source_default.blue.bold(" VALIDATED"));
|
|
48786
|
+
console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
48787
|
+
console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.03 per 300 files"));
|
|
48788
|
+
console.log(source_default.white(" Speed: ") + source_default.white("~30 seconds for 300 files"));
|
|
48789
|
+
console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + AI validation"));
|
|
48790
|
+
console.log(source_default.white(" Best for: ") + source_default.dim("Pre-commit checks, PR reviews"));
|
|
48791
|
+
console.log(source_default.white(" Benefit: ") + source_default.dim("~70% fewer false positives\n"));
|
|
48792
|
+
console.log(source_default.magenta.bold(" DEEP"));
|
|
48793
|
+
console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
48794
|
+
console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.10 per 300 files"));
|
|
48795
|
+
console.log(source_default.white(" Speed: ") + source_default.white("~60 seconds for 300 files"));
|
|
48796
|
+
console.log(source_default.white(" How: ") + source_default.dim("Full semantic analysis with AI reasoning"));
|
|
48797
|
+
console.log(source_default.white(" Best for: ") + source_default.dim("Security audits, release checks"));
|
|
48798
|
+
console.log(source_default.white(" Benefit: ") + source_default.dim("Deepest analysis, remediation advice\n"));
|
|
48799
|
+
console.log(source_default.dim("\u2500".repeat(60)));
|
|
48800
|
+
console.log(source_default.bold("\nUsage Examples:\n"));
|
|
48801
|
+
console.log(source_default.dim(" $ oculum scan . ") + source_default.white("# Quick scan (free)"));
|
|
48802
|
+
console.log(source_default.dim(" $ oculum scan . -d validated ") + source_default.white("# AI-validated scan"));
|
|
48803
|
+
console.log(source_default.dim(" $ oculum scan . -d deep ") + source_default.white("# Deep semantic analysis"));
|
|
48804
|
+
console.log();
|
|
48805
|
+
}
|
|
48806
|
+
function showCISetup() {
|
|
48807
|
+
console.log(source_default.bold("\nCI/CD Integration\n"));
|
|
48808
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48809
|
+
console.log(source_default.bold(" GitHub Actions\n"));
|
|
48810
|
+
console.log(source_default.dim(" Create .github/workflows/oculum.yml:\n"));
|
|
48811
|
+
console.log(source_default.cyan(` name: Security Scan
|
|
48812
|
+
on: [push, pull_request]
|
|
48813
|
+
jobs:
|
|
48814
|
+
scan:
|
|
48815
|
+
runs-on: ubuntu-latest
|
|
48816
|
+
steps:
|
|
48817
|
+
- uses: actions/checkout@v4
|
|
48818
|
+
- uses: actions/setup-node@v4
|
|
48819
|
+
with:
|
|
48820
|
+
node-version: '20'
|
|
48821
|
+
- run: npm install -g oculum
|
|
48822
|
+
- run: oculum scan . --fail-on high
|
|
48823
|
+
env:
|
|
48824
|
+
OCULUM_API_KEY: \${{ secrets.OCULUM_API_KEY }}`));
|
|
48825
|
+
console.log(source_default.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
48826
|
+
console.log(source_default.bold(" GitLab CI\n"));
|
|
48827
|
+
console.log(source_default.dim(" Add to .gitlab-ci.yml:\n"));
|
|
48828
|
+
console.log(source_default.cyan(` security-scan:
|
|
48829
|
+
image: node:20
|
|
48830
|
+
script:
|
|
48831
|
+
- npm install -g oculum
|
|
48832
|
+
- oculum scan . --fail-on high
|
|
48833
|
+
variables:
|
|
48834
|
+
OCULUM_API_KEY: $OCULUM_API_KEY`));
|
|
48835
|
+
console.log(source_default.dim("\n\u2500".repeat(60)));
|
|
48836
|
+
console.log(source_default.bold("\nTips:\n"));
|
|
48837
|
+
console.log(source_default.white(" - Use ") + source_default.cyan("--fail-on high") + source_default.white(" to fail builds on high/critical findings"));
|
|
48838
|
+
console.log(source_default.white(" - Use ") + source_default.cyan("--format sarif") + source_default.white(" for GitHub Code Scanning integration"));
|
|
48839
|
+
console.log(source_default.white(" - Use ") + source_default.cyan("-d cheap") + source_default.white(" for fastest scans (free, no API key needed)"));
|
|
48840
|
+
console.log(source_default.white(" - Store API key in secrets, not in code\n"));
|
|
48841
|
+
}
|
|
48842
|
+
function showConfig() {
|
|
48843
|
+
console.log(source_default.bold("\nConfiguration Files\n"));
|
|
48844
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48845
|
+
console.log(source_default.bold(" Project Config (oculum.config.json)\n"));
|
|
48846
|
+
console.log(source_default.dim(" Create in your project root:\n"));
|
|
48847
|
+
console.log(source_default.cyan(` {
|
|
48848
|
+
"depth": "validated",
|
|
48849
|
+
"failOn": "high",
|
|
48850
|
+
"format": "terminal",
|
|
48851
|
+
"ignore": [
|
|
48852
|
+
"**/node_modules/**",
|
|
48853
|
+
"**/dist/**",
|
|
48854
|
+
"**/*.test.ts"
|
|
48855
|
+
],
|
|
48856
|
+
"include": [
|
|
48857
|
+
"src/**"
|
|
48858
|
+
],
|
|
48859
|
+
"watch": {
|
|
48860
|
+
"debounce": 500,
|
|
48861
|
+
"clear": true
|
|
48862
|
+
}
|
|
48863
|
+
}`));
|
|
48864
|
+
console.log(source_default.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
48865
|
+
console.log(source_default.bold(" Config Options\n"));
|
|
48866
|
+
console.log(source_default.white(" depth ") + source_default.dim("Scan depth: cheap | validated | deep"));
|
|
48867
|
+
console.log(source_default.white(" failOn ") + source_default.dim("Exit code 1 on: critical | high | medium | low | none"));
|
|
48868
|
+
console.log(source_default.white(" format ") + source_default.dim("Output format: terminal | json | sarif | markdown"));
|
|
48869
|
+
console.log(source_default.white(" output ") + source_default.dim("Output file path"));
|
|
48870
|
+
console.log(source_default.white(" quiet ") + source_default.dim("Only show findings (boolean)"));
|
|
48871
|
+
console.log(source_default.white(" ignore ") + source_default.dim("Glob patterns to exclude"));
|
|
48872
|
+
console.log(source_default.white(" include ") + source_default.dim("Glob patterns to include"));
|
|
48873
|
+
console.log(source_default.dim("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
48874
|
+
console.log(source_default.bold(" Config File Priority\n"));
|
|
48875
|
+
console.log(source_default.dim(" CLI flags > Project config > User config > Defaults\n"));
|
|
48876
|
+
console.log(source_default.white(" Supported filenames:"));
|
|
48877
|
+
console.log(source_default.dim(" - oculum.config.json"));
|
|
48878
|
+
console.log(source_default.dim(" - .oculumrc.json"));
|
|
48879
|
+
console.log(source_default.dim(" - .oculumrc\n"));
|
|
48880
|
+
}
|
|
48881
|
+
function showTroubleshooting() {
|
|
48882
|
+
console.log(source_default.bold("\nTroubleshooting\n"));
|
|
48883
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48884
|
+
console.log(source_default.red.bold(" Authentication Errors\n"));
|
|
48885
|
+
console.log(source_default.white(' "Authentication required"'));
|
|
48886
|
+
console.log(source_default.dim(" \u2192 Run `oculum login` to authenticate"));
|
|
48887
|
+
console.log(source_default.dim(" \u2192 Or use `--depth cheap` for free local scans\n"));
|
|
48888
|
+
console.log(source_default.white(' "API key invalid or expired"'));
|
|
48889
|
+
console.log(source_default.dim(" \u2192 Run `oculum login` to re-authenticate"));
|
|
48890
|
+
console.log(source_default.dim(" \u2192 Check key at https://oculum.dev/dashboard/api-keys\n"));
|
|
48891
|
+
console.log(source_default.white(' "Insufficient tier"'));
|
|
48892
|
+
console.log(source_default.dim(" \u2192 Validated/deep scans require Pro subscription"));
|
|
48893
|
+
console.log(source_default.dim(" \u2192 Visit https://oculum.dev/billing to upgrade\n"));
|
|
48894
|
+
console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
48895
|
+
console.log(source_default.yellow.bold(" Network Errors\n"));
|
|
48896
|
+
console.log(source_default.white(' "Connection refused" / "Network error"'));
|
|
48897
|
+
console.log(source_default.dim(" \u2192 Check your internet connection"));
|
|
48898
|
+
console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
|
|
48899
|
+
console.log(source_default.dim(" \u2192 Check https://status.oculum.dev\n"));
|
|
48900
|
+
console.log(source_default.white(' "SSL certificate error"'));
|
|
48901
|
+
console.log(source_default.dim(" \u2192 Corporate proxy may be intercepting HTTPS"));
|
|
48902
|
+
console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
|
|
48903
|
+
console.log(source_default.dim(" \u2192 Contact IT for proxy configuration\n"));
|
|
48904
|
+
console.log(source_default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
48905
|
+
console.log(source_default.blue.bold(" Scan Issues\n"));
|
|
48906
|
+
console.log(source_default.white(' "No scannable files found"'));
|
|
48907
|
+
console.log(source_default.dim(" \u2192 Check path exists: ls <path>"));
|
|
48908
|
+
console.log(source_default.dim(" \u2192 Ensure files have supported extensions"));
|
|
48909
|
+
console.log(source_default.dim(" \u2192 Check .gitignore isn't excluding files\n"));
|
|
48910
|
+
console.log(source_default.white(' "Request timed out"'));
|
|
48911
|
+
console.log(source_default.dim(" \u2192 Try scanning fewer files"));
|
|
48912
|
+
console.log(source_default.dim(" \u2192 Use `--depth cheap` for large codebases"));
|
|
48913
|
+
console.log(source_default.dim(" \u2192 Split into multiple scans\n"));
|
|
48914
|
+
console.log(source_default.white(' "Out of memory"'));
|
|
48915
|
+
console.log(source_default.dim(" \u2192 Scan a smaller directory"));
|
|
48916
|
+
console.log(source_default.dim(' \u2192 Increase Node memory: NODE_OPTIONS="--max-old-space-size=4096"\n'));
|
|
48917
|
+
console.log(source_default.dim("\u2500".repeat(60)));
|
|
48918
|
+
console.log(source_default.bold("\nStill stuck?\n"));
|
|
48919
|
+
console.log(source_default.white(" - Check docs: ") + source_default.cyan("https://oculum.dev/docs"));
|
|
48920
|
+
console.log(source_default.white(" - Report issues: ") + source_default.cyan("https://github.com/oculum-dev/oculum/issues"));
|
|
48921
|
+
console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
|
|
48922
|
+
}
|
|
48923
|
+
|
|
48924
|
+
// src/utils/ci-detect.ts
|
|
48925
|
+
var CI_ENV_VARS = [
|
|
48926
|
+
"CI",
|
|
48927
|
+
// Generic CI flag (GitHub Actions, GitLab CI, etc.)
|
|
48928
|
+
"GITHUB_ACTIONS",
|
|
48929
|
+
// GitHub Actions
|
|
48930
|
+
"GITLAB_CI",
|
|
48931
|
+
// GitLab CI
|
|
48932
|
+
"JENKINS_URL",
|
|
48933
|
+
// Jenkins
|
|
48934
|
+
"CIRCLECI",
|
|
48935
|
+
// CircleCI
|
|
48936
|
+
"TRAVIS",
|
|
48937
|
+
// Travis CI
|
|
48938
|
+
"BUILDKITE",
|
|
48939
|
+
// Buildkite
|
|
48940
|
+
"DRONE",
|
|
48941
|
+
// Drone CI
|
|
48942
|
+
"TEAMCITY_VERSION",
|
|
48943
|
+
// TeamCity
|
|
48944
|
+
"BITBUCKET_COMMIT",
|
|
48945
|
+
// Bitbucket Pipelines
|
|
48946
|
+
"AZURE_HTTP_USER_AGENT",
|
|
48947
|
+
// Azure Pipelines
|
|
48948
|
+
"TF_BUILD",
|
|
48949
|
+
// Azure Pipelines (alternative)
|
|
48950
|
+
"CODEBUILD_BUILD_ID",
|
|
48951
|
+
// AWS CodeBuild
|
|
48952
|
+
"APPVEYOR",
|
|
48953
|
+
// AppVeyor
|
|
48954
|
+
"SEMAPHORE",
|
|
48955
|
+
// Semaphore CI
|
|
48956
|
+
"HEROKU_TEST_RUN_ID",
|
|
48957
|
+
// Heroku CI
|
|
48958
|
+
"RENDER",
|
|
48959
|
+
// Render
|
|
48960
|
+
"VERCEL",
|
|
48961
|
+
// Vercel
|
|
48962
|
+
"NETLIFY",
|
|
48963
|
+
// Netlify
|
|
48964
|
+
"CF_PAGES",
|
|
48965
|
+
// Cloudflare Pages
|
|
48966
|
+
"RAILWAY_ENVIRONMENT"
|
|
48967
|
+
// Railway
|
|
48968
|
+
];
|
|
48969
|
+
function isCI() {
|
|
48970
|
+
return CI_ENV_VARS.some((envVar) => !!process.env[envVar]);
|
|
48971
|
+
}
|
|
48972
|
+
function isInteractiveTerminal() {
|
|
48973
|
+
return !!(process.stdin.isTTY && !isCI());
|
|
48974
|
+
}
|
|
48975
|
+
|
|
48269
48976
|
// src/index.ts
|
|
48977
|
+
function shouldRunUI() {
|
|
48978
|
+
const programName = process.argv[1] || "";
|
|
48979
|
+
const args = process.argv.slice(2);
|
|
48980
|
+
const isOcAlias = programName.endsWith("oc") && !programName.endsWith("oculum");
|
|
48981
|
+
const isUICommand = args.length === 0 || args.length === 1 && args[0] === "ui";
|
|
48982
|
+
return isOcAlias || isUICommand;
|
|
48983
|
+
}
|
|
48270
48984
|
var program2 = new Command();
|
|
48271
|
-
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.
|
|
48985
|
+
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.8").addHelpText("after", `
|
|
48272
48986
|
Quick Start:
|
|
48273
48987
|
$ oculum scan . Scan current directory (free)
|
|
48274
48988
|
$ oculum ui Interactive mode with guided setup
|
|
@@ -48281,11 +48995,13 @@ Common Commands:
|
|
|
48281
48995
|
login Authenticate with Oculum
|
|
48282
48996
|
status Check authentication status
|
|
48283
48997
|
usage View credits and quota
|
|
48998
|
+
help [topic] Get help on specific topics
|
|
48284
48999
|
|
|
48285
49000
|
Learn More:
|
|
48286
49001
|
$ oculum scan --help Detailed scan options
|
|
49002
|
+
$ oculum help scan-modes Compare scan depth options
|
|
48287
49003
|
$ oculum --help All available commands
|
|
48288
|
-
|
|
49004
|
+
|
|
48289
49005
|
Documentation: https://oculum.dev/docs
|
|
48290
49006
|
`);
|
|
48291
49007
|
program2.addCommand(scanCommand, { isDefault: true });
|
|
@@ -48296,7 +49012,17 @@ program2.addCommand(upgradeCommand);
|
|
|
48296
49012
|
program2.addCommand(usageCommand);
|
|
48297
49013
|
program2.addCommand(watchCommand);
|
|
48298
49014
|
program2.addCommand(uiCommand);
|
|
48299
|
-
program2.
|
|
49015
|
+
program2.addCommand(helpCommand);
|
|
49016
|
+
async function main2() {
|
|
49017
|
+
const interactive = isInteractiveTerminal();
|
|
49018
|
+
if (interactive && shouldRunUI()) {
|
|
49019
|
+
process.argv = ["node", "oculum", "ui"];
|
|
49020
|
+
program2.parse();
|
|
49021
|
+
return;
|
|
49022
|
+
}
|
|
49023
|
+
program2.parse();
|
|
49024
|
+
}
|
|
49025
|
+
main2();
|
|
48300
49026
|
/*! Bundled license information:
|
|
48301
49027
|
|
|
48302
49028
|
chokidar/esm/index.js:
|