@oculum/cli 1.0.8 → 1.0.10
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 +743 -120
- package/package.json +4 -3
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 [];
|
|
@@ -43083,6 +43313,16 @@ function getApiBaseUrl() {
|
|
|
43083
43313
|
function setAuthCredentials(apiKey, email, tier) {
|
|
43084
43314
|
updateConfig({ apiKey, email, tier });
|
|
43085
43315
|
}
|
|
43316
|
+
function syncAuthFromVerification(verifyResponse) {
|
|
43317
|
+
if (!verifyResponse.valid) return;
|
|
43318
|
+
const config = getConfig();
|
|
43319
|
+
if (!config.apiKey) return;
|
|
43320
|
+
const email = verifyResponse.email || config.email;
|
|
43321
|
+
const tier = verifyResponse.tier || config.tier || "free";
|
|
43322
|
+
if (tier !== config.tier || email !== config.email) {
|
|
43323
|
+
setAuthCredentials(config.apiKey, email, tier);
|
|
43324
|
+
}
|
|
43325
|
+
}
|
|
43086
43326
|
|
|
43087
43327
|
// src/utils/api.ts
|
|
43088
43328
|
var APIError = class extends Error {
|
|
@@ -43239,8 +43479,11 @@ function enhanceAPIError(error) {
|
|
|
43239
43479
|
case 401:
|
|
43240
43480
|
return {
|
|
43241
43481
|
message: "Authentication required",
|
|
43242
|
-
suggestion: "
|
|
43482
|
+
suggestion: "You need to login to use AI-powered scans.",
|
|
43243
43483
|
category: "auth",
|
|
43484
|
+
errorCode: "OCU-E401",
|
|
43485
|
+
quickFix: "oculum login",
|
|
43486
|
+
learnMoreUrl: "https://oculum.dev/docs/authentication",
|
|
43244
43487
|
recoveryActions: [
|
|
43245
43488
|
{ label: "Login", command: "oculum login", action: "login" },
|
|
43246
43489
|
{ label: "Use free scan", action: "fallback" }
|
|
@@ -43250,8 +43493,11 @@ function enhanceAPIError(error) {
|
|
|
43250
43493
|
if (error.reason === "insufficient_tier") {
|
|
43251
43494
|
return {
|
|
43252
43495
|
message: "This feature requires a Pro subscription",
|
|
43253
|
-
suggestion: "
|
|
43496
|
+
suggestion: "Validated and deep scans require a Pro plan.",
|
|
43254
43497
|
category: "auth",
|
|
43498
|
+
errorCode: "OCU-E403-TIER",
|
|
43499
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43500
|
+
learnMoreUrl: "https://oculum.dev/billing",
|
|
43255
43501
|
recoveryActions: [
|
|
43256
43502
|
{ label: "View pricing", action: "upgrade" },
|
|
43257
43503
|
{ label: "Use free scan", action: "fallback" }
|
|
@@ -43261,8 +43507,11 @@ function enhanceAPIError(error) {
|
|
|
43261
43507
|
if (error.reason === "expired") {
|
|
43262
43508
|
return {
|
|
43263
43509
|
message: "Your API key has expired",
|
|
43264
|
-
suggestion: "Generate a new key
|
|
43510
|
+
suggestion: "Generate a new key to continue using AI-powered scans.",
|
|
43265
43511
|
category: "auth",
|
|
43512
|
+
errorCode: "OCU-E403-EXP",
|
|
43513
|
+
quickFix: "oculum login",
|
|
43514
|
+
learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
|
|
43266
43515
|
recoveryActions: [
|
|
43267
43516
|
{ label: "Login again", command: "oculum login", action: "login" }
|
|
43268
43517
|
]
|
|
@@ -43271,8 +43520,11 @@ function enhanceAPIError(error) {
|
|
|
43271
43520
|
if (error.reason === "invalid_key") {
|
|
43272
43521
|
return {
|
|
43273
43522
|
message: "Invalid API key",
|
|
43274
|
-
suggestion: "Your API key is not recognized.
|
|
43523
|
+
suggestion: "Your API key is not recognized.",
|
|
43275
43524
|
category: "auth",
|
|
43525
|
+
errorCode: "OCU-E403-KEY",
|
|
43526
|
+
quickFix: "oculum login",
|
|
43527
|
+
learnMoreUrl: "https://oculum.dev/dashboard/api-keys",
|
|
43276
43528
|
recoveryActions: [
|
|
43277
43529
|
{ label: "Login", command: "oculum login", action: "login" },
|
|
43278
43530
|
{ label: "Use free scan", action: "fallback" }
|
|
@@ -43281,19 +43533,24 @@ function enhanceAPIError(error) {
|
|
|
43281
43533
|
}
|
|
43282
43534
|
return {
|
|
43283
43535
|
message: "Access denied",
|
|
43284
|
-
suggestion: "Check your API key permissions
|
|
43536
|
+
suggestion: "Check your API key permissions.",
|
|
43285
43537
|
category: "auth",
|
|
43538
|
+
errorCode: "OCU-E403",
|
|
43539
|
+
quickFix: "oculum login",
|
|
43286
43540
|
recoveryActions: [
|
|
43287
43541
|
{ label: "Login", command: "oculum login", action: "login" }
|
|
43288
43542
|
]
|
|
43289
43543
|
};
|
|
43290
43544
|
case 429:
|
|
43291
43545
|
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.
|
|
43546
|
+
const rateLimitSuggestion = error.reason === "quota_exceeded" ? "Your quota resets at the start of next month." : "Wait a moment before trying again.";
|
|
43293
43547
|
return {
|
|
43294
43548
|
message: rateLimitInfo,
|
|
43295
43549
|
suggestion: rateLimitSuggestion,
|
|
43296
43550
|
category: "server",
|
|
43551
|
+
errorCode: error.reason === "quota_exceeded" ? "OCU-E429-QUOTA" : "OCU-E429-RATE",
|
|
43552
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43553
|
+
learnMoreUrl: "https://oculum.dev/dashboard/usage",
|
|
43297
43554
|
recoveryActions: [
|
|
43298
43555
|
{ label: "Use free local scan", command: "oculum scan . --depth cheap", action: "fallback" },
|
|
43299
43556
|
{ label: "View usage & upgrade", action: "upgrade" },
|
|
@@ -43306,8 +43563,11 @@ function enhanceAPIError(error) {
|
|
|
43306
43563
|
case 503:
|
|
43307
43564
|
return {
|
|
43308
43565
|
message: "Oculum servers are experiencing issues",
|
|
43309
|
-
suggestion: "Try again in a few minutes.
|
|
43566
|
+
suggestion: "This is temporary. Try again in a few minutes.",
|
|
43310
43567
|
category: "server",
|
|
43568
|
+
errorCode: `OCU-E${error.statusCode}`,
|
|
43569
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43570
|
+
learnMoreUrl: "https://status.oculum.dev",
|
|
43311
43571
|
recoveryActions: [
|
|
43312
43572
|
{ label: "Retry", action: "retry" },
|
|
43313
43573
|
{ label: "Use free scan (offline)", action: "fallback" }
|
|
@@ -43316,15 +43576,22 @@ function enhanceAPIError(error) {
|
|
|
43316
43576
|
case 504:
|
|
43317
43577
|
return {
|
|
43318
43578
|
message: "Request timed out",
|
|
43319
|
-
suggestion: "The scan may be too large
|
|
43579
|
+
suggestion: "The scan may be too large for the server.",
|
|
43320
43580
|
category: "server",
|
|
43581
|
+
errorCode: "OCU-E504",
|
|
43582
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43583
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#timeout",
|
|
43321
43584
|
recoveryActions: [
|
|
43322
43585
|
{ label: "Retry", action: "retry" },
|
|
43323
43586
|
{ label: "Use quick scan", action: "fallback" }
|
|
43324
43587
|
]
|
|
43325
43588
|
};
|
|
43326
43589
|
default:
|
|
43327
|
-
return {
|
|
43590
|
+
return {
|
|
43591
|
+
message: error.message,
|
|
43592
|
+
category: "unknown",
|
|
43593
|
+
errorCode: "OCU-E000"
|
|
43594
|
+
};
|
|
43328
43595
|
}
|
|
43329
43596
|
}
|
|
43330
43597
|
function detectNetworkErrorType(msg) {
|
|
@@ -43372,6 +43639,7 @@ function enhanceStandardError(error) {
|
|
|
43372
43639
|
message: `Path not found: ${path2}`,
|
|
43373
43640
|
suggestion: "Check that the path exists and is spelled correctly.",
|
|
43374
43641
|
category: "file",
|
|
43642
|
+
errorCode: "OCU-E001",
|
|
43375
43643
|
details: "The file or directory does not exist at the specified location."
|
|
43376
43644
|
};
|
|
43377
43645
|
}
|
|
@@ -43380,15 +43648,33 @@ function enhanceStandardError(error) {
|
|
|
43380
43648
|
message: "Permission denied",
|
|
43381
43649
|
suggestion: "Check file permissions or try running with appropriate access.",
|
|
43382
43650
|
category: "file",
|
|
43651
|
+
errorCode: "OCU-E002",
|
|
43383
43652
|
details: "You do not have permission to access this file or directory."
|
|
43384
43653
|
};
|
|
43385
43654
|
}
|
|
43655
|
+
if (msg.includes("self-signed") || msg.includes("unable to verify") || msg.includes("certificate") && (msg.includes("invalid") || msg.includes("expired"))) {
|
|
43656
|
+
return {
|
|
43657
|
+
message: "SSL certificate error",
|
|
43658
|
+
suggestion: "Your network may be intercepting HTTPS traffic (corporate proxy).",
|
|
43659
|
+
category: "network",
|
|
43660
|
+
errorCode: "OCU-E003",
|
|
43661
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43662
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#ssl",
|
|
43663
|
+
recoveryActions: [
|
|
43664
|
+
{ label: "Use offline scan", command: "oculum scan . --depth cheap", action: "fallback" },
|
|
43665
|
+
{ label: "View help", action: "help" }
|
|
43666
|
+
]
|
|
43667
|
+
};
|
|
43668
|
+
}
|
|
43386
43669
|
if (msg.includes("fetch failed") || msg.includes("network") || msg.includes("econnrefused") || msg.includes("enotfound") || msg.includes("etimedout") || msg.includes("econnreset") || msg.includes("socket")) {
|
|
43387
43670
|
const { type, suggestion } = detectNetworkErrorType(msg);
|
|
43388
43671
|
return {
|
|
43389
43672
|
message: type,
|
|
43390
43673
|
suggestion,
|
|
43391
43674
|
category: "network",
|
|
43675
|
+
errorCode: "OCU-E004",
|
|
43676
|
+
quickFix: "oculum scan . --depth cheap",
|
|
43677
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#network",
|
|
43392
43678
|
recoveryActions: [
|
|
43393
43679
|
{ label: "Retry", action: "retry" },
|
|
43394
43680
|
{ label: "Use offline scan", action: "fallback" }
|
|
@@ -43400,52 +43686,105 @@ function enhanceStandardError(error) {
|
|
|
43400
43686
|
message: "Invalid configuration file",
|
|
43401
43687
|
suggestion: "Check your config file for valid JSON syntax.",
|
|
43402
43688
|
category: "file",
|
|
43689
|
+
errorCode: "OCU-E005",
|
|
43690
|
+
learnMoreUrl: "https://oculum.dev/docs/configuration",
|
|
43403
43691
|
details: "The configuration file contains invalid JSON. Use a JSON validator to find the error."
|
|
43404
43692
|
};
|
|
43405
43693
|
}
|
|
43406
43694
|
if (msg.includes("no scannable files")) {
|
|
43407
43695
|
return {
|
|
43408
43696
|
message: "No scannable files found",
|
|
43409
|
-
suggestion: "Check that the path contains supported file types
|
|
43697
|
+
suggestion: "Check that the path contains supported file types.",
|
|
43410
43698
|
category: "scan",
|
|
43411
|
-
|
|
43699
|
+
errorCode: "OCU-E006",
|
|
43700
|
+
details: "Supported: .js, .jsx, .ts, .tsx, .py, .go, .java, .rb, .php, .yaml, .json"
|
|
43412
43701
|
};
|
|
43413
43702
|
}
|
|
43414
43703
|
if (msg.includes("symlink") || msg.includes("eloop")) {
|
|
43415
43704
|
return {
|
|
43416
43705
|
message: "Symbolic link error",
|
|
43417
43706
|
suggestion: "There may be a circular symlink. Try scanning a specific directory instead.",
|
|
43418
|
-
category: "file"
|
|
43707
|
+
category: "file",
|
|
43708
|
+
errorCode: "OCU-E007"
|
|
43419
43709
|
};
|
|
43420
43710
|
}
|
|
43421
43711
|
if (msg.includes("heap") || msg.includes("memory") || msg.includes("enomem")) {
|
|
43422
43712
|
return {
|
|
43423
43713
|
message: "Out of memory",
|
|
43424
|
-
suggestion: "The scan ran out of memory. Try scanning fewer files
|
|
43425
|
-
category: "scan"
|
|
43714
|
+
suggestion: "The scan ran out of memory. Try scanning fewer files.",
|
|
43715
|
+
category: "scan",
|
|
43716
|
+
errorCode: "OCU-E008",
|
|
43717
|
+
quickFix: 'NODE_OPTIONS="--max-old-space-size=4096" oculum scan .',
|
|
43718
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting#memory"
|
|
43719
|
+
};
|
|
43720
|
+
}
|
|
43721
|
+
if (msg.includes("enospc") || msg.includes("no space left")) {
|
|
43722
|
+
return {
|
|
43723
|
+
message: "Disk full",
|
|
43724
|
+
suggestion: "Free up disk space and try again.",
|
|
43725
|
+
category: "file",
|
|
43726
|
+
errorCode: "OCU-E009"
|
|
43426
43727
|
};
|
|
43427
43728
|
}
|
|
43428
|
-
return {
|
|
43729
|
+
return {
|
|
43730
|
+
message: error.message,
|
|
43731
|
+
category: "unknown",
|
|
43732
|
+
errorCode: "OCU-E000",
|
|
43733
|
+
learnMoreUrl: "https://oculum.dev/docs/troubleshooting"
|
|
43734
|
+
};
|
|
43429
43735
|
}
|
|
43430
43736
|
function formatError(enhanced) {
|
|
43431
|
-
|
|
43737
|
+
const icon = getErrorIcon(enhanced.category);
|
|
43738
|
+
const lines = [];
|
|
43739
|
+
if (enhanced.errorCode) {
|
|
43740
|
+
lines.push(source_default.red.bold(`${icon} ${enhanced.errorCode} ${enhanced.message}`));
|
|
43741
|
+
} else {
|
|
43742
|
+
lines.push(source_default.red.bold(`${icon} ${enhanced.message}`));
|
|
43743
|
+
}
|
|
43744
|
+
lines.push("");
|
|
43432
43745
|
if (enhanced.suggestion) {
|
|
43433
|
-
|
|
43746
|
+
lines.push(source_default.white(enhanced.suggestion));
|
|
43434
43747
|
}
|
|
43435
43748
|
if (enhanced.details) {
|
|
43436
|
-
|
|
43749
|
+
lines.push(source_default.dim(enhanced.details));
|
|
43750
|
+
}
|
|
43751
|
+
if (enhanced.quickFix) {
|
|
43752
|
+
lines.push("");
|
|
43753
|
+
lines.push(source_default.bold("Quick fix:"));
|
|
43754
|
+
lines.push(source_default.cyan(` $ ${enhanced.quickFix}`));
|
|
43437
43755
|
}
|
|
43438
43756
|
if (enhanced.recoveryActions && enhanced.recoveryActions.length > 0) {
|
|
43439
|
-
|
|
43757
|
+
lines.push("");
|
|
43758
|
+
lines.push(source_default.dim("Or try:"));
|
|
43440
43759
|
for (const action of enhanced.recoveryActions) {
|
|
43441
43760
|
if (action.command) {
|
|
43442
|
-
|
|
43761
|
+
lines.push(source_default.dim(" - ") + source_default.white(action.label + ": ") + source_default.cyan(action.command));
|
|
43443
43762
|
} else {
|
|
43444
|
-
|
|
43763
|
+
lines.push(source_default.dim(" - ") + source_default.white(action.label));
|
|
43445
43764
|
}
|
|
43446
43765
|
}
|
|
43447
43766
|
}
|
|
43448
|
-
|
|
43767
|
+
if (enhanced.learnMoreUrl) {
|
|
43768
|
+
lines.push("");
|
|
43769
|
+
lines.push(source_default.dim("Need help? ") + source_default.cyan.underline(enhanced.learnMoreUrl));
|
|
43770
|
+
}
|
|
43771
|
+
return lines.join("\n");
|
|
43772
|
+
}
|
|
43773
|
+
function getErrorIcon(category) {
|
|
43774
|
+
switch (category) {
|
|
43775
|
+
case "network":
|
|
43776
|
+
return "\u{1F310}";
|
|
43777
|
+
case "auth":
|
|
43778
|
+
return "\u{1F510}";
|
|
43779
|
+
case "file":
|
|
43780
|
+
return "\u{1F4C1}";
|
|
43781
|
+
case "scan":
|
|
43782
|
+
return "\u{1F50D}";
|
|
43783
|
+
case "server":
|
|
43784
|
+
return "\u{1F5A5}\uFE0F";
|
|
43785
|
+
default:
|
|
43786
|
+
return "\u274C";
|
|
43787
|
+
}
|
|
43449
43788
|
}
|
|
43450
43789
|
|
|
43451
43790
|
// src/utils/project-config.ts
|
|
@@ -43863,6 +44202,13 @@ async function runScanOnce(targetPath, options) {
|
|
|
43863
44202
|
const config = getConfig();
|
|
43864
44203
|
const noColor = options.color === false;
|
|
43865
44204
|
const cancellationToken = (0, import_scanner.createCancellationToken)();
|
|
44205
|
+
if ((options.depth === "validated" || options.depth === "deep") && isAuthenticated()) {
|
|
44206
|
+
try {
|
|
44207
|
+
const verified = await verifyApiKey(config.apiKey);
|
|
44208
|
+
syncAuthFromVerification(verified);
|
|
44209
|
+
} catch {
|
|
44210
|
+
}
|
|
44211
|
+
}
|
|
43866
44212
|
if ((options.depth === "validated" || options.depth === "deep") && !isAuthenticated()) {
|
|
43867
44213
|
if (!options.quiet) {
|
|
43868
44214
|
console.log("");
|
|
@@ -43946,7 +44292,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
43946
44292
|
}
|
|
43947
44293
|
try {
|
|
43948
44294
|
spinner.start("Starting scan...");
|
|
43949
|
-
const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
|
|
44295
|
+
const hasLocalAI = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
|
|
43950
44296
|
if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
|
|
43951
44297
|
spinner.text = `Backend ${options.depth} scan analyzing ${files.length} files...`;
|
|
43952
44298
|
result = await callBackendAPI(
|
|
@@ -44066,26 +44412,37 @@ async function runScan(targetPath, cliOptions) {
|
|
|
44066
44412
|
}
|
|
44067
44413
|
}
|
|
44068
44414
|
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", `
|
|
44415
|
+
Scan Modes:
|
|
44416
|
+
cheap Free Fast pattern matching, runs locally
|
|
44417
|
+
Best for: Quick checks, CI/CD pipelines
|
|
44418
|
+
|
|
44419
|
+
validated ~$0.03 AI validates findings, ~70% fewer false positives
|
|
44420
|
+
Best for: Pre-commit checks, PR reviews
|
|
44421
|
+
|
|
44422
|
+
deep ~$0.10 Full semantic analysis with AI reasoning
|
|
44423
|
+
Best for: Security audits, release checks
|
|
44424
|
+
|
|
44069
44425
|
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
|
-
|
|
44426
|
+
$ oculum scan . Scan current directory (free)
|
|
44427
|
+
$ oculum scan . -d validated AI-validated scan
|
|
44428
|
+
$ oculum scan ./src --fail-on high Fail CI on high severity findings
|
|
44429
|
+
$ oculum scan . -f sarif -o report Export for GitHub Code Scanning
|
|
44430
|
+
$ oculum scan . --incremental Only scan changed files (git)
|
|
44431
|
+
$ oculum scan . --diff origin/main Compare against base branch
|
|
44432
|
+
$ oculum scan . -q Quiet mode for CI/CD
|
|
44433
|
+
|
|
44434
|
+
Configuration:
|
|
44435
|
+
Create oculum.config.json in your project:
|
|
44436
|
+
{
|
|
44437
|
+
"depth": "validated",
|
|
44438
|
+
"failOn": "high",
|
|
44439
|
+
"ignore": ["**/test/**"]
|
|
44440
|
+
}
|
|
44441
|
+
|
|
44442
|
+
More Help:
|
|
44443
|
+
$ oculum help scan-modes Detailed mode comparison
|
|
44444
|
+
$ oculum help ci-setup CI/CD integration examples
|
|
44445
|
+
$ oculum help config Full configuration options
|
|
44089
44446
|
`).action(runScan);
|
|
44090
44447
|
|
|
44091
44448
|
// src/commands/auth.ts
|
|
@@ -44211,7 +44568,10 @@ async function status() {
|
|
|
44211
44568
|
spinner.succeed(" Authenticated");
|
|
44212
44569
|
console.log("");
|
|
44213
44570
|
const email = result.email || config.email || "unknown";
|
|
44214
|
-
const tier = result.tier ||
|
|
44571
|
+
const tier = result.tier || "free";
|
|
44572
|
+
if (tier !== config.tier || email !== config.email) {
|
|
44573
|
+
setAuthCredentials(config.apiKey, email, tier);
|
|
44574
|
+
}
|
|
44215
44575
|
const tierBadge = tier === "pro" ? source_default.bgBlue.white(" PRO ") : tier === "enterprise" ? source_default.bgMagenta.white(" ENTERPRISE ") : source_default.bgGray.white(" FREE ");
|
|
44216
44576
|
console.log(source_default.dim(" Email: ") + source_default.white(email));
|
|
44217
44577
|
console.log(source_default.dim(" Plan: ") + tierBadge);
|
|
@@ -44788,10 +45148,10 @@ var foreach = (val, fn) => {
|
|
|
44788
45148
|
fn(val);
|
|
44789
45149
|
}
|
|
44790
45150
|
};
|
|
44791
|
-
var addAndConvert = (
|
|
44792
|
-
let container =
|
|
45151
|
+
var addAndConvert = (main3, prop, item) => {
|
|
45152
|
+
let container = main3[prop];
|
|
44793
45153
|
if (!(container instanceof Set)) {
|
|
44794
|
-
|
|
45154
|
+
main3[prop] = container = /* @__PURE__ */ new Set([container]);
|
|
44795
45155
|
}
|
|
44796
45156
|
container.add(item);
|
|
44797
45157
|
};
|
|
@@ -44803,12 +45163,12 @@ var clearItem = (cont) => (key) => {
|
|
|
44803
45163
|
delete cont[key];
|
|
44804
45164
|
}
|
|
44805
45165
|
};
|
|
44806
|
-
var delFromSet = (
|
|
44807
|
-
const container =
|
|
45166
|
+
var delFromSet = (main3, prop, item) => {
|
|
45167
|
+
const container = main3[prop];
|
|
44808
45168
|
if (container instanceof Set) {
|
|
44809
45169
|
container.delete(item);
|
|
44810
45170
|
} else if (container === item) {
|
|
44811
|
-
delete
|
|
45171
|
+
delete main3[prop];
|
|
44812
45172
|
}
|
|
44813
45173
|
};
|
|
44814
45174
|
var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
|
|
@@ -46853,6 +47213,7 @@ var CONFIG_DIR3 = (0, import_path6.join)((0, import_os4.homedir)(), ".oculum");
|
|
|
46853
47213
|
var STATE_FILE = (0, import_path6.join)(CONFIG_DIR3, "state.json");
|
|
46854
47214
|
var DEFAULT_STATE = {
|
|
46855
47215
|
onboardingComplete: false,
|
|
47216
|
+
setupWizardComplete: false,
|
|
46856
47217
|
totalScans: 0,
|
|
46857
47218
|
welcomeShown: false
|
|
46858
47219
|
};
|
|
@@ -46881,7 +47242,7 @@ function updateUserState(updates) {
|
|
|
46881
47242
|
}
|
|
46882
47243
|
function isFirstTimeUser() {
|
|
46883
47244
|
const state = getUserState();
|
|
46884
|
-
return !state.onboardingComplete && state.totalScans === 0;
|
|
47245
|
+
return !state.setupWizardComplete && !state.onboardingComplete && state.totalScans === 0;
|
|
46885
47246
|
}
|
|
46886
47247
|
function shouldShowWelcome() {
|
|
46887
47248
|
const state = getUserState();
|
|
@@ -47797,6 +48158,9 @@ async function runAuthFlow() {
|
|
|
47797
48158
|
const verified = await verifyApiKey(config.apiKey);
|
|
47798
48159
|
if (verified.valid && verified.tier) {
|
|
47799
48160
|
currentTier = verified.tier;
|
|
48161
|
+
if (currentTier !== config.tier || verified.email && verified.email !== config.email) {
|
|
48162
|
+
setAuthCredentials(config.apiKey, verified.email || config.email, currentTier);
|
|
48163
|
+
}
|
|
47800
48164
|
}
|
|
47801
48165
|
} catch {
|
|
47802
48166
|
}
|
|
@@ -47826,9 +48190,14 @@ async function runAuthFlow() {
|
|
|
47826
48190
|
s.stop("Stored credentials are invalid or expired.");
|
|
47827
48191
|
continue;
|
|
47828
48192
|
}
|
|
48193
|
+
const email = verified.email || getConfig().email || "unknown";
|
|
48194
|
+
const tier = verified.tier || "free";
|
|
48195
|
+
if (tier !== getConfig().tier || email !== getConfig().email) {
|
|
48196
|
+
setAuthCredentials(getConfig().apiKey, email, tier);
|
|
48197
|
+
}
|
|
47829
48198
|
s.stop("Authenticated");
|
|
47830
|
-
M2.info(`Email: ${
|
|
47831
|
-
M2.info(`Tier: ${
|
|
48199
|
+
M2.info(`Email: ${email}`);
|
|
48200
|
+
M2.info(`Tier: ${tier}`);
|
|
47832
48201
|
} catch (err) {
|
|
47833
48202
|
s.stop("Verification failed");
|
|
47834
48203
|
M2.error(String(err));
|
|
@@ -48397,9 +48766,251 @@ function getTimeAgo2(date) {
|
|
|
48397
48766
|
}
|
|
48398
48767
|
var usageCommand = new Command("usage").description("Show current usage and quota").option("--json", "Output as JSON").action(usage);
|
|
48399
48768
|
|
|
48769
|
+
// src/commands/help.ts
|
|
48770
|
+
var TOPICS = {
|
|
48771
|
+
"scan-modes": showScanModes,
|
|
48772
|
+
"ci-setup": showCISetup,
|
|
48773
|
+
"config": showConfig,
|
|
48774
|
+
"troubleshooting": showTroubleshooting
|
|
48775
|
+
};
|
|
48776
|
+
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) => {
|
|
48777
|
+
if (!topic) {
|
|
48778
|
+
showTopicList();
|
|
48779
|
+
return;
|
|
48780
|
+
}
|
|
48781
|
+
const normalizedTopic = topic.toLowerCase().replace(/_/g, "-");
|
|
48782
|
+
if (normalizedTopic in TOPICS) {
|
|
48783
|
+
TOPICS[normalizedTopic]();
|
|
48784
|
+
} else {
|
|
48785
|
+
console.log(source_default.red(`Unknown topic: ${topic}
|
|
48786
|
+
`));
|
|
48787
|
+
showTopicList();
|
|
48788
|
+
}
|
|
48789
|
+
});
|
|
48790
|
+
function showTopicList() {
|
|
48791
|
+
console.log(source_default.bold("\nOculum Help Topics\n"));
|
|
48792
|
+
console.log(source_default.dim("\u2500".repeat(50)));
|
|
48793
|
+
console.log();
|
|
48794
|
+
console.log(source_default.cyan(" scan-modes ") + source_default.white("Compare cheap, validated, and deep scans"));
|
|
48795
|
+
console.log(source_default.cyan(" ci-setup ") + source_default.white("GitHub Actions and GitLab CI examples"));
|
|
48796
|
+
console.log(source_default.cyan(" config ") + source_default.white("Configuration file documentation"));
|
|
48797
|
+
console.log(source_default.cyan(" troubleshooting ") + source_default.white("Common issues and solutions"));
|
|
48798
|
+
console.log();
|
|
48799
|
+
console.log(source_default.dim("\u2500".repeat(50)));
|
|
48800
|
+
console.log(source_default.dim("\nUsage: oculum help <topic>"));
|
|
48801
|
+
console.log(source_default.dim("Example: oculum help scan-modes\n"));
|
|
48802
|
+
}
|
|
48803
|
+
function showScanModes() {
|
|
48804
|
+
console.log(source_default.bold("\nScan Modes Comparison\n"));
|
|
48805
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48806
|
+
console.log(source_default.green.bold(" CHEAP (Quick Scan)"));
|
|
48807
|
+
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"));
|
|
48808
|
+
console.log(source_default.white(" Cost: ") + source_default.green("Free"));
|
|
48809
|
+
console.log(source_default.white(" Speed: ") + source_default.white("~1000 files/second"));
|
|
48810
|
+
console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + entropy analysis"));
|
|
48811
|
+
console.log(source_default.white(" Best for: ") + source_default.dim("Quick checks, CI/CD pipelines"));
|
|
48812
|
+
console.log(source_default.white(" Limitation: ") + source_default.dim("May have more false positives\n"));
|
|
48813
|
+
console.log(source_default.blue.bold(" VALIDATED"));
|
|
48814
|
+
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"));
|
|
48815
|
+
console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.03 per 300 files"));
|
|
48816
|
+
console.log(source_default.white(" Speed: ") + source_default.white("~30 seconds for 300 files"));
|
|
48817
|
+
console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + AI validation"));
|
|
48818
|
+
console.log(source_default.white(" Best for: ") + source_default.dim("Pre-commit checks, PR reviews"));
|
|
48819
|
+
console.log(source_default.white(" Benefit: ") + source_default.dim("~70% fewer false positives\n"));
|
|
48820
|
+
console.log(source_default.magenta.bold(" DEEP"));
|
|
48821
|
+
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"));
|
|
48822
|
+
console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.10 per 300 files"));
|
|
48823
|
+
console.log(source_default.white(" Speed: ") + source_default.white("~60 seconds for 300 files"));
|
|
48824
|
+
console.log(source_default.white(" How: ") + source_default.dim("Full semantic analysis with AI reasoning"));
|
|
48825
|
+
console.log(source_default.white(" Best for: ") + source_default.dim("Security audits, release checks"));
|
|
48826
|
+
console.log(source_default.white(" Benefit: ") + source_default.dim("Deepest analysis, remediation advice\n"));
|
|
48827
|
+
console.log(source_default.dim("\u2500".repeat(60)));
|
|
48828
|
+
console.log(source_default.bold("\nUsage Examples:\n"));
|
|
48829
|
+
console.log(source_default.dim(" $ oculum scan . ") + source_default.white("# Quick scan (free)"));
|
|
48830
|
+
console.log(source_default.dim(" $ oculum scan . -d validated ") + source_default.white("# AI-validated scan"));
|
|
48831
|
+
console.log(source_default.dim(" $ oculum scan . -d deep ") + source_default.white("# Deep semantic analysis"));
|
|
48832
|
+
console.log();
|
|
48833
|
+
}
|
|
48834
|
+
function showCISetup() {
|
|
48835
|
+
console.log(source_default.bold("\nCI/CD Integration\n"));
|
|
48836
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48837
|
+
console.log(source_default.bold(" GitHub Actions\n"));
|
|
48838
|
+
console.log(source_default.dim(" Create .github/workflows/oculum.yml:\n"));
|
|
48839
|
+
console.log(source_default.cyan(` name: Security Scan
|
|
48840
|
+
on: [push, pull_request]
|
|
48841
|
+
jobs:
|
|
48842
|
+
scan:
|
|
48843
|
+
runs-on: ubuntu-latest
|
|
48844
|
+
steps:
|
|
48845
|
+
- uses: actions/checkout@v4
|
|
48846
|
+
- uses: actions/setup-node@v4
|
|
48847
|
+
with:
|
|
48848
|
+
node-version: '20'
|
|
48849
|
+
- run: npm install -g oculum
|
|
48850
|
+
- run: oculum scan . --fail-on high
|
|
48851
|
+
env:
|
|
48852
|
+
OCULUM_API_KEY: \${{ secrets.OCULUM_API_KEY }}`));
|
|
48853
|
+
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"));
|
|
48854
|
+
console.log(source_default.bold(" GitLab CI\n"));
|
|
48855
|
+
console.log(source_default.dim(" Add to .gitlab-ci.yml:\n"));
|
|
48856
|
+
console.log(source_default.cyan(` security-scan:
|
|
48857
|
+
image: node:20
|
|
48858
|
+
script:
|
|
48859
|
+
- npm install -g oculum
|
|
48860
|
+
- oculum scan . --fail-on high
|
|
48861
|
+
variables:
|
|
48862
|
+
OCULUM_API_KEY: $OCULUM_API_KEY`));
|
|
48863
|
+
console.log(source_default.dim("\n\u2500".repeat(60)));
|
|
48864
|
+
console.log(source_default.bold("\nTips:\n"));
|
|
48865
|
+
console.log(source_default.white(" - Use ") + source_default.cyan("--fail-on high") + source_default.white(" to fail builds on high/critical findings"));
|
|
48866
|
+
console.log(source_default.white(" - Use ") + source_default.cyan("--format sarif") + source_default.white(" for GitHub Code Scanning integration"));
|
|
48867
|
+
console.log(source_default.white(" - Use ") + source_default.cyan("-d cheap") + source_default.white(" for fastest scans (free, no API key needed)"));
|
|
48868
|
+
console.log(source_default.white(" - Store API key in secrets, not in code\n"));
|
|
48869
|
+
}
|
|
48870
|
+
function showConfig() {
|
|
48871
|
+
console.log(source_default.bold("\nConfiguration Files\n"));
|
|
48872
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48873
|
+
console.log(source_default.bold(" Project Config (oculum.config.json)\n"));
|
|
48874
|
+
console.log(source_default.dim(" Create in your project root:\n"));
|
|
48875
|
+
console.log(source_default.cyan(` {
|
|
48876
|
+
"depth": "validated",
|
|
48877
|
+
"failOn": "high",
|
|
48878
|
+
"format": "terminal",
|
|
48879
|
+
"ignore": [
|
|
48880
|
+
"**/node_modules/**",
|
|
48881
|
+
"**/dist/**",
|
|
48882
|
+
"**/*.test.ts"
|
|
48883
|
+
],
|
|
48884
|
+
"include": [
|
|
48885
|
+
"src/**"
|
|
48886
|
+
],
|
|
48887
|
+
"watch": {
|
|
48888
|
+
"debounce": 500,
|
|
48889
|
+
"clear": true
|
|
48890
|
+
}
|
|
48891
|
+
}`));
|
|
48892
|
+
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"));
|
|
48893
|
+
console.log(source_default.bold(" Config Options\n"));
|
|
48894
|
+
console.log(source_default.white(" depth ") + source_default.dim("Scan depth: cheap | validated | deep"));
|
|
48895
|
+
console.log(source_default.white(" failOn ") + source_default.dim("Exit code 1 on: critical | high | medium | low | none"));
|
|
48896
|
+
console.log(source_default.white(" format ") + source_default.dim("Output format: terminal | json | sarif | markdown"));
|
|
48897
|
+
console.log(source_default.white(" output ") + source_default.dim("Output file path"));
|
|
48898
|
+
console.log(source_default.white(" quiet ") + source_default.dim("Only show findings (boolean)"));
|
|
48899
|
+
console.log(source_default.white(" ignore ") + source_default.dim("Glob patterns to exclude"));
|
|
48900
|
+
console.log(source_default.white(" include ") + source_default.dim("Glob patterns to include"));
|
|
48901
|
+
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"));
|
|
48902
|
+
console.log(source_default.bold(" Config File Priority\n"));
|
|
48903
|
+
console.log(source_default.dim(" CLI flags > Project config > User config > Defaults\n"));
|
|
48904
|
+
console.log(source_default.white(" Supported filenames:"));
|
|
48905
|
+
console.log(source_default.dim(" - oculum.config.json"));
|
|
48906
|
+
console.log(source_default.dim(" - .oculumrc.json"));
|
|
48907
|
+
console.log(source_default.dim(" - .oculumrc\n"));
|
|
48908
|
+
}
|
|
48909
|
+
function showTroubleshooting() {
|
|
48910
|
+
console.log(source_default.bold("\nTroubleshooting\n"));
|
|
48911
|
+
console.log(source_default.dim("\u2500".repeat(60) + "\n"));
|
|
48912
|
+
console.log(source_default.red.bold(" Authentication Errors\n"));
|
|
48913
|
+
console.log(source_default.white(' "Authentication required"'));
|
|
48914
|
+
console.log(source_default.dim(" \u2192 Run `oculum login` to authenticate"));
|
|
48915
|
+
console.log(source_default.dim(" \u2192 Or use `--depth cheap` for free local scans\n"));
|
|
48916
|
+
console.log(source_default.white(' "API key invalid or expired"'));
|
|
48917
|
+
console.log(source_default.dim(" \u2192 Run `oculum login` to re-authenticate"));
|
|
48918
|
+
console.log(source_default.dim(" \u2192 Check key at https://oculum.dev/dashboard/api-keys\n"));
|
|
48919
|
+
console.log(source_default.white(' "Insufficient tier"'));
|
|
48920
|
+
console.log(source_default.dim(" \u2192 Validated/deep scans require Pro subscription"));
|
|
48921
|
+
console.log(source_default.dim(" \u2192 Visit https://oculum.dev/billing to upgrade\n"));
|
|
48922
|
+
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"));
|
|
48923
|
+
console.log(source_default.yellow.bold(" Network Errors\n"));
|
|
48924
|
+
console.log(source_default.white(' "Connection refused" / "Network error"'));
|
|
48925
|
+
console.log(source_default.dim(" \u2192 Check your internet connection"));
|
|
48926
|
+
console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
|
|
48927
|
+
console.log(source_default.dim(" \u2192 Check https://status.oculum.dev\n"));
|
|
48928
|
+
console.log(source_default.white(' "SSL certificate error"'));
|
|
48929
|
+
console.log(source_default.dim(" \u2192 Corporate proxy may be intercepting HTTPS"));
|
|
48930
|
+
console.log(source_default.dim(" \u2192 Use `--depth cheap` for offline scans"));
|
|
48931
|
+
console.log(source_default.dim(" \u2192 Contact IT for proxy configuration\n"));
|
|
48932
|
+
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"));
|
|
48933
|
+
console.log(source_default.blue.bold(" Scan Issues\n"));
|
|
48934
|
+
console.log(source_default.white(' "No scannable files found"'));
|
|
48935
|
+
console.log(source_default.dim(" \u2192 Check path exists: ls <path>"));
|
|
48936
|
+
console.log(source_default.dim(" \u2192 Ensure files have supported extensions"));
|
|
48937
|
+
console.log(source_default.dim(" \u2192 Check .gitignore isn't excluding files\n"));
|
|
48938
|
+
console.log(source_default.white(' "Request timed out"'));
|
|
48939
|
+
console.log(source_default.dim(" \u2192 Try scanning fewer files"));
|
|
48940
|
+
console.log(source_default.dim(" \u2192 Use `--depth cheap` for large codebases"));
|
|
48941
|
+
console.log(source_default.dim(" \u2192 Split into multiple scans\n"));
|
|
48942
|
+
console.log(source_default.white(' "Out of memory"'));
|
|
48943
|
+
console.log(source_default.dim(" \u2192 Scan a smaller directory"));
|
|
48944
|
+
console.log(source_default.dim(' \u2192 Increase Node memory: NODE_OPTIONS="--max-old-space-size=4096"\n'));
|
|
48945
|
+
console.log(source_default.dim("\u2500".repeat(60)));
|
|
48946
|
+
console.log(source_default.bold("\nStill stuck?\n"));
|
|
48947
|
+
console.log(source_default.white(" - Check docs: ") + source_default.cyan("https://oculum.dev/docs"));
|
|
48948
|
+
console.log(source_default.white(" - Report issues: ") + source_default.cyan("https://github.com/oculum-dev/oculum/issues"));
|
|
48949
|
+
console.log(source_default.white(" - Get support: ") + source_default.cyan("support@oculum.dev\n"));
|
|
48950
|
+
}
|
|
48951
|
+
|
|
48952
|
+
// src/utils/ci-detect.ts
|
|
48953
|
+
var CI_ENV_VARS = [
|
|
48954
|
+
"CI",
|
|
48955
|
+
// Generic CI flag (GitHub Actions, GitLab CI, etc.)
|
|
48956
|
+
"GITHUB_ACTIONS",
|
|
48957
|
+
// GitHub Actions
|
|
48958
|
+
"GITLAB_CI",
|
|
48959
|
+
// GitLab CI
|
|
48960
|
+
"JENKINS_URL",
|
|
48961
|
+
// Jenkins
|
|
48962
|
+
"CIRCLECI",
|
|
48963
|
+
// CircleCI
|
|
48964
|
+
"TRAVIS",
|
|
48965
|
+
// Travis CI
|
|
48966
|
+
"BUILDKITE",
|
|
48967
|
+
// Buildkite
|
|
48968
|
+
"DRONE",
|
|
48969
|
+
// Drone CI
|
|
48970
|
+
"TEAMCITY_VERSION",
|
|
48971
|
+
// TeamCity
|
|
48972
|
+
"BITBUCKET_COMMIT",
|
|
48973
|
+
// Bitbucket Pipelines
|
|
48974
|
+
"AZURE_HTTP_USER_AGENT",
|
|
48975
|
+
// Azure Pipelines
|
|
48976
|
+
"TF_BUILD",
|
|
48977
|
+
// Azure Pipelines (alternative)
|
|
48978
|
+
"CODEBUILD_BUILD_ID",
|
|
48979
|
+
// AWS CodeBuild
|
|
48980
|
+
"APPVEYOR",
|
|
48981
|
+
// AppVeyor
|
|
48982
|
+
"SEMAPHORE",
|
|
48983
|
+
// Semaphore CI
|
|
48984
|
+
"HEROKU_TEST_RUN_ID",
|
|
48985
|
+
// Heroku CI
|
|
48986
|
+
"RENDER",
|
|
48987
|
+
// Render
|
|
48988
|
+
"VERCEL",
|
|
48989
|
+
// Vercel
|
|
48990
|
+
"NETLIFY",
|
|
48991
|
+
// Netlify
|
|
48992
|
+
"CF_PAGES",
|
|
48993
|
+
// Cloudflare Pages
|
|
48994
|
+
"RAILWAY_ENVIRONMENT"
|
|
48995
|
+
// Railway
|
|
48996
|
+
];
|
|
48997
|
+
function isCI() {
|
|
48998
|
+
return CI_ENV_VARS.some((envVar) => !!process.env[envVar]);
|
|
48999
|
+
}
|
|
49000
|
+
function isInteractiveTerminal() {
|
|
49001
|
+
return !!(process.stdin.isTTY && !isCI());
|
|
49002
|
+
}
|
|
49003
|
+
|
|
48400
49004
|
// src/index.ts
|
|
49005
|
+
function shouldRunUI() {
|
|
49006
|
+
const programName = process.argv[1] || "";
|
|
49007
|
+
const args = process.argv.slice(2);
|
|
49008
|
+
const isOcAlias = programName.endsWith("oc") && !programName.endsWith("oculum");
|
|
49009
|
+
const isUICommand = args.length === 0 || args.length === 1 && args[0] === "ui";
|
|
49010
|
+
return isOcAlias || isUICommand;
|
|
49011
|
+
}
|
|
48401
49012
|
var program2 = new Command();
|
|
48402
|
-
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.
|
|
49013
|
+
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.9").addHelpText("after", `
|
|
48403
49014
|
Quick Start:
|
|
48404
49015
|
$ oculum scan . Scan current directory (free)
|
|
48405
49016
|
$ oculum ui Interactive mode with guided setup
|
|
@@ -48412,11 +49023,13 @@ Common Commands:
|
|
|
48412
49023
|
login Authenticate with Oculum
|
|
48413
49024
|
status Check authentication status
|
|
48414
49025
|
usage View credits and quota
|
|
49026
|
+
help [topic] Get help on specific topics
|
|
48415
49027
|
|
|
48416
49028
|
Learn More:
|
|
48417
49029
|
$ oculum scan --help Detailed scan options
|
|
49030
|
+
$ oculum help scan-modes Compare scan depth options
|
|
48418
49031
|
$ oculum --help All available commands
|
|
48419
|
-
|
|
49032
|
+
|
|
48420
49033
|
Documentation: https://oculum.dev/docs
|
|
48421
49034
|
`);
|
|
48422
49035
|
program2.addCommand(scanCommand, { isDefault: true });
|
|
@@ -48427,7 +49040,17 @@ program2.addCommand(upgradeCommand);
|
|
|
48427
49040
|
program2.addCommand(usageCommand);
|
|
48428
49041
|
program2.addCommand(watchCommand);
|
|
48429
49042
|
program2.addCommand(uiCommand);
|
|
48430
|
-
program2.
|
|
49043
|
+
program2.addCommand(helpCommand);
|
|
49044
|
+
async function main2() {
|
|
49045
|
+
const interactive = isInteractiveTerminal();
|
|
49046
|
+
if (interactive && shouldRunUI()) {
|
|
49047
|
+
process.argv = ["node", "oculum", "ui"];
|
|
49048
|
+
program2.parse();
|
|
49049
|
+
return;
|
|
49050
|
+
}
|
|
49051
|
+
program2.parse();
|
|
49052
|
+
}
|
|
49053
|
+
main2();
|
|
48431
49054
|
/*! Bundled license information:
|
|
48432
49055
|
|
|
48433
49056
|
chokidar/esm/index.js:
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oculum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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": {
|
|
7
|
-
"oculum": "./dist/index.js"
|
|
7
|
+
"oculum": "./dist/index.js",
|
|
8
|
+
"oc": "./dist/index.js"
|
|
8
9
|
},
|
|
9
10
|
"author": "Felix Westin <felix.lwestin@gmail.com>",
|
|
10
11
|
"license": "MIT",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"url": "https://github.com/flexipie/oculum/issues"
|
|
19
20
|
},
|
|
20
21
|
"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.
|
|
22
|
+
"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.9\"'",
|
|
22
23
|
"dev": "npm run build -- --watch",
|
|
23
24
|
"test": "echo \"No tests configured yet\"",
|
|
24
25
|
"lint": "eslint src/"
|