@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.
Files changed (2) hide show
  1. package/dist/index.js +181 -126
  2. 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 (const file of files) {
14422
- const entropyFindings = (0, entropy_1.detectHighEntropyStrings)(file.content, file.path);
14423
- const patternFindings = (0, patterns_1.detectKnownPatterns)(file.content, file.path);
14424
- const configFindings = (0, config_audit_1.auditConfiguration)(file.content, file.path);
14425
- const fileFlags = (0, file_flags_1.detectDangerousFiles)(file.content, file.path);
14426
- const commentFindings = (0, comments_1.detectAICommentPatterns)(file.content, file.path);
14427
- const urlFindings = (0, urls_1.detectSensitiveURLs)(file.content, file.path);
14428
- const cryptoFindings = (0, weak_crypto_1.detectWeakCrypto)(file.content, file.path);
14429
- rawStats.entropy += entropyFindings.length;
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 (const file of files) {
20703
- if (isCodeFile(file.path)) {
20704
- const variableFindings = (0, variables_1.detectSensitiveVariables)(file.content, file.path);
20705
- const logicFindings = (0, logic_gates_1.detectLogicGates)(file.content, file.path);
20706
- const dangerousFuncFindings = (0, dangerous_functions_1.detectDangerousFunctions)(file.content, file.path);
20707
- const riskyImportFindings = (0, risky_imports_1.detectRiskyImports)(file.content, file.path);
20708
- const authFindings = (0, auth_antipatterns_1.detectAuthAntipatterns)(file.content, file.path, {
20709
- middlewareConfig: options.middlewareConfig,
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 = 5;
36131
- var PARALLEL_API_BATCHES = 4;
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
- console.log(`[Scanner] repo=${repoInfo.name} mode=${scanModeConfig.mode} depth=${depth} files=${files.length}`);
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
- console.log(`[Scanner] repo=${repoInfo.name} incremental_files=${scanModeConfig.changedFiles.length}`);
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
- console.log(`[Scanner] repo=${repoInfo.name} auth_middleware=${middlewareConfig.authType || "unknown"} file=${middlewareConfig.middlewareFile}`);
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
- console.log(`[Scanner] repo=${repoInfo.name} files_with_imported_auth=${filesWithImportedAuth}`);
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
- console.log(`[Layer1] repo=${repoInfo.name} findings_raw=${layer1RawCount} findings_deduped=${layer1Result.vulnerabilities.length}`);
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
- console.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}}`);
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
- console.log(`[Scanner] repo=${repoInfo.name} tier_breakdown=${(0, tiers_1.formatTierStats)(tierFiltered.tierStats)}`);
38805
- console.log(`[Scanner] repo=${repoInfo.name} depth=${depth} tier_routing: surface=${tierFiltered.toSurface.length} validate=${tierFiltered.toValidate.length} hidden=${tierFiltered.hidden.length}`);
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
- console.log(`[Layer2] repo=${repoInfo.name} auto_dismissed_total=${autoDismissed.length} by_severity={info:${autoDismissBySeverity.info},low:${autoDismissBySeverity.low},medium:${autoDismissBySeverity.medium},high:${autoDismissBySeverity.high}}`);
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
- console.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}`);
38836
- console.log(`[AI Validation] cost_estimate: input_tokens=${validationStats.estimatedInputTokens} output_tokens=${validationStats.estimatedOutputTokens} cost=$${validationStats.estimatedCost.toFixed(4)} api_calls=${validationStats.apiCalls}`);
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
- console.log(`[AI Validation] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
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
- console.log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
38912
+ log(`[Layer3] repo=${repoInfo.name} depth=${depth} files_analyzed=${layer3Result.aiAnalyzed} findings=${layer3Result.vulnerabilities.length}`);
38867
38913
  } else if (scanModeConfig.skipLayer3) {
38868
- console.log(`[Layer3] repo=${repoInfo.name} depth=${depth} skipped=true reason=scan_mode_config`);
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 = `Scanning patterns... ${source_default.dim(`(${progress.vulnerabilitiesFound} potential issues)`)}`;
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 = `Analyzing code structure... ${source_default.dim(`(${progress.vulnerabilitiesFound} findings)`)}`;
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 validating findings... ${source_default.dim(`(${progress.vulnerabilitiesFound} candidates)`)}`;
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 in progress...`;
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 = `Sending ${files.length} files to backend for ${options.depth} scan...`;
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.0").addHelpText("after", `
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.2",
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.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",