@oculum/cli 1.0.2 → 1.0.4
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 +181 -126
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -14406,6 +14406,44 @@ var require_layer1 = __commonJS({
|
|
|
14406
14406
|
var weak_crypto_1 = require_weak_crypto();
|
|
14407
14407
|
var tiers_1 = require_tiers();
|
|
14408
14408
|
var path_exclusions_1 = require_path_exclusions();
|
|
14409
|
+
function processFileLayer1(file) {
|
|
14410
|
+
const stats = {
|
|
14411
|
+
known_secrets: 0,
|
|
14412
|
+
weak_crypto: 0,
|
|
14413
|
+
sensitive_urls: 0,
|
|
14414
|
+
entropy: 0,
|
|
14415
|
+
config_audit: 0,
|
|
14416
|
+
file_flags: 0,
|
|
14417
|
+
ai_comments: 0
|
|
14418
|
+
};
|
|
14419
|
+
const entropyFindings = (0, entropy_1.detectHighEntropyStrings)(file.content, file.path);
|
|
14420
|
+
const patternFindings = (0, patterns_1.detectKnownPatterns)(file.content, file.path);
|
|
14421
|
+
const configFindings = (0, config_audit_1.auditConfiguration)(file.content, file.path);
|
|
14422
|
+
const fileFlags = (0, file_flags_1.detectDangerousFiles)(file.content, file.path);
|
|
14423
|
+
const commentFindings = (0, comments_1.detectAICommentPatterns)(file.content, file.path);
|
|
14424
|
+
const urlFindings = (0, urls_1.detectSensitiveURLs)(file.content, file.path);
|
|
14425
|
+
const cryptoFindings = (0, weak_crypto_1.detectWeakCrypto)(file.content, file.path);
|
|
14426
|
+
stats.entropy = entropyFindings.length;
|
|
14427
|
+
stats.known_secrets = patternFindings.length;
|
|
14428
|
+
stats.config_audit = configFindings.length;
|
|
14429
|
+
stats.file_flags = fileFlags.length;
|
|
14430
|
+
stats.ai_comments = commentFindings.length;
|
|
14431
|
+
stats.sensitive_urls = urlFindings.length;
|
|
14432
|
+
stats.weak_crypto = cryptoFindings.length;
|
|
14433
|
+
return {
|
|
14434
|
+
findings: [
|
|
14435
|
+
...entropyFindings,
|
|
14436
|
+
...patternFindings,
|
|
14437
|
+
...configFindings,
|
|
14438
|
+
...fileFlags,
|
|
14439
|
+
...commentFindings,
|
|
14440
|
+
...urlFindings,
|
|
14441
|
+
...cryptoFindings
|
|
14442
|
+
],
|
|
14443
|
+
stats
|
|
14444
|
+
};
|
|
14445
|
+
}
|
|
14446
|
+
var LAYER1_PARALLEL_BATCH_SIZE = 50;
|
|
14409
14447
|
async function runLayer1Scan(files) {
|
|
14410
14448
|
const startTime = Date.now();
|
|
14411
14449
|
const vulnerabilities = [];
|
|
@@ -14418,50 +14456,24 @@ var require_layer1 = __commonJS({
|
|
|
14418
14456
|
file_flags: 0,
|
|
14419
14457
|
ai_comments: 0
|
|
14420
14458
|
};
|
|
14421
|
-
for (
|
|
14422
|
-
const
|
|
14423
|
-
const
|
|
14424
|
-
const
|
|
14425
|
-
|
|
14426
|
-
|
|
14427
|
-
|
|
14428
|
-
|
|
14429
|
-
|
|
14430
|
-
rawStats.known_secrets += patternFindings.length;
|
|
14431
|
-
rawStats.config_audit += configFindings.length;
|
|
14432
|
-
rawStats.file_flags += fileFlags.length;
|
|
14433
|
-
rawStats.ai_comments += commentFindings.length;
|
|
14434
|
-
rawStats.sensitive_urls += urlFindings.length;
|
|
14435
|
-
rawStats.weak_crypto += cryptoFindings.length;
|
|
14436
|
-
vulnerabilities.push(...entropyFindings, ...patternFindings, ...configFindings, ...fileFlags, ...commentFindings, ...urlFindings, ...cryptoFindings);
|
|
14459
|
+
for (let i = 0; i < files.length; i += LAYER1_PARALLEL_BATCH_SIZE) {
|
|
14460
|
+
const batch = files.slice(i, i + LAYER1_PARALLEL_BATCH_SIZE);
|
|
14461
|
+
const results = await Promise.all(batch.map((file) => Promise.resolve(processFileLayer1(file))));
|
|
14462
|
+
for (const result of results) {
|
|
14463
|
+
vulnerabilities.push(...result.findings);
|
|
14464
|
+
for (const [key, value] of Object.entries(result.stats)) {
|
|
14465
|
+
rawStats[key] += value;
|
|
14466
|
+
}
|
|
14467
|
+
}
|
|
14437
14468
|
}
|
|
14438
14469
|
const dedupedVulnerabilities = deduplicateFindings(vulnerabilities);
|
|
14439
14470
|
const { kept: uniqueVulnerabilities, suppressed } = (0, path_exclusions_1.filterFindingsByPath)(dedupedVulnerabilities);
|
|
14440
|
-
if (suppressed.length > 0) {
|
|
14441
|
-
const byReason = {};
|
|
14442
|
-
for (const s of suppressed) {
|
|
14443
|
-
const reason = s.reason || "unknown";
|
|
14444
|
-
byReason[reason] = (byReason[reason] || 0) + 1;
|
|
14445
|
-
}
|
|
14446
|
-
console.log(`[Layer 1] Suppressed ${suppressed.length} findings in test/seed/example files:`);
|
|
14447
|
-
for (const [reason, count] of Object.entries(byReason)) {
|
|
14448
|
-
console.log(` - ${reason}: ${count}`);
|
|
14449
|
-
}
|
|
14450
|
-
}
|
|
14451
14471
|
const dedupedStats = {};
|
|
14452
14472
|
for (const vuln of uniqueVulnerabilities) {
|
|
14453
14473
|
const cat = vuln.category;
|
|
14454
14474
|
dedupedStats[cat] = (dedupedStats[cat] || 0) + 1;
|
|
14455
14475
|
}
|
|
14456
14476
|
const tierStats = (0, tiers_1.computeTierStats)(uniqueVulnerabilities.map((v2) => ({ category: v2.category, layer: 1 })));
|
|
14457
|
-
console.log("[Layer 1] Heuristic breakdown (raw findings before dedupe):");
|
|
14458
|
-
for (const [name, count] of Object.entries(rawStats)) {
|
|
14459
|
-
if (count > 0) {
|
|
14460
|
-
const tier = (0, tiers_1.getLayer1DetectorTier)(name);
|
|
14461
|
-
console.log(` - ${name}: ${count} (${tier})`);
|
|
14462
|
-
}
|
|
14463
|
-
}
|
|
14464
|
-
console.log(`[Layer 1] Tier breakdown (after dedupe): ${(0, tiers_1.formatTierStats)(tierStats)}`);
|
|
14465
14477
|
return {
|
|
14466
14478
|
vulnerabilities: uniqueVulnerabilities,
|
|
14467
14479
|
filesScanned: files.length,
|
|
@@ -20677,6 +20689,85 @@ var require_layer2 = __commonJS({
|
|
|
20677
20689
|
var ai_endpoint_protection_1 = require_ai_endpoint_protection();
|
|
20678
20690
|
var ai_schema_validation_1 = require_ai_schema_validation();
|
|
20679
20691
|
var tiers_1 = require_tiers();
|
|
20692
|
+
function processFileLayer2(file, options, authHelperContext) {
|
|
20693
|
+
const stats = {
|
|
20694
|
+
variables: 0,
|
|
20695
|
+
logicGates: 0,
|
|
20696
|
+
dangerousFunctions: 0,
|
|
20697
|
+
riskyImports: 0,
|
|
20698
|
+
authAntipatterns: 0,
|
|
20699
|
+
frameworkIssues: 0,
|
|
20700
|
+
aiFingerprints: 0,
|
|
20701
|
+
dataExposure: 0,
|
|
20702
|
+
byokPatterns: 0,
|
|
20703
|
+
promptHygiene: 0,
|
|
20704
|
+
executionSinks: 0,
|
|
20705
|
+
agentTools: 0,
|
|
20706
|
+
ragSafety: 0,
|
|
20707
|
+
endpointProtection: 0,
|
|
20708
|
+
schemaValidation: 0
|
|
20709
|
+
};
|
|
20710
|
+
if (!isCodeFile(file.path)) {
|
|
20711
|
+
return { findings: [], stats };
|
|
20712
|
+
}
|
|
20713
|
+
const variableFindings = (0, variables_1.detectSensitiveVariables)(file.content, file.path);
|
|
20714
|
+
const logicFindings = (0, logic_gates_1.detectLogicGates)(file.content, file.path);
|
|
20715
|
+
const dangerousFuncFindings = (0, dangerous_functions_1.detectDangerousFunctions)(file.content, file.path);
|
|
20716
|
+
const riskyImportFindings = (0, risky_imports_1.detectRiskyImports)(file.content, file.path);
|
|
20717
|
+
const authFindings = (0, auth_antipatterns_1.detectAuthAntipatterns)(file.content, file.path, {
|
|
20718
|
+
middlewareConfig: options.middlewareConfig,
|
|
20719
|
+
authHelpers: authHelperContext,
|
|
20720
|
+
fileAuthImports: options.fileAuthImports
|
|
20721
|
+
});
|
|
20722
|
+
const frameworkFindings = (0, framework_checks_1.detectFrameworkIssues)(file.content, file.path);
|
|
20723
|
+
const aiFindings = (0, ai_fingerprinting_1.detectAIFingerprints)(file.content, file.path);
|
|
20724
|
+
const dataExposureFindings = (0, data_exposure_1.detectDataExposure)(file.content, file.path);
|
|
20725
|
+
const byokFindings = (0, byok_patterns_1.detectBYOKPatterns)(file.content, file.path, options.middlewareConfig);
|
|
20726
|
+
const promptHygieneFindings = (0, ai_prompt_hygiene_1.detectAIPromptHygiene)(file.content, file.path);
|
|
20727
|
+
const executionSinkFindings = (0, ai_execution_sinks_1.detectAIExecutionSinks)(file.content, file.path);
|
|
20728
|
+
const agentToolFindings = (0, ai_agent_tools_1.detectAIAgentTools)(file.content, file.path);
|
|
20729
|
+
const ragSafetyFindings = (0, ai_rag_safety_1.detectRAGSafetyIssues)(file.content, file.path);
|
|
20730
|
+
const endpointProtectionFindings = (0, ai_endpoint_protection_1.detectAIEndpointProtection)(file.content, file.path, {
|
|
20731
|
+
middlewareConfig: options.middlewareConfig
|
|
20732
|
+
});
|
|
20733
|
+
const schemaValidationFindings = (0, ai_schema_validation_1.detectAISchemaValidation)(file.content, file.path);
|
|
20734
|
+
stats.variables = variableFindings.length;
|
|
20735
|
+
stats.logicGates = logicFindings.length;
|
|
20736
|
+
stats.dangerousFunctions = dangerousFuncFindings.length;
|
|
20737
|
+
stats.riskyImports = riskyImportFindings.length;
|
|
20738
|
+
stats.authAntipatterns = authFindings.length;
|
|
20739
|
+
stats.frameworkIssues = frameworkFindings.length;
|
|
20740
|
+
stats.aiFingerprints = aiFindings.length;
|
|
20741
|
+
stats.dataExposure = dataExposureFindings.length;
|
|
20742
|
+
stats.byokPatterns = byokFindings.length;
|
|
20743
|
+
stats.promptHygiene = promptHygieneFindings.length;
|
|
20744
|
+
stats.executionSinks = executionSinkFindings.length;
|
|
20745
|
+
stats.agentTools = agentToolFindings.length;
|
|
20746
|
+
stats.ragSafety = ragSafetyFindings.length;
|
|
20747
|
+
stats.endpointProtection = endpointProtectionFindings.length;
|
|
20748
|
+
stats.schemaValidation = schemaValidationFindings.length;
|
|
20749
|
+
return {
|
|
20750
|
+
findings: [
|
|
20751
|
+
...variableFindings,
|
|
20752
|
+
...logicFindings,
|
|
20753
|
+
...dangerousFuncFindings,
|
|
20754
|
+
...riskyImportFindings,
|
|
20755
|
+
...authFindings,
|
|
20756
|
+
...frameworkFindings,
|
|
20757
|
+
...aiFindings,
|
|
20758
|
+
...dataExposureFindings,
|
|
20759
|
+
...byokFindings,
|
|
20760
|
+
...promptHygieneFindings,
|
|
20761
|
+
...executionSinkFindings,
|
|
20762
|
+
...agentToolFindings,
|
|
20763
|
+
...ragSafetyFindings,
|
|
20764
|
+
...endpointProtectionFindings,
|
|
20765
|
+
...schemaValidationFindings
|
|
20766
|
+
],
|
|
20767
|
+
stats
|
|
20768
|
+
};
|
|
20769
|
+
}
|
|
20770
|
+
var LAYER2_PARALLEL_BATCH_SIZE = 50;
|
|
20680
20771
|
async function runLayer2Scan(files, options = {}) {
|
|
20681
20772
|
const startTime = Date.now();
|
|
20682
20773
|
const vulnerabilities = [];
|
|
@@ -20693,51 +20784,19 @@ var require_layer2 = __commonJS({
|
|
|
20693
20784
|
promptHygiene: 0,
|
|
20694
20785
|
executionSinks: 0,
|
|
20695
20786
|
agentTools: 0,
|
|
20696
|
-
// M5: New AI-era detectors
|
|
20697
20787
|
ragSafety: 0,
|
|
20698
20788
|
endpointProtection: 0,
|
|
20699
20789
|
schemaValidation: 0
|
|
20700
20790
|
};
|
|
20701
20791
|
const authHelperContext = options.authHelperContext || (0, auth_helper_detector_1.detectAuthHelpers)(files);
|
|
20702
|
-
for (
|
|
20703
|
-
|
|
20704
|
-
|
|
20705
|
-
|
|
20706
|
-
|
|
20707
|
-
const
|
|
20708
|
-
|
|
20709
|
-
|
|
20710
|
-
authHelpers: authHelperContext,
|
|
20711
|
-
fileAuthImports: options.fileAuthImports
|
|
20712
|
-
});
|
|
20713
|
-
const frameworkFindings = (0, framework_checks_1.detectFrameworkIssues)(file.content, file.path);
|
|
20714
|
-
const aiFindings = (0, ai_fingerprinting_1.detectAIFingerprints)(file.content, file.path);
|
|
20715
|
-
const dataExposureFindings = (0, data_exposure_1.detectDataExposure)(file.content, file.path);
|
|
20716
|
-
const byokFindings = (0, byok_patterns_1.detectBYOKPatterns)(file.content, file.path, options.middlewareConfig);
|
|
20717
|
-
const promptHygieneFindings = (0, ai_prompt_hygiene_1.detectAIPromptHygiene)(file.content, file.path);
|
|
20718
|
-
const executionSinkFindings = (0, ai_execution_sinks_1.detectAIExecutionSinks)(file.content, file.path);
|
|
20719
|
-
const agentToolFindings = (0, ai_agent_tools_1.detectAIAgentTools)(file.content, file.path);
|
|
20720
|
-
const ragSafetyFindings = (0, ai_rag_safety_1.detectRAGSafetyIssues)(file.content, file.path);
|
|
20721
|
-
const endpointProtectionFindings = (0, ai_endpoint_protection_1.detectAIEndpointProtection)(file.content, file.path, {
|
|
20722
|
-
middlewareConfig: options.middlewareConfig
|
|
20723
|
-
});
|
|
20724
|
-
const schemaValidationFindings = (0, ai_schema_validation_1.detectAISchemaValidation)(file.content, file.path);
|
|
20725
|
-
stats.variables += variableFindings.length;
|
|
20726
|
-
stats.logicGates += logicFindings.length;
|
|
20727
|
-
stats.dangerousFunctions += dangerousFuncFindings.length;
|
|
20728
|
-
stats.riskyImports += riskyImportFindings.length;
|
|
20729
|
-
stats.authAntipatterns += authFindings.length;
|
|
20730
|
-
stats.frameworkIssues += frameworkFindings.length;
|
|
20731
|
-
stats.aiFingerprints += aiFindings.length;
|
|
20732
|
-
stats.dataExposure += dataExposureFindings.length;
|
|
20733
|
-
stats.byokPatterns += byokFindings.length;
|
|
20734
|
-
stats.promptHygiene += promptHygieneFindings.length;
|
|
20735
|
-
stats.executionSinks += executionSinkFindings.length;
|
|
20736
|
-
stats.agentTools += agentToolFindings.length;
|
|
20737
|
-
stats.ragSafety += ragSafetyFindings.length;
|
|
20738
|
-
stats.endpointProtection += endpointProtectionFindings.length;
|
|
20739
|
-
stats.schemaValidation += schemaValidationFindings.length;
|
|
20740
|
-
vulnerabilities.push(...variableFindings, ...logicFindings, ...dangerousFuncFindings, ...riskyImportFindings, ...authFindings, ...frameworkFindings, ...aiFindings, ...dataExposureFindings, ...byokFindings, ...promptHygieneFindings, ...executionSinkFindings, ...agentToolFindings, ...ragSafetyFindings, ...endpointProtectionFindings, ...schemaValidationFindings);
|
|
20792
|
+
for (let i = 0; i < files.length; i += LAYER2_PARALLEL_BATCH_SIZE) {
|
|
20793
|
+
const batch = files.slice(i, i + LAYER2_PARALLEL_BATCH_SIZE);
|
|
20794
|
+
const results = await Promise.all(batch.map((file) => Promise.resolve(processFileLayer2(file, options, authHelperContext))));
|
|
20795
|
+
for (const result of results) {
|
|
20796
|
+
vulnerabilities.push(...result.findings);
|
|
20797
|
+
for (const [key, value] of Object.entries(result.stats)) {
|
|
20798
|
+
stats[key] += value;
|
|
20799
|
+
}
|
|
20741
20800
|
}
|
|
20742
20801
|
}
|
|
20743
20802
|
const dedupedVulnerabilities = deduplicateFindings(vulnerabilities);
|
|
@@ -20757,16 +20816,6 @@ var require_layer2 = __commonJS({
|
|
|
20757
20816
|
];
|
|
20758
20817
|
}
|
|
20759
20818
|
const { kept: uniqueVulnerabilities, suppressed } = (0, path_exclusions_1.filterFindingsByPath)(dedupedVulnerabilities, Object.keys(exclusionConfig).length > 0 ? exclusionConfig : void 0);
|
|
20760
|
-
if (suppressed.length > 0) {
|
|
20761
|
-
console.log(`[Layer 2] Suppressed ${suppressed.length} findings in test/seed/example files:`);
|
|
20762
|
-
const byReason = /* @__PURE__ */ new Map();
|
|
20763
|
-
for (const { reason } of suppressed) {
|
|
20764
|
-
byReason.set(reason || "unknown", (byReason.get(reason || "unknown") || 0) + 1);
|
|
20765
|
-
}
|
|
20766
|
-
for (const [reason, count] of byReason) {
|
|
20767
|
-
console.log(` - ${reason}: ${count}`);
|
|
20768
|
-
}
|
|
20769
|
-
}
|
|
20770
20819
|
const rawStats = {
|
|
20771
20820
|
sensitive_variables: stats.variables,
|
|
20772
20821
|
logic_gates: stats.logicGates,
|
|
@@ -20813,15 +20862,6 @@ var require_layer2 = __commonJS({
|
|
|
20813
20862
|
ai_endpoint_protection: "ai_endpoint_protection",
|
|
20814
20863
|
ai_schema_validation: "ai_schema_validation"
|
|
20815
20864
|
};
|
|
20816
|
-
console.log("[Layer 2] Heuristic breakdown (raw findings before dedupe):");
|
|
20817
|
-
for (const [name, count] of Object.entries(rawStats)) {
|
|
20818
|
-
if (count > 0) {
|
|
20819
|
-
const detectorName = detectorNameMap[name];
|
|
20820
|
-
const tier = detectorName ? (0, tiers_1.getLayer2DetectorTier)(detectorName) : "unknown";
|
|
20821
|
-
console.log(` - ${name}: ${count} (${tier})`);
|
|
20822
|
-
}
|
|
20823
|
-
}
|
|
20824
|
-
console.log(`[Layer 2] Tier breakdown (after dedupe): ${(0, tiers_1.formatTierStats)(tierStats)}`);
|
|
20825
20865
|
return {
|
|
20826
20866
|
vulnerabilities: uniqueVulnerabilities,
|
|
20827
20867
|
filesScanned: files.filter((f) => isCodeFile(f.path)).length,
|
|
@@ -36127,8 +36167,8 @@ var require_anthropic = __commonJS({
|
|
|
36127
36167
|
var context_helpers_1 = require_context_helpers();
|
|
36128
36168
|
var project_context_builder_1 = require_project_context_builder();
|
|
36129
36169
|
var tiers_1 = require_tiers();
|
|
36130
|
-
var FILES_PER_API_BATCH =
|
|
36131
|
-
var PARALLEL_API_BATCHES =
|
|
36170
|
+
var FILES_PER_API_BATCH = 8;
|
|
36171
|
+
var PARALLEL_API_BATCHES = 6;
|
|
36132
36172
|
function getAnthropicClient() {
|
|
36133
36173
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
36134
36174
|
if (!apiKey) {
|
|
@@ -38758,9 +38798,15 @@ var require_dist = __commonJS({
|
|
|
38758
38798
|
const scanModeConfig = resolveScanModeConfig(options);
|
|
38759
38799
|
const isIncremental = scanModeConfig.mode === "incremental";
|
|
38760
38800
|
const depth = scanModeConfig.scanDepth || "cheap";
|
|
38761
|
-
|
|
38801
|
+
const quiet = options.quiet ?? false;
|
|
38802
|
+
const log = (message) => {
|
|
38803
|
+
if (!quiet) {
|
|
38804
|
+
console.log(message);
|
|
38805
|
+
}
|
|
38806
|
+
};
|
|
38807
|
+
log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`);
|
|
38762
38808
|
if (isIncremental && scanModeConfig.changedFiles) {
|
|
38763
|
-
|
|
38809
|
+
log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`);
|
|
38764
38810
|
}
|
|
38765
38811
|
const reportProgress = (status2, message, vulnerabilitiesFound = allVulnerabilities.length) => {
|
|
38766
38812
|
if (onProgress) {
|
|
@@ -38777,12 +38823,12 @@ var require_dist = __commonJS({
|
|
|
38777
38823
|
try {
|
|
38778
38824
|
const middlewareConfig = (0, middleware_detector_1.detectGlobalAuthMiddleware)(files);
|
|
38779
38825
|
if (middlewareConfig.hasAuthMiddleware) {
|
|
38780
|
-
|
|
38826
|
+
log(`[Scanner] repo=${repoInfo.name} auth_middleware=${middlewareConfig.authType || "unknown"} file=${middlewareConfig.middlewareFile}`);
|
|
38781
38827
|
}
|
|
38782
38828
|
const fileAuthImports = (0, imported_auth_detector_1.buildFileAuthImports)(files);
|
|
38783
38829
|
const filesWithImportedAuth = Array.from(fileAuthImports.values()).filter((f) => f.usesImportedAuth).length;
|
|
38784
38830
|
if (filesWithImportedAuth > 0) {
|
|
38785
|
-
|
|
38831
|
+
log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`);
|
|
38786
38832
|
}
|
|
38787
38833
|
reportProgress("layer1", "Running surface scan (patterns, entropy, config)...");
|
|
38788
38834
|
let layer1Result = await (0, layer1_1.runLayer1Scan)(files);
|
|
@@ -38791,18 +38837,18 @@ var require_dist = __commonJS({
|
|
|
38791
38837
|
...layer1Result,
|
|
38792
38838
|
vulnerabilities: (0, urls_1.aggregateLocalhostFindings)(layer1Result.vulnerabilities)
|
|
38793
38839
|
};
|
|
38794
|
-
|
|
38840
|
+
log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`);
|
|
38795
38841
|
reportProgress("layer2", "Running structural scan (variables, logic gates)...", layer1Result.vulnerabilities.length);
|
|
38796
38842
|
const layer2Result = await (0, layer2_1.runLayer2Scan)(files, { middlewareConfig, fileAuthImports });
|
|
38797
38843
|
const heuristicBreakdown = Object.entries(layer2Result.stats.raw).filter(([, count]) => count > 0).map(([name, count]) => `${name}:${count}`).join(",");
|
|
38798
|
-
|
|
38844
|
+
log(`[Layer2] repo=${repoInfo.name} findings_raw=${Object.values(layer2Result.stats.raw).reduce((a, b3) => a + b3, 0)} findings_deduped=${layer2Result.vulnerabilities.length} heuristic_breakdown={${heuristicBreakdown}}`);
|
|
38799
38845
|
const layer12Findings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities];
|
|
38800
38846
|
const beforeAggregationCount = layer12Findings.length;
|
|
38801
38847
|
const aggregatedFindings = aggregateNoisyFindings(layer12Findings);
|
|
38802
38848
|
const aggregatedCount = beforeAggregationCount - aggregatedFindings.length;
|
|
38803
38849
|
const tierFiltered = filterByTierAndDepth(aggregatedFindings, depth);
|
|
38804
|
-
|
|
38805
|
-
|
|
38850
|
+
log(`[Scanner] repo=${repoInfo.name} tier_breakdown=${(0, tiers_1.formatTierStats)(tierFiltered.tierStats)}`);
|
|
38851
|
+
log(`[Scanner] repo=${repoInfo.name} depth=${depth} tier_routing: surface=${tierFiltered.toSurface.length} validate=${tierFiltered.toValidate.length} hidden=${tierFiltered.hidden.length}`);
|
|
38806
38852
|
const additionalValidation = tierFiltered.toSurface.filter(
|
|
38807
38853
|
(v2) => v2.requiresAIValidation || v2.category === "ai_prompt_injection" || // Story B1: Prompt hygiene
|
|
38808
38854
|
v2.category === "ai_unsafe_execution" || // Story B2: LLM output execution
|
|
@@ -38817,7 +38863,7 @@ var require_dist = __commonJS({
|
|
|
38817
38863
|
autoDismissBySeverity[d3.finding.severity] = (autoDismissBySeverity[d3.finding.severity] || 0) + 1;
|
|
38818
38864
|
}
|
|
38819
38865
|
if (autoDismissed.length > 0) {
|
|
38820
|
-
|
|
38866
|
+
log(`[Layer2] repo=${repoInfo.name} auto_dismissed_total=${autoDismissed.length} by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}}`);
|
|
38821
38867
|
}
|
|
38822
38868
|
const maxValidationFiles = scanModeConfig.maxAIValidationFiles || MAX_VALIDATION_CANDIDATES_PER_FILE;
|
|
38823
38869
|
const cappedValidation = capValidationCandidatesPerFile(afterAutoDismiss, maxValidationFiles);
|
|
@@ -38832,13 +38878,13 @@ var require_dist = __commonJS({
|
|
|
38832
38878
|
validatedFindings = validationResult.vulnerabilities;
|
|
38833
38879
|
const { stats: validationStats } = validationResult;
|
|
38834
38880
|
capturedValidationStats = validationStats;
|
|
38835
|
-
|
|
38836
|
-
|
|
38881
|
+
log(`[AI Validation] repo=${repoInfo.name} depth=${depth} candidates=${findingsToValidate.length} capped_from=${requiresValidation.length} auto_dismissed=${autoDismissed.length} kept=${validationStats.confirmedFindings} rejected=${validationStats.dismissedFindings} downgraded=${validationStats.downgradedFindings}`);
|
|
38882
|
+
log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`);
|
|
38837
38883
|
const notValidated = cappedValidation.filter((v2) => !findingsToValidate.includes(v2));
|
|
38838
38884
|
validatedFindings.push(...notValidated);
|
|
38839
38885
|
}
|
|
38840
38886
|
} else if (scanModeConfig.skipAIValidation) {
|
|
38841
|
-
|
|
38887
|
+
log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
|
|
38842
38888
|
}
|
|
38843
38889
|
allVulnerabilities.push(...validatedFindings, ...noValidationNeeded);
|
|
38844
38890
|
const shouldRunLayer3 = options.enableAI !== false && !scanModeConfig.skipLayer3;
|
|
@@ -38863,9 +38909,9 @@ var require_dist = __commonJS({
|
|
|
38863
38909
|
}
|
|
38864
38910
|
});
|
|
38865
38911
|
allVulnerabilities.push(...layer3Result.vulnerabilities);
|
|
38866
|
-
|
|
38912
|
+
log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
|
|
38867
38913
|
} else if (scanModeConfig.skipLayer3) {
|
|
38868
|
-
|
|
38914
|
+
log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
|
|
38869
38915
|
}
|
|
38870
38916
|
const uniqueVulnerabilities = deduplicateVulnerabilities(allVulnerabilities);
|
|
38871
38917
|
const resolvedVulnerabilities = resolveContradictions(uniqueVulnerabilities, middlewareConfig);
|
|
@@ -38976,9 +39022,6 @@ Found ${group.length} occurrences at lines: ${lineDisplay}`,
|
|
|
38976
39022
|
});
|
|
38977
39023
|
const capped = sorted.slice(0, maxPerFile);
|
|
38978
39024
|
result.push(...capped);
|
|
38979
|
-
if (sorted.length > maxPerFile) {
|
|
38980
|
-
console.log(`[Scanner] Capped ${filePath}: ${sorted.length} \u2192 ${maxPerFile} validation candidates`);
|
|
38981
|
-
}
|
|
38982
39025
|
}
|
|
38983
39026
|
return result;
|
|
38984
39027
|
}
|
|
@@ -39031,7 +39074,6 @@ Found ${group.length} occurrences at lines: ${lineDisplay}`,
|
|
|
39031
39074
|
for (const vuln of otherAuthFindings) {
|
|
39032
39075
|
if (vuln.severity === "critical" || vuln.severity === "high") {
|
|
39033
39076
|
const reason = isAPIRouteProtected ? "API route protected by middleware" : isClientCallingProtectedAPI ? "client component calling protected API" : "route is protected";
|
|
39034
|
-
console.log(`[Contradiction] Dropping "${vuln.title}" (${vuln.severity}) - ${reason}`);
|
|
39035
39077
|
continue;
|
|
39036
39078
|
}
|
|
39037
39079
|
result.push(vuln);
|
|
@@ -43498,18 +43540,22 @@ async function runScanOnce(targetPath, options) {
|
|
|
43498
43540
|
}
|
|
43499
43541
|
spinner.succeed(`Found ${files.length} files to scan`);
|
|
43500
43542
|
const onProgress = (progress) => {
|
|
43543
|
+
if (options.verbose) {
|
|
43544
|
+
console.log(`[Progress] ${progress.status}: ${progress.filesProcessed}/${progress.totalFiles} files, ${progress.vulnerabilitiesFound} findings`);
|
|
43545
|
+
}
|
|
43546
|
+
const fileProgress = progress.filesProcessed && progress.filesProcessed < progress.totalFiles ? ` \u2022 ${progress.filesProcessed}/${progress.totalFiles} files` : ` (${progress.totalFiles} files)`;
|
|
43501
43547
|
switch (progress.status) {
|
|
43502
43548
|
case "layer1":
|
|
43503
|
-
spinner.text = `
|
|
43549
|
+
spinner.text = `Layer 1: Pattern matching${fileProgress} ${source_default.dim(`\u2192 ${progress.vulnerabilitiesFound} candidates`)}`;
|
|
43504
43550
|
break;
|
|
43505
43551
|
case "layer2":
|
|
43506
|
-
spinner.text = `
|
|
43552
|
+
spinner.text = `Layer 2: Code structure analysis${fileProgress} ${source_default.dim(`\u2192 ${progress.vulnerabilitiesFound} findings`)}`;
|
|
43507
43553
|
break;
|
|
43508
43554
|
case "validating":
|
|
43509
|
-
spinner.text = `AI
|
|
43555
|
+
spinner.text = `AI validation${fileProgress} ${source_default.dim(`\u2192 reviewing ${progress.vulnerabilitiesFound} candidates`)}`;
|
|
43510
43556
|
break;
|
|
43511
43557
|
case "layer3":
|
|
43512
|
-
spinner.text = `Deep AI analysis
|
|
43558
|
+
spinner.text = `Layer 3: Deep AI analysis${fileProgress} ${source_default.dim(`\u2192 semantic security check`)}`;
|
|
43513
43559
|
break;
|
|
43514
43560
|
case "complete":
|
|
43515
43561
|
const issueText = progress.vulnerabilitiesFound === 1 ? "issue" : "issues";
|
|
@@ -43529,7 +43575,7 @@ async function runScanOnce(targetPath, options) {
|
|
|
43529
43575
|
spinner.start("Starting scan...");
|
|
43530
43576
|
const hasLocalAI = !!process.env.ANTHROPIC_API_KEY;
|
|
43531
43577
|
if (options.depth !== "cheap" && isAuthenticated() && !hasLocalAI) {
|
|
43532
|
-
spinner.text = `
|
|
43578
|
+
spinner.text = `Backend ${options.depth} scan analyzing ${files.length} files...`;
|
|
43533
43579
|
result = await callBackendAPI(
|
|
43534
43580
|
files,
|
|
43535
43581
|
options.depth,
|
|
@@ -43540,9 +43586,10 @@ async function runScanOnce(targetPath, options) {
|
|
|
43540
43586
|
branch: "local"
|
|
43541
43587
|
}
|
|
43542
43588
|
);
|
|
43543
|
-
spinner.succeed(`Backend scan complete`);
|
|
43589
|
+
spinner.succeed(`Backend ${options.depth} scan complete`);
|
|
43544
43590
|
} else {
|
|
43545
43591
|
const enableAI = options.depth !== "cheap" && hasLocalAI;
|
|
43592
|
+
const shouldBeQuiet = options.quiet ?? !options.verbose;
|
|
43546
43593
|
result = await (0, import_scanner.runScan)(
|
|
43547
43594
|
files,
|
|
43548
43595
|
{
|
|
@@ -43552,7 +43599,8 @@ async function runScanOnce(targetPath, options) {
|
|
|
43552
43599
|
},
|
|
43553
43600
|
{
|
|
43554
43601
|
enableAI,
|
|
43555
|
-
scanDepth: options.depth
|
|
43602
|
+
scanDepth: options.depth,
|
|
43603
|
+
quiet: shouldBeQuiet
|
|
43556
43604
|
},
|
|
43557
43605
|
onProgress
|
|
43558
43606
|
);
|
|
@@ -43595,6 +43643,7 @@ async function runScan(targetPath, cliOptions) {
|
|
|
43595
43643
|
failOn: cliOptions.failOn ?? "high",
|
|
43596
43644
|
color: cliOptions.color,
|
|
43597
43645
|
quiet: cliOptions.quiet,
|
|
43646
|
+
verbose: cliOptions.verbose,
|
|
43598
43647
|
incremental: cliOptions.incremental,
|
|
43599
43648
|
diff: cliOptions.diff,
|
|
43600
43649
|
output: cliOptions.output
|
|
@@ -43605,6 +43654,7 @@ async function runScan(targetPath, cliOptions) {
|
|
|
43605
43654
|
format: baseOptions.format ?? projectConfig?.format ?? "terminal",
|
|
43606
43655
|
failOn: baseOptions.failOn ?? projectConfig?.failOn ?? "high",
|
|
43607
43656
|
quiet: baseOptions.quiet ?? projectConfig?.quiet,
|
|
43657
|
+
verbose: baseOptions.verbose,
|
|
43608
43658
|
output: baseOptions.output ?? projectConfig?.output
|
|
43609
43659
|
};
|
|
43610
43660
|
try {
|
|
@@ -43619,7 +43669,7 @@ async function runScan(targetPath, cliOptions) {
|
|
|
43619
43669
|
process.exit(1);
|
|
43620
43670
|
}
|
|
43621
43671
|
}
|
|
43622
|
-
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("-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", `
|
|
43672
|
+
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", `
|
|
43623
43673
|
Examples:
|
|
43624
43674
|
$ oculum scan . # Scan current directory (quick/free)
|
|
43625
43675
|
$ oculum scan ./src # Scan specific directory
|
|
@@ -47052,6 +47102,8 @@ async function runScanWizard() {
|
|
|
47052
47102
|
format,
|
|
47053
47103
|
failOn,
|
|
47054
47104
|
color: true,
|
|
47105
|
+
quiet: true,
|
|
47106
|
+
// Suppress scanner logs in interactive UI mode
|
|
47055
47107
|
incremental,
|
|
47056
47108
|
diff: diffRef,
|
|
47057
47109
|
output: outputPath
|
|
@@ -47079,7 +47131,9 @@ async function runQuickScan(targetPath = ".") {
|
|
|
47079
47131
|
depth: "cheap",
|
|
47080
47132
|
format: "terminal",
|
|
47081
47133
|
failOn: "high",
|
|
47082
|
-
color: true
|
|
47134
|
+
color: true,
|
|
47135
|
+
quiet: true
|
|
47136
|
+
// Suppress scanner logs in interactive UI mode
|
|
47083
47137
|
},
|
|
47084
47138
|
cancelled: false
|
|
47085
47139
|
};
|
|
@@ -47532,7 +47586,9 @@ async function runUI() {
|
|
|
47532
47586
|
depth,
|
|
47533
47587
|
format: "terminal",
|
|
47534
47588
|
failOn: "high",
|
|
47535
|
-
color: true
|
|
47589
|
+
color: true,
|
|
47590
|
+
quiet: true
|
|
47591
|
+
// Suppress scanner logs in interactive UI mode
|
|
47536
47592
|
});
|
|
47537
47593
|
console.log(output);
|
|
47538
47594
|
recordScan();
|
|
@@ -47771,7 +47827,6 @@ async function usage(options) {
|
|
|
47771
47827
|
console.log("");
|
|
47772
47828
|
console.log(source_default.dim(" Scans: ") + source_default.white(formatNumber(usageData.totalScans)));
|
|
47773
47829
|
console.log(source_default.dim(" Files: ") + source_default.white(formatNumber(usageData.totalFiles)));
|
|
47774
|
-
console.log(source_default.dim(" Cost: ") + source_default.white(`$${usageData.totalCostDollars.toFixed(4)}`));
|
|
47775
47830
|
console.log("");
|
|
47776
47831
|
console.log(source_default.dim(" Resets on: ") + source_default.white(formatDate(usageData.resetDate)));
|
|
47777
47832
|
console.log("");
|
|
@@ -47817,7 +47872,7 @@ var usageCommand = new Command("usage").description("Show current usage and quot
|
|
|
47817
47872
|
|
|
47818
47873
|
// src/index.ts
|
|
47819
47874
|
var program2 = new Command();
|
|
47820
|
-
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.
|
|
47875
|
+
program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.4").addHelpText("after", `
|
|
47821
47876
|
Quick Start:
|
|
47822
47877
|
$ oculum scan . Scan current directory (free)
|
|
47823
47878
|
$ oculum ui Interactive mode with guided setup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oculum/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "AI-native security scanner CLI for detecting vulnerabilities in AI-generated code, BYOK patterns, and modern web applications",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"url": "https://github.com/flexipie/oculum/issues"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --banner:js=\"#!/usr/bin/env node\" --define:process.env.OCULUM_API_URL='undefined'",
|
|
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.4\"'",
|
|
22
22
|
"dev": "npm run build -- --watch",
|
|
23
23
|
"test": "echo \"No tests configured yet\"",
|
|
24
24
|
"lint": "eslint src/"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@oculum/scanner": "^1.0.
|
|
27
|
+
"@oculum/scanner": "^1.0.2",
|
|
28
28
|
"@oculum/shared": "^1.0.0",
|
|
29
29
|
"commander": "^12.1.0",
|
|
30
30
|
"chalk": "^5.3.0",
|