@oculum/cli 1.0.19 → 1.0.20

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 +1282 -108
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -22260,6 +22260,222 @@ var require_patterns = __commonJS({
22260
22260
  }
22261
22261
  });
22262
22262
 
22263
+ // ../scanner/dist/shared/registry-clients.js
22264
+ var require_registry_clients = __commonJS({
22265
+ "../scanner/dist/shared/registry-clients.js"(exports2) {
22266
+ "use strict";
22267
+ Object.defineProperty(exports2, "__esModule", { value: true });
22268
+ exports2.fetchNPMMetadata = fetchNPMMetadata;
22269
+ exports2.fetchPyPIMetadata = fetchPyPIMetadata;
22270
+ exports2.extractNpmDependencies = extractNpmDependencies;
22271
+ exports2.extractPythonRequirements = extractPythonRequirements;
22272
+ exports2.extractPyprojectDependencies = extractPyprojectDependencies;
22273
+ exports2.getPackageFileType = getPackageFileType;
22274
+ exports2.calculatePackageAgeDays = calculatePackageAgeDays;
22275
+ exports2.rateLimitDelay = rateLimitDelay;
22276
+ exports2.clearRegistryCaches = clearRegistryCaches;
22277
+ var npmMetadataCache = /* @__PURE__ */ new Map();
22278
+ var pypiMetadataCache = /* @__PURE__ */ new Map();
22279
+ var RATE_LIMIT_DELAY_MS = 100;
22280
+ async function fetchNPMMetadata(packageName) {
22281
+ if (npmMetadataCache.has(packageName)) {
22282
+ return npmMetadataCache.get(packageName) || null;
22283
+ }
22284
+ try {
22285
+ const registryResponse = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, {
22286
+ headers: {
22287
+ "Accept": "application/json"
22288
+ }
22289
+ });
22290
+ if (!registryResponse.ok) {
22291
+ if (registryResponse.status === 404) {
22292
+ npmMetadataCache.set(packageName, null);
22293
+ return null;
22294
+ }
22295
+ console.warn(`[Registry] npm registry error for ${packageName}: ${registryResponse.status}`);
22296
+ return null;
22297
+ }
22298
+ const data = await registryResponse.json();
22299
+ let weeklyDownloads = 0;
22300
+ try {
22301
+ const downloadsResponse = await fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`);
22302
+ if (downloadsResponse.ok) {
22303
+ const downloadsData = await downloadsResponse.json();
22304
+ weeklyDownloads = downloadsData.downloads || 0;
22305
+ }
22306
+ } catch {
22307
+ }
22308
+ const metadata = {
22309
+ name: data.name,
22310
+ version: data["dist-tags"]?.latest || "",
22311
+ description: data.description,
22312
+ maintainers: data.maintainers || [],
22313
+ time: data.time || { created: "", modified: "" },
22314
+ repository: data.repository,
22315
+ homepage: data.homepage,
22316
+ license: data.license,
22317
+ downloads: { weekly: weeklyDownloads }
22318
+ };
22319
+ npmMetadataCache.set(packageName, metadata);
22320
+ return metadata;
22321
+ } catch (error) {
22322
+ console.warn(`[Registry] Failed to fetch npm metadata for ${packageName}:`, error);
22323
+ return null;
22324
+ }
22325
+ }
22326
+ async function fetchPyPIMetadata(packageName) {
22327
+ if (pypiMetadataCache.has(packageName)) {
22328
+ return pypiMetadataCache.get(packageName) || null;
22329
+ }
22330
+ try {
22331
+ const response = await fetch(`https://pypi.org/pypi/${encodeURIComponent(packageName)}/json`, {
22332
+ headers: {
22333
+ "Accept": "application/json"
22334
+ }
22335
+ });
22336
+ if (!response.ok) {
22337
+ if (response.status === 404) {
22338
+ pypiMetadataCache.set(packageName, null);
22339
+ return null;
22340
+ }
22341
+ console.warn(`[Registry] PyPI registry error for ${packageName}: ${response.status}`);
22342
+ return null;
22343
+ }
22344
+ const data = await response.json();
22345
+ const info = data.info || {};
22346
+ let releaseDate;
22347
+ const releases = data.releases?.[info.version];
22348
+ if (releases && releases.length > 0) {
22349
+ releaseDate = releases[0].upload_time;
22350
+ }
22351
+ const metadata = {
22352
+ name: info.name,
22353
+ version: info.version,
22354
+ summary: info.summary,
22355
+ author: info.author,
22356
+ authorEmail: info.author_email,
22357
+ license: info.license,
22358
+ projectUrls: info.project_urls,
22359
+ releaseDate,
22360
+ requiresPython: info.requires_python
22361
+ };
22362
+ pypiMetadataCache.set(packageName, metadata);
22363
+ return metadata;
22364
+ } catch (error) {
22365
+ console.warn(`[Registry] Failed to fetch PyPI metadata for ${packageName}:`, error);
22366
+ return null;
22367
+ }
22368
+ }
22369
+ function extractNpmDependencies(content) {
22370
+ try {
22371
+ const pkg = JSON.parse(content);
22372
+ const deps = [];
22373
+ const lines = content.split("\n");
22374
+ const depSections = [
22375
+ { key: "dependencies", source: "dependencies" },
22376
+ { key: "devDependencies", source: "devDependencies" },
22377
+ { key: "peerDependencies", source: "peerDependencies" },
22378
+ { key: "optionalDependencies", source: "optionalDependencies" }
22379
+ ];
22380
+ for (const { key, source } of depSections) {
22381
+ const depsObj = pkg[key];
22382
+ if (!depsObj || typeof depsObj !== "object")
22383
+ continue;
22384
+ for (const [name, version] of Object.entries(depsObj)) {
22385
+ const lineIndex = lines.findIndex((l2) => l2.includes(`"${name}"`));
22386
+ deps.push({
22387
+ name,
22388
+ version,
22389
+ source,
22390
+ line: lineIndex >= 0 ? lineIndex + 1 : 1
22391
+ });
22392
+ }
22393
+ }
22394
+ return deps;
22395
+ } catch {
22396
+ return [];
22397
+ }
22398
+ }
22399
+ function extractPythonRequirements(content) {
22400
+ const deps = [];
22401
+ const lines = content.split("\n");
22402
+ lines.forEach((line, index) => {
22403
+ const trimmed = line.trim();
22404
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) {
22405
+ return;
22406
+ }
22407
+ const match3 = trimmed.match(/^([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:[=<>~!]+(.+))?/);
22408
+ if (match3) {
22409
+ deps.push({
22410
+ name: match3[1],
22411
+ version: match3[2],
22412
+ source: "requirements",
22413
+ line: index + 1
22414
+ });
22415
+ }
22416
+ });
22417
+ return deps;
22418
+ }
22419
+ function extractPyprojectDependencies(content) {
22420
+ const deps = [];
22421
+ const lines = content.split("\n");
22422
+ let inDependencies = false;
22423
+ lines.forEach((line, index) => {
22424
+ const trimmed = line.trim();
22425
+ if (trimmed === "[project.dependencies]" || trimmed === "dependencies = [") {
22426
+ inDependencies = true;
22427
+ return;
22428
+ }
22429
+ if (inDependencies && (trimmed.startsWith("[") || trimmed === "]")) {
22430
+ inDependencies = false;
22431
+ return;
22432
+ }
22433
+ if (inDependencies) {
22434
+ const match3 = trimmed.match(/^["']?([a-zA-Z0-9_-]+)(?:\[.*?\])?(?:[=<>~!]+)?/);
22435
+ if (match3 && match3[1]) {
22436
+ deps.push({
22437
+ name: match3[1],
22438
+ version: void 0,
22439
+ source: "requirements",
22440
+ line: index + 1
22441
+ });
22442
+ }
22443
+ }
22444
+ });
22445
+ return deps;
22446
+ }
22447
+ function getPackageFileType(filePath) {
22448
+ const fileName = filePath.split("/").pop()?.toLowerCase() || "";
22449
+ if (fileName === "package.json" || fileName === "package-lock.json" || fileName === "yarn.lock" || fileName === "pnpm-lock.yaml") {
22450
+ return "npm";
22451
+ }
22452
+ if (fileName === "requirements.txt" || fileName === "pyproject.toml" || fileName === "pipfile" || fileName === "pipfile.lock") {
22453
+ return "python";
22454
+ }
22455
+ return null;
22456
+ }
22457
+ function calculatePackageAgeDays(createdDate) {
22458
+ if (!createdDate)
22459
+ return Infinity;
22460
+ try {
22461
+ const created = new Date(createdDate);
22462
+ const now = /* @__PURE__ */ new Date();
22463
+ const diffMs = now.getTime() - created.getTime();
22464
+ return Math.floor(diffMs / (1e3 * 60 * 60 * 24));
22465
+ } catch {
22466
+ return Infinity;
22467
+ }
22468
+ }
22469
+ async function rateLimitDelay() {
22470
+ return new Promise((resolve9) => setTimeout(resolve9, RATE_LIMIT_DELAY_MS));
22471
+ }
22472
+ function clearRegistryCaches() {
22473
+ npmMetadataCache.clear();
22474
+ pypiMetadataCache.clear();
22475
+ }
22476
+ }
22477
+ });
22478
+
22263
22479
  // ../scanner/dist/detect/secrets/config-audit.js
22264
22480
  var require_config_audit = __commonJS({
22265
22481
  "../scanner/dist/detect/secrets/config-audit.js"(exports2) {
@@ -22267,6 +22483,8 @@ var require_config_audit = __commonJS({
22267
22483
  Object.defineProperty(exports2, "__esModule", { value: true });
22268
22484
  exports2.CONFIG_RULES = void 0;
22269
22485
  exports2.auditConfiguration = auditConfiguration;
22486
+ exports2.enrichPostinstallFindings = enrichPostinstallFindings;
22487
+ var registry_clients_1 = require_registry_clients();
22270
22488
  var BASE_CONFIDENCE = 0.5;
22271
22489
  exports2.CONFIG_RULES = [
22272
22490
  // Dockerfile rules
@@ -22541,6 +22759,79 @@ var require_config_audit = __commonJS({
22541
22759
  };
22542
22760
  return fixes[ruleName] || "Review and fix the security configuration";
22543
22761
  }
22762
+ function extractPostinstallCommand(lineContent) {
22763
+ const match3 = lineContent.match(/"(?:postinstall|preinstall)"\s*:\s*"([^"]+)"/);
22764
+ return match3?.[1] || null;
22765
+ }
22766
+ function extractScriptBinary(command) {
22767
+ const trimmed = command.trim();
22768
+ if (trimmed.startsWith("npx ")) {
22769
+ const parts2 = trimmed.slice(4).trim().split(/\s+/);
22770
+ return parts2[0] || null;
22771
+ }
22772
+ if (trimmed.startsWith("npm run ") || trimmed.startsWith("npm exec ")) {
22773
+ return null;
22774
+ }
22775
+ if (trimmed.startsWith("node ") || trimmed.startsWith("sh ") || trimmed.startsWith("bash ")) {
22776
+ return null;
22777
+ }
22778
+ const parts = trimmed.split(/\s+/);
22779
+ return parts[0] || null;
22780
+ }
22781
+ function formatDownloads(n) {
22782
+ if (n >= 1e6)
22783
+ return `${(n / 1e6).toFixed(1)}M`;
22784
+ if (n >= 1e3)
22785
+ return `${(n / 1e3).toFixed(0)}k`;
22786
+ return `${n}`;
22787
+ }
22788
+ async function enrichPostinstallFindings(findings) {
22789
+ const result = [];
22790
+ for (const finding of findings) {
22791
+ if (finding.category !== "insecure_config" || !finding.description.includes("install scripts")) {
22792
+ result.push(finding);
22793
+ continue;
22794
+ }
22795
+ const command = extractPostinstallCommand(finding.lineContent);
22796
+ if (!command) {
22797
+ result.push(finding);
22798
+ continue;
22799
+ }
22800
+ const scriptBinary = extractScriptBinary(command);
22801
+ if (scriptBinary) {
22802
+ const metadata = await (0, registry_clients_1.fetchNPMMetadata)(scriptBinary);
22803
+ if (metadata) {
22804
+ const weeklyDownloads = metadata.downloads?.weekly || 0;
22805
+ const ageDays = (0, registry_clients_1.calculatePackageAgeDays)(metadata.time?.created);
22806
+ if (weeklyDownloads >= 1e6 && ageDays >= 365) {
22807
+ continue;
22808
+ }
22809
+ if (weeklyDownloads >= 1e5 && ageDays >= 180) {
22810
+ finding.severity = "info";
22811
+ finding.description = `postinstall runs "${command}" (${scriptBinary}: ${formatDownloads(weeklyDownloads)}/week, ${Math.floor(ageDays / 365)}+ years old)`;
22812
+ result.push(finding);
22813
+ continue;
22814
+ }
22815
+ if (weeklyDownloads >= 1e4 && ageDays >= 90) {
22816
+ finding.severity = "low";
22817
+ result.push(finding);
22818
+ continue;
22819
+ }
22820
+ finding.severity = "medium";
22821
+ finding.description = `postinstall runs "${command}" \u2014 ${scriptBinary} has only ${formatDownloads(weeklyDownloads)} weekly downloads (${ageDays} days old). Review carefully.`;
22822
+ result.push(finding);
22823
+ continue;
22824
+ } else {
22825
+ finding.severity = "high";
22826
+ finding.description = `postinstall runs "${command}" \u2014 "${scriptBinary}" not found on npm registry. Possible supply chain risk.`;
22827
+ result.push(finding);
22828
+ continue;
22829
+ }
22830
+ }
22831
+ result.push(finding);
22832
+ }
22833
+ return result;
22834
+ }
22544
22835
  }
22545
22836
  });
22546
22837
 
@@ -63466,6 +63757,770 @@ var require_pipeline = __commonJS({
63466
63757
  }
63467
63758
  });
63468
63759
 
63760
+ // ../scanner/dist/detect/config/osv-check.js
63761
+ var require_osv_check = __commonJS({
63762
+ "../scanner/dist/detect/config/osv-check.js"(exports2) {
63763
+ "use strict";
63764
+ Object.defineProperty(exports2, "__esModule", { value: true });
63765
+ exports2.advisoryCache = void 0;
63766
+ exports2.checkPackageAdvisories = checkPackageAdvisories;
63767
+ exports2.queryOSV = queryOSV;
63768
+ exports2.queryOSVBatch = queryOSVBatch;
63769
+ exports2.mapOSVSeverity = mapOSVSeverity;
63770
+ exports2.isMaliciousPackage = isMaliciousPackage;
63771
+ exports2.getCacheKey = getCacheKey;
63772
+ var registry_clients_1 = require_registry_clients();
63773
+ var OSV_API_URL = "https://api.osv.dev/v1/query";
63774
+ var OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch";
63775
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
63776
+ var MAX_PACKAGES_TO_CHECK = 100;
63777
+ var advisoryCache = /* @__PURE__ */ new Map();
63778
+ exports2.advisoryCache = advisoryCache;
63779
+ function getCacheKey(name, ecosystem) {
63780
+ return `${ecosystem}:${name.toLowerCase()}`;
63781
+ }
63782
+ function isCacheValid(cached) {
63783
+ return Date.now() - cached.timestamp < CACHE_TTL_MS;
63784
+ }
63785
+ function getCachedAdvisories(name, ecosystem) {
63786
+ const key = getCacheKey(name, ecosystem);
63787
+ const cached = advisoryCache.get(key);
63788
+ if (cached && isCacheValid(cached)) {
63789
+ return cached.advisories;
63790
+ }
63791
+ return null;
63792
+ }
63793
+ function cacheAdvisories(name, ecosystem, advisories) {
63794
+ const key = getCacheKey(name, ecosystem);
63795
+ advisoryCache.set(key, {
63796
+ timestamp: Date.now(),
63797
+ advisories
63798
+ });
63799
+ }
63800
+ async function queryOSV(packageName, ecosystem, version) {
63801
+ const cached = getCachedAdvisories(packageName, ecosystem);
63802
+ if (cached !== null) {
63803
+ return cached;
63804
+ }
63805
+ try {
63806
+ const query = {
63807
+ package: {
63808
+ name: packageName,
63809
+ ecosystem
63810
+ }
63811
+ };
63812
+ if (version) {
63813
+ query.version = version;
63814
+ }
63815
+ const response = await fetch(OSV_API_URL, {
63816
+ method: "POST",
63817
+ headers: {
63818
+ "Content-Type": "application/json"
63819
+ },
63820
+ body: JSON.stringify(query)
63821
+ });
63822
+ if (!response.ok) {
63823
+ cacheAdvisories(packageName, ecosystem, []);
63824
+ return [];
63825
+ }
63826
+ const data = await response.json();
63827
+ const advisories = data.vulns || [];
63828
+ cacheAdvisories(packageName, ecosystem, advisories);
63829
+ return advisories;
63830
+ } catch {
63831
+ return [];
63832
+ }
63833
+ }
63834
+ async function queryOSVBatch(packages) {
63835
+ const results = /* @__PURE__ */ new Map();
63836
+ const uncached = [];
63837
+ for (let i = 0; i < packages.length; i++) {
63838
+ const pkg = packages[i];
63839
+ const cached = getCachedAdvisories(pkg.name, pkg.ecosystem);
63840
+ if (cached !== null) {
63841
+ results.set(getCacheKey(pkg.name, pkg.ecosystem), cached);
63842
+ } else {
63843
+ uncached.push({ ...pkg, index: i });
63844
+ }
63845
+ }
63846
+ if (uncached.length === 0) {
63847
+ return results;
63848
+ }
63849
+ try {
63850
+ const queries = uncached.map((pkg) => ({
63851
+ package: {
63852
+ name: pkg.name,
63853
+ ecosystem: pkg.ecosystem
63854
+ },
63855
+ version: pkg.version
63856
+ }));
63857
+ const response = await fetch(OSV_BATCH_URL, {
63858
+ method: "POST",
63859
+ headers: {
63860
+ "Content-Type": "application/json"
63861
+ },
63862
+ body: JSON.stringify({ queries })
63863
+ });
63864
+ if (!response.ok) {
63865
+ for (const pkg of uncached) {
63866
+ cacheAdvisories(pkg.name, pkg.ecosystem, []);
63867
+ results.set(getCacheKey(pkg.name, pkg.ecosystem), []);
63868
+ }
63869
+ return results;
63870
+ }
63871
+ const data = await response.json();
63872
+ for (let i = 0; i < uncached.length; i++) {
63873
+ const pkg = uncached[i];
63874
+ const advisories = data.results[i]?.vulns || [];
63875
+ cacheAdvisories(pkg.name, pkg.ecosystem, advisories);
63876
+ results.set(getCacheKey(pkg.name, pkg.ecosystem), advisories);
63877
+ }
63878
+ return results;
63879
+ } catch {
63880
+ for (const pkg of uncached) {
63881
+ results.set(getCacheKey(pkg.name, pkg.ecosystem), []);
63882
+ }
63883
+ return results;
63884
+ }
63885
+ }
63886
+ function mapOSVSeverity(vuln) {
63887
+ if (vuln.database_specific?.malicious) {
63888
+ return "critical";
63889
+ }
63890
+ const cvss = vuln.severity?.find((s) => s.type === "CVSS_V3");
63891
+ if (cvss) {
63892
+ const score = parseFloat(cvss.score);
63893
+ if (score >= 9)
63894
+ return "critical";
63895
+ if (score >= 7)
63896
+ return "high";
63897
+ if (score >= 4)
63898
+ return "medium";
63899
+ return "low";
63900
+ }
63901
+ const dbSeverity = vuln.database_specific?.severity?.toLowerCase();
63902
+ if (dbSeverity) {
63903
+ if (dbSeverity === "critical")
63904
+ return "critical";
63905
+ if (dbSeverity === "high")
63906
+ return "high";
63907
+ if (dbSeverity === "moderate" || dbSeverity === "medium")
63908
+ return "medium";
63909
+ if (dbSeverity === "low")
63910
+ return "low";
63911
+ }
63912
+ return "high";
63913
+ }
63914
+ function isMaliciousPackage(vuln) {
63915
+ return vuln.database_specific?.malicious === true || vuln.id.startsWith("MAL-") || (vuln.summary?.toLowerCase().includes("malicious") ?? false);
63916
+ }
63917
+ async function checkPackageAdvisories(content, filePath) {
63918
+ const vulnerabilities = [];
63919
+ const fileType = (0, registry_clients_1.getPackageFileType)(filePath);
63920
+ if (!fileType) {
63921
+ return vulnerabilities;
63922
+ }
63923
+ let dependencies = [];
63924
+ if (fileType === "npm" && filePath.endsWith("package.json")) {
63925
+ dependencies = (0, registry_clients_1.extractNpmDependencies)(content);
63926
+ } else if (fileType === "python") {
63927
+ dependencies = (0, registry_clients_1.extractPythonRequirements)(content);
63928
+ }
63929
+ if (dependencies.length === 0) {
63930
+ return vulnerabilities;
63931
+ }
63932
+ const lines = content.split("\n");
63933
+ const ecosystem = fileType === "npm" ? "npm" : "PyPI";
63934
+ const limitedDeps = dependencies.slice(0, MAX_PACKAGES_TO_CHECK);
63935
+ const packagesToQuery = limitedDeps.map((dep) => ({
63936
+ name: dep.name,
63937
+ ecosystem,
63938
+ version: dep.version
63939
+ }));
63940
+ const advisoriesMap = await queryOSVBatch(packagesToQuery);
63941
+ for (const dep of limitedDeps) {
63942
+ const key = getCacheKey(dep.name, ecosystem);
63943
+ const advisories = advisoriesMap.get(key) || [];
63944
+ if (advisories.length === 0)
63945
+ continue;
63946
+ let mostSevere = null;
63947
+ let highestSeverity = "info";
63948
+ const severityOrder = ["info", "low", "medium", "high", "critical"];
63949
+ for (const adv of advisories) {
63950
+ const sev = mapOSVSeverity(adv);
63951
+ if (severityOrder.indexOf(sev) > severityOrder.indexOf(highestSeverity)) {
63952
+ highestSeverity = sev;
63953
+ mostSevere = adv;
63954
+ }
63955
+ }
63956
+ if (!mostSevere)
63957
+ continue;
63958
+ const isMalicious = advisories.some((a) => isMaliciousPackage(a));
63959
+ const category = isMalicious ? "ai_package_malicious" : "suspicious_package";
63960
+ const advIds = advisories.map((a) => a.id).slice(0, 3).join(", ");
63961
+ const moreCount = advisories.length > 3 ? ` +${advisories.length - 3} more` : "";
63962
+ const description = isMalicious ? `Package "${dep.name}" is flagged as MALICIOUS in OSV.dev (${advIds}${moreCount}). This package may contain malware or data exfiltration code.` : `Package "${dep.name}" has ${advisories.length} known security advisories (${advIds}${moreCount}). ${mostSevere.summary || "Review before use."}`;
63963
+ const suggestedFix = isMalicious ? `Remove "${dep.name}" immediately. Do not use this package.` : `Update "${dep.name}" to a patched version or find an alternative. Check: https://osv.dev/list?ecosystem=${ecosystem}&q=${encodeURIComponent(dep.name)}`;
63964
+ vulnerabilities.push({
63965
+ id: `osv-${filePath}-${dep.name}`,
63966
+ filePath,
63967
+ lineNumber: dep.line,
63968
+ lineContent: lines[dep.line - 1]?.trim() || dep.name,
63969
+ severity: highestSeverity,
63970
+ category,
63971
+ title: isMalicious ? `Malicious package: ${dep.name}` : `Vulnerable package: ${dep.name} (${advisories.length} advisories)`,
63972
+ description,
63973
+ suggestedFix,
63974
+ confidence: "high",
63975
+ layer: 3,
63976
+ source: "config",
63977
+ requiresAIValidation: false
63978
+ // OSV data is authoritative
63979
+ });
63980
+ await (0, registry_clients_1.rateLimitDelay)();
63981
+ }
63982
+ return vulnerabilities;
63983
+ }
63984
+ }
63985
+ });
63986
+
63987
+ // ../scanner/dist/detect/config/package-check.js
63988
+ var require_package_check = __commonJS({
63989
+ "../scanner/dist/detect/config/package-check.js"(exports2) {
63990
+ "use strict";
63991
+ Object.defineProperty(exports2, "__esModule", { value: true });
63992
+ exports2.LEGITIMATE_PACKAGES = exports2.POPULAR_PYTHON_PACKAGES = exports2.POPULAR_NPM_PACKAGES = void 0;
63993
+ exports2.checkPackages = checkPackages;
63994
+ exports2.levenshteinDistance = levenshteinDistance;
63995
+ exports2.checkTyposquatting = checkTyposquatting;
63996
+ exports2.hasSuspiciousNamingPattern = hasSuspiciousNamingPattern;
63997
+ exports2.computeNPMRiskScore = computeNPMRiskScore;
63998
+ exports2.computePyPIRiskScore = computePyPIRiskScore;
63999
+ var registry_clients_1 = require_registry_clients();
64000
+ var MAX_PACKAGES_TO_CHECK = 50;
64001
+ var POPULAR_NPM_PACKAGES = /* @__PURE__ */ new Set([
64002
+ // Core frameworks
64003
+ "react",
64004
+ "vue",
64005
+ "angular",
64006
+ "svelte",
64007
+ "next",
64008
+ "nuxt",
64009
+ "gatsby",
64010
+ "express",
64011
+ "fastify",
64012
+ "koa",
64013
+ "hapi",
64014
+ "nest",
64015
+ "nestjs",
64016
+ // Utilities
64017
+ "lodash",
64018
+ "underscore",
64019
+ "ramda",
64020
+ "date-fns",
64021
+ "dayjs",
64022
+ "moment",
64023
+ "axios",
64024
+ "node-fetch",
64025
+ "got",
64026
+ "request",
64027
+ "superagent",
64028
+ // Build tools
64029
+ "webpack",
64030
+ "rollup",
64031
+ "vite",
64032
+ "parcel",
64033
+ "esbuild",
64034
+ "swc",
64035
+ "babel",
64036
+ "typescript",
64037
+ "eslint",
64038
+ "prettier",
64039
+ "jest",
64040
+ "vitest",
64041
+ "mocha",
64042
+ // Database
64043
+ "mongoose",
64044
+ "sequelize",
64045
+ "prisma",
64046
+ "typeorm",
64047
+ "knex",
64048
+ "pg",
64049
+ "mysql",
64050
+ "sqlite3",
64051
+ // Other popular
64052
+ "socket.io",
64053
+ "ws",
64054
+ "graphql",
64055
+ "apollo",
64056
+ "redux",
64057
+ "mobx",
64058
+ "zustand",
64059
+ "tailwindcss",
64060
+ "styled-components",
64061
+ "emotion",
64062
+ "sass",
64063
+ "postcss",
64064
+ "dotenv",
64065
+ "cors",
64066
+ "helmet",
64067
+ "morgan",
64068
+ "winston",
64069
+ "pino",
64070
+ "uuid",
64071
+ "crypto-js",
64072
+ "bcrypt",
64073
+ "jsonwebtoken",
64074
+ "passport",
64075
+ "commander",
64076
+ "yargs",
64077
+ "inquirer",
64078
+ "chalk",
64079
+ "ora"
64080
+ ]);
64081
+ exports2.POPULAR_NPM_PACKAGES = POPULAR_NPM_PACKAGES;
64082
+ var POPULAR_PYTHON_PACKAGES = /* @__PURE__ */ new Set([
64083
+ "requests",
64084
+ "flask",
64085
+ "django",
64086
+ "fastapi",
64087
+ "numpy",
64088
+ "pandas",
64089
+ "scipy",
64090
+ "matplotlib",
64091
+ "tensorflow",
64092
+ "pytorch",
64093
+ "torch",
64094
+ "keras",
64095
+ "scikit-learn",
64096
+ "sklearn",
64097
+ "pillow",
64098
+ "opencv-python",
64099
+ "beautifulsoup4",
64100
+ "sqlalchemy",
64101
+ "celery",
64102
+ "redis",
64103
+ "boto3",
64104
+ "pytest",
64105
+ "black",
64106
+ "flake8",
64107
+ "pydantic",
64108
+ "httpx",
64109
+ "aiohttp",
64110
+ "uvicorn",
64111
+ "gunicorn"
64112
+ ]);
64113
+ exports2.POPULAR_PYTHON_PACKAGES = POPULAR_PYTHON_PACKAGES;
64114
+ var LEGITIMATE_PACKAGES = /* @__PURE__ */ new Set([
64115
+ // Scoped packages from trusted orgs
64116
+ "@supabase/ssr",
64117
+ "@supabase/supabase-js",
64118
+ "@supabase/auth-helpers-nextjs",
64119
+ "@anthropic-ai/sdk",
64120
+ "@openai/openai",
64121
+ "@langchain/core",
64122
+ "@langchain/openai",
64123
+ "@octokit/rest",
64124
+ "@octokit/core",
64125
+ "@radix-ui/react-avatar",
64126
+ "@radix-ui/react-dialog",
64127
+ "@radix-ui/react-dropdown-menu",
64128
+ "@radix-ui/react-scroll-area",
64129
+ "@radix-ui/react-slot",
64130
+ "@radix-ui/react-tabs",
64131
+ "@tailwindcss/postcss",
64132
+ "@tailwindcss/typography",
64133
+ "@types/node",
64134
+ "@types/react",
64135
+ "@types/react-dom",
64136
+ // Common packages with unusual names
64137
+ "class-variance-authority",
64138
+ "clsx",
64139
+ "tailwind-merge",
64140
+ "cva",
64141
+ "lucide-react",
64142
+ "next-themes",
64143
+ "sonner",
64144
+ "zod",
64145
+ "zustand",
64146
+ "geist",
64147
+ "sharp",
64148
+ "turbo",
64149
+ "tsup",
64150
+ "tsx",
64151
+ // Known short names
64152
+ "ms",
64153
+ "qs",
64154
+ "ws",
64155
+ "pg",
64156
+ "ip",
64157
+ "os",
64158
+ "fs",
64159
+ "vm"
64160
+ ]);
64161
+ exports2.LEGITIMATE_PACKAGES = LEGITIMATE_PACKAGES;
64162
+ function levenshteinDistance(a, b3) {
64163
+ const matrix = [];
64164
+ for (let i = 0; i <= b3.length; i++) {
64165
+ matrix[i] = [i];
64166
+ }
64167
+ for (let j2 = 0; j2 <= a.length; j2++) {
64168
+ matrix[0][j2] = j2;
64169
+ }
64170
+ for (let i = 1; i <= b3.length; i++) {
64171
+ for (let j2 = 1; j2 <= a.length; j2++) {
64172
+ if (b3.charAt(i - 1) === a.charAt(j2 - 1)) {
64173
+ matrix[i][j2] = matrix[i - 1][j2 - 1];
64174
+ } else {
64175
+ matrix[i][j2] = Math.min(matrix[i - 1][j2 - 1] + 1, matrix[i][j2 - 1] + 1, matrix[i - 1][j2] + 1);
64176
+ }
64177
+ }
64178
+ }
64179
+ return matrix[b3.length][a.length];
64180
+ }
64181
+ function checkTyposquatting(packageName, ecosystem) {
64182
+ const name = packageName.toLowerCase();
64183
+ const popularPackages = ecosystem === "npm" ? POPULAR_NPM_PACKAGES : POPULAR_PYTHON_PACKAGES;
64184
+ for (const popular of popularPackages) {
64185
+ if (name === popular)
64186
+ continue;
64187
+ const distance = levenshteinDistance(name, popular);
64188
+ if (distance === 1 && Math.abs(name.length - popular.length) <= 1) {
64189
+ return { isSimilar: true, similarTo: popular, distance };
64190
+ }
64191
+ if (distance === 2 && name.length >= 5 && Math.abs(name.length - popular.length) <= 1) {
64192
+ return { isSimilar: true, similarTo: popular, distance };
64193
+ }
64194
+ }
64195
+ return { isSimilar: false };
64196
+ }
64197
+ function hasSuspiciousNamingPattern(packageName) {
64198
+ const suspiciousPatterns = [
64199
+ { pattern: /^[a-z]+-js$/, desc: "package-js suffix (common typosquat pattern)" },
64200
+ { pattern: /^node-[a-z]{2,}$/, desc: "node-package prefix" },
64201
+ { pattern: /^[a-z]+-node$/, desc: "package-node suffix" },
64202
+ { pattern: /-\d{3,}$/, desc: "ends with many numbers" },
64203
+ { pattern: /^[a-z]{1,2}-[a-z]+$/, desc: "very short prefix" },
64204
+ { pattern: /[0o][0o]|[1l][1l]/i, desc: "character substitution (0/o, 1/l)" }
64205
+ ];
64206
+ for (const { pattern, desc } of suspiciousPatterns) {
64207
+ if (pattern.test(packageName)) {
64208
+ return { suspicious: true, pattern: desc };
64209
+ }
64210
+ }
64211
+ return { suspicious: false };
64212
+ }
64213
+ async function computeNPMRiskScore(dep, metadata) {
64214
+ const factors = [];
64215
+ let totalScore = 0;
64216
+ if (!metadata) {
64217
+ factors.push({
64218
+ name: "package_not_found",
64219
+ score: 100,
64220
+ description: "Package does not exist in npm registry. Likely a hallucinated package name."
64221
+ });
64222
+ return {
64223
+ package: dep.name,
64224
+ ecosystem: "npm",
64225
+ totalScore: 100,
64226
+ factors,
64227
+ recommendation: "block",
64228
+ severity: "critical"
64229
+ };
64230
+ }
64231
+ const ageInDays = (0, registry_clients_1.calculatePackageAgeDays)(metadata.time?.created);
64232
+ if (ageInDays < 7) {
64233
+ factors.push({
64234
+ name: "very_new_package",
64235
+ score: 30,
64236
+ description: `Package created ${ageInDays} days ago (< 7 days)`
64237
+ });
64238
+ totalScore += 30;
64239
+ } else if (ageInDays < 30) {
64240
+ factors.push({
64241
+ name: "new_package",
64242
+ score: 15,
64243
+ description: `Package created ${ageInDays} days ago (< 30 days)`
64244
+ });
64245
+ totalScore += 15;
64246
+ }
64247
+ const weeklyDownloads = metadata.downloads?.weekly || 0;
64248
+ if (weeklyDownloads < 10) {
64249
+ factors.push({
64250
+ name: "no_downloads",
64251
+ score: 25,
64252
+ description: `Only ${weeklyDownloads} weekly downloads`
64253
+ });
64254
+ totalScore += 25;
64255
+ } else if (weeklyDownloads < 100) {
64256
+ factors.push({
64257
+ name: "low_downloads",
64258
+ score: 15,
64259
+ description: `Only ${weeklyDownloads} weekly downloads (< 100)`
64260
+ });
64261
+ totalScore += 15;
64262
+ } else if (weeklyDownloads < 1e3) {
64263
+ factors.push({
64264
+ name: "moderate_downloads",
64265
+ score: 5,
64266
+ description: `${weeklyDownloads} weekly downloads (< 1000)`
64267
+ });
64268
+ totalScore += 5;
64269
+ }
64270
+ const typoCheck = checkTyposquatting(dep.name, "npm");
64271
+ if (typoCheck.isSimilar && typoCheck.distance === 1) {
64272
+ factors.push({
64273
+ name: "likely_typosquat",
64274
+ score: 40,
64275
+ description: `Name differs by 1 character from popular package "${typoCheck.similarTo}"`
64276
+ });
64277
+ totalScore += 40;
64278
+ } else if (typoCheck.isSimilar && typoCheck.distance === 2) {
64279
+ factors.push({
64280
+ name: "possible_typosquat",
64281
+ score: 20,
64282
+ description: `Name similar to popular package "${typoCheck.similarTo}" (${typoCheck.distance} char diff)`
64283
+ });
64284
+ totalScore += 20;
64285
+ }
64286
+ const namingCheck = hasSuspiciousNamingPattern(dep.name);
64287
+ if (namingCheck.suspicious) {
64288
+ factors.push({
64289
+ name: "suspicious_name",
64290
+ score: 15,
64291
+ description: `Suspicious naming pattern: ${namingCheck.pattern}`
64292
+ });
64293
+ totalScore += 15;
64294
+ }
64295
+ const hasRepo = !!metadata.repository?.url;
64296
+ const hasHomepage = !!metadata.homepage;
64297
+ if (!hasRepo && !hasHomepage) {
64298
+ factors.push({
64299
+ name: "no_source_links",
64300
+ score: 15,
64301
+ description: "Package has no repository or homepage link"
64302
+ });
64303
+ totalScore += 15;
64304
+ }
64305
+ if (!metadata.description || metadata.description.length < 10) {
64306
+ factors.push({
64307
+ name: "no_description",
64308
+ score: 10,
64309
+ description: "Package has no meaningful description"
64310
+ });
64311
+ totalScore += 10;
64312
+ }
64313
+ const maintainerCount = metadata.maintainers?.length || 0;
64314
+ if (maintainerCount === 1 && ageInDays < 30) {
64315
+ factors.push({
64316
+ name: "single_new_maintainer",
64317
+ score: 10,
64318
+ description: "Single maintainer on a new package"
64319
+ });
64320
+ totalScore += 10;
64321
+ }
64322
+ let recommendation;
64323
+ let severity;
64324
+ if (totalScore >= 70) {
64325
+ recommendation = "block";
64326
+ severity = "high";
64327
+ } else if (totalScore >= 40) {
64328
+ recommendation = "review";
64329
+ severity = "medium";
64330
+ } else if (totalScore >= 20) {
64331
+ recommendation = "review";
64332
+ severity = "low";
64333
+ } else {
64334
+ recommendation = "allow";
64335
+ severity = "info";
64336
+ }
64337
+ return {
64338
+ package: dep.name,
64339
+ ecosystem: "npm",
64340
+ totalScore: Math.min(totalScore, 100),
64341
+ factors,
64342
+ recommendation,
64343
+ severity
64344
+ };
64345
+ }
64346
+ async function computePyPIRiskScore(dep, metadata) {
64347
+ const factors = [];
64348
+ let totalScore = 0;
64349
+ if (!metadata) {
64350
+ factors.push({
64351
+ name: "package_not_found",
64352
+ score: 100,
64353
+ description: "Package does not exist in PyPI registry. Likely a hallucinated package name."
64354
+ });
64355
+ return {
64356
+ package: dep.name,
64357
+ ecosystem: "python",
64358
+ totalScore: 100,
64359
+ factors,
64360
+ recommendation: "block",
64361
+ severity: "critical"
64362
+ };
64363
+ }
64364
+ const typoCheck = checkTyposquatting(dep.name, "python");
64365
+ if (typoCheck.isSimilar && typoCheck.distance === 1) {
64366
+ factors.push({
64367
+ name: "likely_typosquat",
64368
+ score: 40,
64369
+ description: `Name differs by 1 character from popular package "${typoCheck.similarTo}"`
64370
+ });
64371
+ totalScore += 40;
64372
+ } else if (typoCheck.isSimilar && typoCheck.distance === 2) {
64373
+ factors.push({
64374
+ name: "possible_typosquat",
64375
+ score: 20,
64376
+ description: `Name similar to popular package "${typoCheck.similarTo}"`
64377
+ });
64378
+ totalScore += 20;
64379
+ }
64380
+ const namingCheck = hasSuspiciousNamingPattern(dep.name);
64381
+ if (namingCheck.suspicious) {
64382
+ factors.push({
64383
+ name: "suspicious_name",
64384
+ score: 15,
64385
+ description: `Suspicious naming pattern: ${namingCheck.pattern}`
64386
+ });
64387
+ totalScore += 15;
64388
+ }
64389
+ const hasProjectUrls = metadata.projectUrls && Object.keys(metadata.projectUrls).length > 0;
64390
+ if (!hasProjectUrls) {
64391
+ factors.push({
64392
+ name: "no_project_urls",
64393
+ score: 15,
64394
+ description: "Package has no project URLs (repository, homepage, etc.)"
64395
+ });
64396
+ totalScore += 15;
64397
+ }
64398
+ if (!metadata.summary || metadata.summary.length < 10) {
64399
+ factors.push({
64400
+ name: "no_description",
64401
+ score: 10,
64402
+ description: "Package has no meaningful description"
64403
+ });
64404
+ totalScore += 10;
64405
+ }
64406
+ let recommendation;
64407
+ let severity;
64408
+ if (totalScore >= 70) {
64409
+ recommendation = "block";
64410
+ severity = "high";
64411
+ } else if (totalScore >= 40) {
64412
+ recommendation = "review";
64413
+ severity = "medium";
64414
+ } else if (totalScore >= 20) {
64415
+ recommendation = "review";
64416
+ severity = "low";
64417
+ } else {
64418
+ recommendation = "allow";
64419
+ severity = "info";
64420
+ }
64421
+ return {
64422
+ package: dep.name,
64423
+ ecosystem: "python",
64424
+ totalScore: Math.min(totalScore, 100),
64425
+ factors,
64426
+ recommendation,
64427
+ severity
64428
+ };
64429
+ }
64430
+ function buildRiskDescription(risk) {
64431
+ const factorList = risk.factors.map((f) => `- ${f.description}`).join("\n");
64432
+ if (risk.totalScore >= 70) {
64433
+ return `Package "${risk.package}" has high risk indicators (score: ${risk.totalScore}/100):
64434
+ ${factorList}
64435
+
64436
+ This may be a hallucinated package name or a typosquatting attempt.`;
64437
+ }
64438
+ if (risk.totalScore >= 40) {
64439
+ return `Package "${risk.package}" has moderate risk indicators (score: ${risk.totalScore}/100):
64440
+ ${factorList}
64441
+
64442
+ Review this dependency before using.`;
64443
+ }
64444
+ return `Package "${risk.package}" has some risk factors (score: ${risk.totalScore}/100):
64445
+ ${factorList}`;
64446
+ }
64447
+ function buildRiskSuggestedFix(risk) {
64448
+ if (risk.factors.some((f) => f.name === "package_not_found")) {
64449
+ return "Verify the package name is correct. This package does not exist in the registry - it may be a hallucinated name from an AI tool.";
64450
+ }
64451
+ if (risk.factors.some((f) => f.name.includes("typosquat"))) {
64452
+ const typoFactor = risk.factors.find((f) => f.name.includes("typosquat"));
64453
+ const match3 = typoFactor?.description.match(/"([^"]+)"/);
64454
+ const intendedPackage = match3?.[1];
64455
+ return `Verify this is the intended package. Did you mean "${intendedPackage}"?`;
64456
+ }
64457
+ if (risk.totalScore >= 40) {
64458
+ return "Review this package before using. Check the repository, maintainers, and recent activity.";
64459
+ }
64460
+ return "Consider reviewing this package's repository and maintainers.";
64461
+ }
64462
+ async function checkPackages(content, filePath) {
64463
+ const vulnerabilities = [];
64464
+ const fileType = (0, registry_clients_1.getPackageFileType)(filePath);
64465
+ if (!fileType) {
64466
+ return vulnerabilities;
64467
+ }
64468
+ let dependencies = [];
64469
+ if (fileType === "npm" && filePath.endsWith("package.json")) {
64470
+ dependencies = (0, registry_clients_1.extractNpmDependencies)(content);
64471
+ } else if (fileType === "python") {
64472
+ dependencies = (0, registry_clients_1.extractPythonRequirements)(content);
64473
+ }
64474
+ if (dependencies.length === 0) {
64475
+ return vulnerabilities;
64476
+ }
64477
+ const lines = content.split("\n");
64478
+ const packagesToCheck = dependencies.filter((dep) => {
64479
+ if (dep.name.startsWith("@"))
64480
+ return false;
64481
+ if (LEGITIMATE_PACKAGES.has(dep.name))
64482
+ return false;
64483
+ if (POPULAR_NPM_PACKAGES.has(dep.name.toLowerCase()))
64484
+ return false;
64485
+ if (POPULAR_PYTHON_PACKAGES.has(dep.name.toLowerCase()))
64486
+ return false;
64487
+ return true;
64488
+ });
64489
+ const limitedPackages = packagesToCheck.slice(0, MAX_PACKAGES_TO_CHECK);
64490
+ for (const dep of limitedPackages) {
64491
+ let risk;
64492
+ if (fileType === "npm") {
64493
+ const metadata = await (0, registry_clients_1.fetchNPMMetadata)(dep.name);
64494
+ risk = await computeNPMRiskScore(dep, metadata);
64495
+ } else {
64496
+ const metadata = await (0, registry_clients_1.fetchPyPIMetadata)(dep.name);
64497
+ risk = await computePyPIRiskScore(dep, metadata);
64498
+ }
64499
+ if (risk.recommendation !== "allow") {
64500
+ vulnerabilities.push({
64501
+ id: `pkg-risk-${filePath}-${dep.name}`,
64502
+ filePath,
64503
+ lineNumber: dep.line,
64504
+ lineContent: lines[dep.line - 1]?.trim() || dep.name,
64505
+ severity: risk.severity,
64506
+ category: "suspicious_package",
64507
+ title: risk.totalScore >= 70 ? "Potentially hallucinated dependency" : "Suspicious dependency",
64508
+ description: buildRiskDescription(risk),
64509
+ suggestedFix: buildRiskSuggestedFix(risk),
64510
+ confidence: risk.totalScore >= 70 ? "high" : "medium",
64511
+ layer: 3,
64512
+ source: "config",
64513
+ requiresAIValidation: risk.totalScore < 70
64514
+ // High-confidence issues don't need AI validation
64515
+ });
64516
+ }
64517
+ await (0, registry_clients_1.rateLimitDelay)();
64518
+ }
64519
+ return vulnerabilities;
64520
+ }
64521
+ }
64522
+ });
64523
+
63469
64524
  // ../scanner/dist/pipeline/index.js
63470
64525
  var require_pipeline2 = __commonJS({
63471
64526
  "../scanner/dist/pipeline/index.js"(exports2) {
@@ -63487,6 +64542,9 @@ var require_pipeline2 = __commonJS({
63487
64542
  var summary_1 = require_summary();
63488
64543
  var dedup_1 = require_dedup();
63489
64544
  var contradictions_1 = require_contradictions();
64545
+ var config_audit_1 = require_config_audit();
64546
+ var osv_check_1 = require_osv_check();
64547
+ var package_check_1 = require_package_check();
63490
64548
  async function runScan2(files, repoInfo, options = {}, onProgress) {
63491
64549
  const startTime = Date.now();
63492
64550
  const allVulnerabilities = [];
@@ -63574,11 +64632,24 @@ var require_pipeline2 = __commonJS({
63574
64632
  const phaseTiming = {
63575
64633
  ...detectorOutput.phaseTiming
63576
64634
  };
63577
- const beforeAggregationCount = detectorOutput.findings.length;
63578
- const aggregatedFindings = (0, aggregation_1.aggregateNoisyFindings)(detectorOutput.findings);
64635
+ const enableDepChecks = (options.enableDependencyChecks ?? false) && depth !== "local";
64636
+ let enrichedPostinstallFindings = detectorOutput.findings;
64637
+ if (enableDepChecks) {
64638
+ for (const file of files) {
64639
+ const osvFindings = await (0, osv_check_1.checkPackageAdvisories)(file.content, file.path);
64640
+ const pkgFindings = await (0, package_check_1.checkPackages)(file.content, file.path);
64641
+ enrichedPostinstallFindings.push(...osvFindings, ...pkgFindings);
64642
+ }
64643
+ enrichedPostinstallFindings = await (0, config_audit_1.enrichPostinstallFindings)(enrichedPostinstallFindings);
64644
+ log(`[DepAudit] repo=${repoInfo.name} osv+pkg checks completed`);
64645
+ } else if (depth !== "local") {
64646
+ log(`[DepAudit] repo=${repoInfo.name} skipped=true reason=tier_gated`);
64647
+ }
64648
+ const beforeAggregationCount = enrichedPostinstallFindings.length;
64649
+ const aggregatedFindings = (0, aggregation_1.aggregateNoisyFindings)(enrichedPostinstallFindings);
63579
64650
  if (filterPipeline.isEnabled) {
63580
64651
  const afterIds = new Set(aggregatedFindings.map(fid));
63581
- for (const v2 of detectorOutput.findings) {
64652
+ for (const v2 of enrichedPostinstallFindings) {
63582
64653
  if (!afterIds.has(fid(v2))) {
63583
64654
  filterPipeline.record(fid(v2), { stage: "noisy_aggregation", action: "aggregated", reason: "Aggregated noisy finding (3+ similar per file)" });
63584
64655
  }
@@ -66844,26 +67915,115 @@ var init_auth_types = __esm({
66844
67915
  }
66845
67916
  });
66846
67917
 
67918
+ // ../shared/dist/billing-constants.js
67919
+ var init_billing_constants = __esm({
67920
+ "../shared/dist/billing-constants.js"() {
67921
+ "use strict";
67922
+ }
67923
+ });
67924
+
66847
67925
  // ../shared/dist/index.js
66848
67926
  var init_dist = __esm({
66849
67927
  "../shared/dist/index.js"() {
66850
67928
  "use strict";
66851
67929
  init_auth_types();
67930
+ init_billing_constants();
66852
67931
  }
66853
67932
  });
66854
67933
 
66855
67934
  // src/utils/api.ts
67935
+ function getMaxFilesForTier(tier) {
67936
+ switch (tier) {
67937
+ case "max":
67938
+ return Infinity;
67939
+ case "pro":
67940
+ return 2500;
67941
+ case "starter":
67942
+ return 1e3;
67943
+ default:
67944
+ return 500;
67945
+ }
67946
+ }
67947
+ function batchFilesForBackend(files, tier) {
67948
+ const maxFiles = getMaxFilesForTier(tier);
67949
+ const capped = files.slice(0, maxFiles === Infinity ? files.length : maxFiles);
67950
+ const truncated = files.length > maxFiles;
67951
+ const batches = [];
67952
+ for (let i = 0; i < capped.length; i += MAX_FILES_PER_REQUEST) {
67953
+ const chunk = capped.slice(i, i + MAX_FILES_PER_REQUEST);
67954
+ const prepared = prepareFilesForBackend(chunk);
67955
+ batches.push(prepared.files);
67956
+ }
67957
+ return { batches, totalFiles: capped.length, truncated };
67958
+ }
67959
+ async function callBackendAPIBatched(batches, depth, apiKey, repoInfo, onBatchProgress) {
67960
+ if (batches.length === 1) {
67961
+ return callBackendAPI(batches[0], depth, apiKey, repoInfo);
67962
+ }
67963
+ const results = [];
67964
+ for (let i = 0; i < batches.length; i++) {
67965
+ onBatchProgress?.(i + 1, batches.length);
67966
+ const result = await callBackendAPI(batches[i], depth, apiKey, repoInfo);
67967
+ results.push(result);
67968
+ }
67969
+ return mergeResults(results);
67970
+ }
67971
+ function mergeResults(results) {
67972
+ const merged = {
67973
+ ...results[0],
67974
+ vulnerabilities: [...results[0].vulnerabilities],
67975
+ severityCounts: { ...results[0].severityCounts },
67976
+ categoryCounts: { ...results[0].categoryCounts }
67977
+ };
67978
+ if (merged.validationStats) {
67979
+ merged.validationStats = { ...merged.validationStats };
67980
+ }
67981
+ for (let i = 1; i < results.length; i++) {
67982
+ const r2 = results[i];
67983
+ merged.vulnerabilities.push(...r2.vulnerabilities);
67984
+ merged.filesScanned += r2.filesScanned;
67985
+ merged.filesSkipped += r2.filesSkipped;
67986
+ merged.scanDuration += r2.scanDuration;
67987
+ for (const [sev, count] of Object.entries(r2.severityCounts)) {
67988
+ const key = sev;
67989
+ merged.severityCounts[key] = (merged.severityCounts[key] || 0) + (count || 0);
67990
+ }
67991
+ for (const [cat, count] of Object.entries(r2.categoryCounts)) {
67992
+ const key = cat;
67993
+ merged.categoryCounts[key] = (merged.categoryCounts[key] || 0) + (count || 0);
67994
+ }
67995
+ if (r2.validationStats && merged.validationStats) {
67996
+ merged.validationStats.totalFindings += r2.validationStats.totalFindings;
67997
+ merged.validationStats.validatedFindings += r2.validationStats.validatedFindings;
67998
+ merged.validationStats.confirmedFindings += r2.validationStats.confirmedFindings;
67999
+ merged.validationStats.dismissedFindings += r2.validationStats.dismissedFindings;
68000
+ merged.validationStats.downgradedFindings += r2.validationStats.downgradedFindings;
68001
+ merged.validationStats.autoDismissedFindings += r2.validationStats.autoDismissedFindings;
68002
+ merged.validationStats.estimatedInputTokens += r2.validationStats.estimatedInputTokens;
68003
+ merged.validationStats.estimatedOutputTokens += r2.validationStats.estimatedOutputTokens;
68004
+ merged.validationStats.estimatedCost += r2.validationStats.estimatedCost;
68005
+ merged.validationStats.apiCalls += r2.validationStats.apiCalls;
68006
+ merged.validationStats.cacheCreationTokens += r2.validationStats.cacheCreationTokens;
68007
+ merged.validationStats.cacheReadTokens += r2.validationStats.cacheReadTokens;
68008
+ const totalCacheReads = results.reduce((sum, res) => sum + (res.validationStats?.cacheReadTokens || 0), 0);
68009
+ const totalTokens = results.reduce((sum, res) => sum + (res.validationStats?.estimatedInputTokens || 0), 0);
68010
+ merged.validationStats.cacheHitRate = totalTokens > 0 ? totalCacheReads / totalTokens : 0;
68011
+ }
68012
+ merged.hasBlockingIssues = merged.hasBlockingIssues || r2.hasBlockingIssues;
68013
+ }
68014
+ return merged;
68015
+ }
66856
68016
  function prepareFilesForBackend(files) {
66857
68017
  const originalCount = files.length;
66858
68018
  let totalSize = 0;
66859
68019
  const result = [];
66860
68020
  for (const file of files) {
66861
- if (result.length >= MAX_FILES_FOR_BACKEND) {
68021
+ if (result.length >= MAX_FILES_PER_REQUEST) {
66862
68022
  return {
66863
68023
  files: result,
66864
68024
  truncated: true,
66865
68025
  originalCount,
66866
- reason: `file count limit (${MAX_FILES_FOR_BACKEND} files)`
68026
+ reason: `file count limit (${MAX_FILES_PER_REQUEST} files)`
66867
68027
  };
66868
68028
  }
66869
68029
  const fileJsonSize = file.content.length + file.path.length + 200;
@@ -67027,7 +68187,7 @@ async function getUsage(apiKey) {
67027
68187
  };
67028
68188
  }
67029
68189
  }
67030
- var APIError, MAX_FILES_FOR_BACKEND, MAX_TOTAL_SIZE_BYTES;
68190
+ var APIError, MAX_FILES_PER_REQUEST, MAX_TOTAL_SIZE_BYTES;
67031
68191
  var init_api = __esm({
67032
68192
  "src/utils/api.ts"() {
67033
68193
  "use strict";
@@ -67042,7 +68202,7 @@ var init_api = __esm({
67042
68202
  this.name = "APIError";
67043
68203
  }
67044
68204
  };
67045
- MAX_FILES_FOR_BACKEND = 500;
68205
+ MAX_FILES_PER_REQUEST = 500;
67046
68206
  MAX_TOTAL_SIZE_BYTES = 4 * 1024 * 1024;
67047
68207
  }
67048
68208
  });
@@ -67076,10 +68236,10 @@ function enhanceAPIError(error) {
67076
68236
  if (error.reason === "insufficient_tier") {
67077
68237
  return {
67078
68238
  message: "This feature requires a Pro subscription",
67079
- suggestion: "Validated and deep scans require a Pro plan.",
68239
+ suggestion: "Validated scans use AI credits from your plan.",
67080
68240
  category: "auth",
67081
68241
  errorCode: "OCU-E403-TIER",
67082
- quickFix: "oculum scan . --depth cheap",
68242
+ quickFix: "oculum scan . --depth local",
67083
68243
  learnMoreUrl: "https://oculum.dev/billing",
67084
68244
  recoveryActions: [
67085
68245
  { label: "View pricing", action: "upgrade" },
@@ -67132,10 +68292,10 @@ function enhanceAPIError(error) {
67132
68292
  suggestion: rateLimitSuggestion,
67133
68293
  category: "server",
67134
68294
  errorCode: error.reason === "quota_exceeded" ? "OCU-E429-QUOTA" : "OCU-E429-RATE",
67135
- quickFix: "oculum scan . --depth cheap",
68295
+ quickFix: "oculum scan . --depth local",
67136
68296
  learnMoreUrl: "https://oculum.dev/dashboard/usage",
67137
68297
  recoveryActions: [
67138
- { label: "Use free local scan", command: "oculum scan . --depth cheap", action: "fallback" },
68298
+ { label: "Use free local scan", command: "oculum scan . --depth local", action: "fallback" },
67139
68299
  { label: "View usage & upgrade", action: "upgrade" },
67140
68300
  { label: "Retry in 30 seconds", action: "retry" }
67141
68301
  ],
@@ -67149,7 +68309,7 @@ function enhanceAPIError(error) {
67149
68309
  suggestion: "This is temporary. Try again in a few minutes.",
67150
68310
  category: "server",
67151
68311
  errorCode: `OCU-E${error.statusCode}`,
67152
- quickFix: "oculum scan . --depth cheap",
68312
+ quickFix: "oculum scan . --depth local",
67153
68313
  learnMoreUrl: "https://status.oculum.dev",
67154
68314
  recoveryActions: [
67155
68315
  { label: "Retry", action: "retry" },
@@ -67162,11 +68322,11 @@ function enhanceAPIError(error) {
67162
68322
  suggestion: "The scan may be too large for the server.",
67163
68323
  category: "server",
67164
68324
  errorCode: "OCU-E504",
67165
- quickFix: "oculum scan . --depth cheap",
68325
+ quickFix: "oculum scan . --depth local",
67166
68326
  learnMoreUrl: "https://oculum.dev/docs/troubleshooting#timeout",
67167
68327
  recoveryActions: [
67168
68328
  { label: "Retry", action: "retry" },
67169
- { label: "Use quick scan", action: "fallback" }
68329
+ { label: "Use local scan", action: "fallback" }
67170
68330
  ]
67171
68331
  };
67172
68332
  default:
@@ -67181,7 +68341,7 @@ function detectNetworkErrorType(msg) {
67181
68341
  if (msg.includes("econnrefused")) {
67182
68342
  return {
67183
68343
  type: "Connection refused",
67184
- suggestion: "The Oculum server may be down or unreachable. Try again later or use `--depth cheap` for offline scans."
68344
+ suggestion: "The Oculum server may be down or unreachable. Try again later or use `--depth local` for offline scans."
67185
68345
  };
67186
68346
  }
67187
68347
  if (msg.includes("enotfound") || msg.includes("dns")) {
@@ -67241,10 +68401,10 @@ function enhanceStandardError(error) {
67241
68401
  suggestion: "Your network may be intercepting HTTPS traffic (corporate proxy).",
67242
68402
  category: "network",
67243
68403
  errorCode: "OCU-E003",
67244
- quickFix: "oculum scan . --depth cheap",
68404
+ quickFix: "oculum scan . --depth local",
67245
68405
  learnMoreUrl: "https://oculum.dev/docs/troubleshooting#ssl",
67246
68406
  recoveryActions: [
67247
- { label: "Use offline scan", command: "oculum scan . --depth cheap", action: "fallback" },
68407
+ { label: "Use offline scan", command: "oculum scan . --depth local", action: "fallback" },
67248
68408
  { label: "View help", action: "help" }
67249
68409
  ]
67250
68410
  };
@@ -67256,7 +68416,7 @@ function enhanceStandardError(error) {
67256
68416
  suggestion,
67257
68417
  category: "network",
67258
68418
  errorCode: "OCU-E004",
67259
- quickFix: "oculum scan . --depth cheap",
68419
+ quickFix: "oculum scan . --depth local",
67260
68420
  learnMoreUrl: "https://oculum.dev/docs/troubleshooting#network",
67261
68421
  recoveryActions: [
67262
68422
  { label: "Retry", action: "retry" },
@@ -68116,29 +69276,35 @@ async function runScanOnce(targetPath, options, ignorePatterns = []) {
68116
69276
  spinner.start("Starting scan...");
68117
69277
  const hasLocalAI = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY);
68118
69278
  if (options.depth !== "local" && isAuthenticated() && !hasLocalAI) {
68119
- const prepared = prepareFilesForBackend(files);
68120
- if (prepared.truncated) {
69279
+ const { batches, totalFiles, truncated } = batchFilesForBackend(files, config.tier);
69280
+ if (truncated) {
68121
69281
  spinner.warn(source_default.yellow(
68122
- `Truncating scan from ${prepared.originalCount} to ${prepared.files.length} files due to ${prepared.reason}`
69282
+ `Truncating scan from ${files.length} to ${totalFiles} files (${config.tier || "free"} plan limit)`
68123
69283
  ));
68124
69284
  if (!options.quiet) {
68125
- console.log(source_default.dim(" Tip: Use --ignore patterns to exclude files, or scan a subdirectory"));
69285
+ console.log(source_default.dim(" Tip: Upgrade to Pro or Max for higher limits"));
68126
69286
  console.log("");
68127
69287
  }
68128
69288
  }
68129
- spinner.text = `Backend ${options.depth} scan analyzing ${prepared.files.length} files...`;
68130
- result = await callBackendAPI(
68131
- prepared.files,
69289
+ if (batches.length > 1 && !options.quiet) {
69290
+ console.log(source_default.dim(` Scanning ${totalFiles} files in ${batches.length} batches`));
69291
+ }
69292
+ spinner.text = `Backend ${options.depth} scan analyzing ${totalFiles} files...`;
69293
+ result = await callBackendAPIBatched(
69294
+ batches,
68132
69295
  options.depth,
68133
69296
  config.apiKey,
68134
69297
  {
68135
69298
  name: (0, import_path4.basename)((0, import_path4.resolve)(targetPath)),
68136
69299
  url: "",
68137
69300
  branch: "local"
69301
+ },
69302
+ (batch, total) => {
69303
+ spinner.text = `Backend ${options.depth} scan \u2014 batch ${batch}/${total}...`;
68138
69304
  }
68139
69305
  );
68140
- if (prepared.truncated) {
68141
- result.filesSkipped = prepared.originalCount - prepared.files.length;
69306
+ if (truncated) {
69307
+ result.filesSkipped = files.length - totalFiles;
68142
69308
  }
68143
69309
  spinner.succeed(`Backend ${options.depth} scan complete`);
68144
69310
  } else {
@@ -68496,7 +69662,7 @@ var init_scan = __esm({
68496
69662
  stopAndPersist: () => quietSpinner,
68497
69663
  text: ""
68498
69664
  };
68499
- scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: local (free), verified, deep").option("-m, --mode <mode>", "alias for --depth (local, verified, deep)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown").option("--fail-on <severity>", "exit with error code if findings at severity").option("--fail-on-categories <categories>", "fail only on specific categories (comma-separated, supports wildcards like ai-*)").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show additional details (references, validation notes)").option("-c, --compact", "minimal output (severity + title + location only)").option("-o, --output <file>", "write output to file").option("-p, --profile <name>", "use named profile from oculum.config.json").option("-i, --ignore <pattern...>", "ignore files matching pattern (can be used multiple times)").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").option("--show-suppressed", "include suppressed findings in output").option("--new", "only show findings new since baseline").option("--all", "show all findings (default, ignores baseline)").option("--ai-context", "generate .oculum/ai-context.md for AI assistants").option("--cursor", "generate .cursor/rules/security.mdc for Cursor IDE").option("--windsurf", "generate .windsurfrules for Windsurf IDE").option("--claude-code", "update CLAUDE.md with security section").option("--ide-rules", "auto-detect and update all IDE configs").option("--clear", "clear IDE rule files (use after fixing issues)").addHelpText("after", `
69665
+ scanCommand = new Command("scan").description("Scan a directory or file for security vulnerabilities").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: local (free), verified").option("-m, --mode <mode>", "alias for --depth (local, verified)").option("-f, --format <format>", "output format: terminal, json, sarif, markdown").option("--fail-on <severity>", "exit with error code if findings at severity").option("--fail-on-categories <categories>", "fail only on specific categories (comma-separated, supports wildcards like ai-*)").option("--no-color", "disable colored output").option("-q, --quiet", "minimal output for CI (suppress spinners and decorations)").option("-v, --verbose", "show additional details (references, validation notes)").option("-c, --compact", "minimal output (severity + title + location only)").option("-o, --output <file>", "write output to file").option("-p, --profile <name>", "use named profile from oculum.config.json").option("-i, --ignore <pattern...>", "ignore files matching pattern (can be used multiple times)").option("--incremental", "only scan changed files (requires git)").option("--diff <ref>", "diff against branch/commit for incremental scan").option("--show-suppressed", "include suppressed findings in output").option("--new", "only show findings new since baseline").option("--all", "show all findings (default, ignores baseline)").option("--ai-context", "generate .oculum/ai-context.md for AI assistants").option("--cursor", "generate .cursor/rules/security.mdc for Cursor IDE").option("--windsurf", "generate .windsurfrules for Windsurf IDE").option("--claude-code", "update CLAUDE.md with security section").option("--ide-rules", "auto-detect and update all IDE configs").option("--clear", "clear IDE rule files (use after fixing issues)").addHelpText("after", `
68500
69666
  Scan Modes:
68501
69667
  local Free Fast pattern matching, runs locally
68502
69668
  Best for: Quick checks, CI/CD pipelines
@@ -68504,9 +69670,6 @@ Scan Modes:
68504
69670
  verified ~$0.03 AI validates findings, ~70% fewer false positives
68505
69671
  Best for: Pre-commit checks, PR reviews
68506
69672
 
68507
- deep ~$0.10 Full semantic analysis with AI reasoning
68508
- Best for: Security audits, release checks
68509
-
68510
69673
  Examples:
68511
69674
  $ oculum scan . Scan current directory (free)
68512
69675
  $ oculum scan . -d verified AI-verified scan
@@ -70405,7 +71568,7 @@ async function watch2(targetPath, options) {
70405
71568
  console.log(source_default.dim(" " + "\u2500".repeat(50)));
70406
71569
  console.log("");
70407
71570
  console.log(source_default.dim(" Watching: ") + source_default.white(absolutePath));
70408
- console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "local" ? "Quick (pattern matching)" : options.depth));
71571
+ console.log(source_default.dim(" Depth: ") + source_default.white(options.depth === "local" ? "Local (pattern matching)" : options.depth));
70409
71572
  console.log(source_default.dim(" Status: ") + (isPaused ? source_default.yellow("Paused") : source_default.green("Active")));
70410
71573
  if (scanCount > 0) {
70411
71574
  console.log(source_default.dim(" Scans: ") + source_default.white(`${scanCount} (${totalIssues} issues found)`));
@@ -70739,7 +71902,7 @@ var init_watch = __esm({
70739
71902
  "**/.env.*.local",
70740
71903
  "**/**.env"
70741
71904
  ];
70742
- watchCommand = new Command("watch").description("Watch files and scan on changes").argument("[path]", "path to watch", ".").option("-d, --depth <depth>", "scan depth: local, verified, deep", "local").option("--debounce <ms>", "debounce time in milliseconds", "500").option("--cooldown <seconds>", "minimum seconds between scans", "10").option("--clear", "clear console before each scan", false).option("-q, --quiet", "minimal output (only show findings)").addHelpText("after", `
71905
+ watchCommand = new Command("watch").description("Watch files and scan on changes").argument("[path]", "path to watch", ".").option("-d, --depth <depth>", "scan depth: local, verified", "local").option("--debounce <ms>", "debounce time in milliseconds", "500").option("--cooldown <seconds>", "minimum seconds between scans", "10").option("--clear", "clear console before each scan", false).option("-q, --quiet", "minimal output (only show findings)").addHelpText("after", `
70743
71906
  Examples:
70744
71907
  $ oculum watch . # Watch current directory
70745
71908
  $ oculum watch ./src # Watch specific directory
@@ -71155,7 +72318,7 @@ Files:
71155
72318
  Baseline is saved to .oculum/baseline.json
71156
72319
  Add this to .gitignore or commit to share across team
71157
72320
  `);
71158
- baselineCommand.command("create").description("Create a baseline from current scan").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: local (default), verified, deep", "local").option("-q, --quiet", "minimal output").action(createBaseline);
72321
+ baselineCommand.command("create").description("Create a baseline from current scan").argument("[path]", "path to scan", ".").option("-d, --depth <depth>", "scan depth: local (default), verified", "local").option("-q, --quiet", "minimal output").action(createBaseline);
71159
72322
  baselineCommand.command("show").description("Show baseline summary").argument("[path]", "project path", ".").action(showBaseline);
71160
72323
  baselineCommand.command("clear").description("Remove baseline").argument("[path]", "project path", ".").action(clearBaseline);
71161
72324
  }
@@ -72333,7 +73496,7 @@ function renderLogo() {
72333
73496
  }
72334
73497
  function renderHeader() {
72335
73498
  const config = getConfig();
72336
- const version = true ? "1.0.19" : "0.0.0";
73499
+ const version = true ? "1.0.20" : "0.0.0";
72337
73500
  const authState = isAuthenticated() ? `${config.email || config.tier || "authenticated"}` : "local";
72338
73501
  console.log("");
72339
73502
  console.log(source_default.bold(`oculum v${version}`) + source_default.dim(` | ${authState}`));
@@ -72434,8 +73597,7 @@ async function runConfigure(initialPath) {
72434
73597
  message: "Scan depth",
72435
73598
  options: [
72436
73599
  { value: "local", label: "local", hint: "Free, pattern matching" },
72437
- { value: "verified", label: "verified", hint: "~$0.03, AI validation" },
72438
- { value: "deep", label: "deep", hint: "~$0.10, full AI analysis" }
73600
+ { value: "verified", label: "verified", hint: "~$0.03, AI validation" }
72439
73601
  ]
72440
73602
  });
72441
73603
  if (pD(depth)) return null;
@@ -72459,8 +73621,8 @@ async function handleScan(args) {
72459
73621
  console.log(source_default.red(`Path not found: ${targetPath}`));
72460
73622
  return;
72461
73623
  }
72462
- if (!["local", "verified", "deep"].includes(depth)) {
72463
- console.log(source_default.red(`Invalid depth: ${depth}`) + source_default.dim(" (use local, verified, or deep)"));
73624
+ if (!["local", "verified"].includes(depth)) {
73625
+ console.log(source_default.red(`Invalid depth: ${depth}`) + source_default.dim(" (use local or verified)"));
72464
73626
  return;
72465
73627
  }
72466
73628
  const options = {
@@ -72816,7 +73978,7 @@ function showCommandTable() {
72816
73978
  console.log(` ${source_default.cyan(cmd.padEnd(28))} ${source_default.dim(desc)}`);
72817
73979
  }
72818
73980
  console.log("");
72819
- console.log(source_default.dim(" /help scan-modes - Compare scan depths"));
73981
+ console.log(source_default.dim(" help scan-modes - Compare scan depths"));
72820
73982
  console.log("");
72821
73983
  }
72822
73984
  function showScanModes2() {
@@ -72826,9 +73988,8 @@ function showScanModes2() {
72826
73988
  console.log("");
72827
73989
  console.log(source_default.green(" local ") + source_default.white("Free ") + source_default.dim("Pattern matching + heuristics"));
72828
73990
  console.log(source_default.blue(" verified ") + source_default.white("~$0.03") + source_default.dim(" AI validation, ~70% fewer FPs"));
72829
- console.log(source_default.magenta(" deep ") + source_default.white("~$0.10") + source_default.dim(" Full semantic analysis"));
72830
73991
  console.log("");
72831
- console.log(source_default.dim(" Usage: /scan . -d verified"));
73992
+ console.log(source_default.dim(" Usage: scan . -d verified"));
72832
73993
  console.log("");
72833
73994
  }
72834
73995
  var COMMAND_TABLE, TOPICS2;
@@ -72837,20 +73998,20 @@ var init_help = __esm({
72837
73998
  "use strict";
72838
73999
  init_source();
72839
74000
  COMMAND_TABLE = [
72840
- { cmd: "/scan [path]", desc: "Scan for vulnerabilities (default: current dir)" },
72841
- { cmd: "/scan [path] -d verified", desc: "AI-verified scan" },
72842
- { cmd: "/scan --configure", desc: "Configure scan with prompts" },
72843
- { cmd: "/show [n]", desc: "View finding details from last scan" },
72844
- { cmd: "/fix [n]", desc: "Show fix suggestions" },
72845
- { cmd: "/watch [path]", desc: "Watch files and scan on changes" },
72846
- { cmd: "/baseline <cmd>", desc: "Manage scan baselines" },
72847
- { cmd: "/ignore [hash]", desc: "Manage finding suppressions" },
72848
- { cmd: "/history", desc: "View scan history" },
72849
- { cmd: "/login", desc: "Authenticate with Oculum" },
72850
- { cmd: "/logout", desc: "Clear authentication" },
72851
- { cmd: "/status", desc: "Check auth status" },
72852
- { cmd: "/usage", desc: "View credits and quota" },
72853
- { cmd: "/exit", desc: "Exit" }
74001
+ { cmd: "scan [path]", desc: "Scan for vulnerabilities (default: current dir)" },
74002
+ { cmd: "scan [path] -d verified", desc: "AI-verified scan" },
74003
+ { cmd: "scan --configure", desc: "Configure scan with prompts" },
74004
+ { cmd: "show [n]", desc: "View finding details from last scan" },
74005
+ { cmd: "fix [n]", desc: "Show fix suggestions" },
74006
+ { cmd: "watch [path]", desc: "Watch files and scan on changes" },
74007
+ { cmd: "baseline <cmd>", desc: "Manage scan baselines" },
74008
+ { cmd: "ignore [hash]", desc: "Manage finding suppressions" },
74009
+ { cmd: "history", desc: "View scan history" },
74010
+ { cmd: "login", desc: "Authenticate with Oculum" },
74011
+ { cmd: "logout", desc: "Clear authentication" },
74012
+ { cmd: "status", desc: "Check auth status" },
74013
+ { cmd: "usage", desc: "View credits and quota" },
74014
+ { cmd: "exit", desc: "Exit" }
72854
74015
  ];
72855
74016
  TOPICS2 = {
72856
74017
  "scan-modes": showScanModes2,
@@ -72971,9 +74132,9 @@ function suggestCommand(input) {
72971
74132
  const names = Object.keys(COMMANDS);
72972
74133
  const match3 = names.find((n) => n.startsWith(input));
72973
74134
  if (match3) {
72974
- console.log(source_default.yellow(`Unknown command: ${input}`) + source_default.dim(` Did you mean /${match3}?`));
74135
+ console.log(source_default.yellow(`Unknown command: ${input}`) + source_default.dim(` Did you mean ${match3}?`));
72975
74136
  } else {
72976
- console.log(source_default.yellow(`Unknown command: ${input}`) + source_default.dim(" Type /help for available commands."));
74137
+ console.log(source_default.yellow(`Unknown command: ${input}`) + source_default.dim(" Type help for available commands."));
72977
74138
  }
72978
74139
  }
72979
74140
  var COMMANDS;
@@ -72991,8 +74152,8 @@ var init_command_router = __esm({
72991
74152
  scan: {
72992
74153
  description: "Scan for vulnerabilities",
72993
74154
  flags: [
72994
- { name: "-d", description: "Scan depth (local, verified, deep)", takesValue: true },
72995
- { name: "--depth", description: "Scan depth (local, verified, deep)", takesValue: true },
74155
+ { name: "-d", description: "Scan depth", takesValue: true, values: ["local", "verified"] },
74156
+ { name: "--depth", description: "Scan depth", takesValue: true, values: ["local", "verified"] },
72996
74157
  { name: "--configure", description: "Interactive scan configuration" }
72997
74158
  ]
72998
74159
  },
@@ -73024,8 +74185,8 @@ var init_command_router = __esm({
73024
74185
  watch: {
73025
74186
  description: "Watch files and scan on changes",
73026
74187
  flags: [
73027
- { name: "-d", description: "Scan depth (local, verified, deep)", takesValue: true },
73028
- { name: "--depth", description: "Scan depth (local, verified, deep)", takesValue: true },
74188
+ { name: "-d", description: "Scan depth", takesValue: true, values: ["local", "verified"] },
74189
+ { name: "--depth", description: "Scan depth", takesValue: true, values: ["local", "verified"] },
73029
74190
  { name: "--debounce", description: "Debounce time in milliseconds", takesValue: true },
73030
74191
  { name: "--cooldown", description: "Min seconds between scans", takesValue: true },
73031
74192
  { name: "--clear", description: "Clear console before each scan" },
@@ -73049,8 +74210,8 @@ var init_command_router = __esm({
73049
74210
  flags: [
73050
74211
  { name: "-a", description: "Show all fix suggestions" },
73051
74212
  { name: "--all", description: "Show all fix suggestions" },
73052
- { name: "-s", description: "Filter by severity level", takesValue: true },
73053
- { name: "--severity", description: "Filter by severity level", takesValue: true }
74213
+ { name: "-s", description: "Filter by severity", takesValue: true, values: ["critical", "high", "medium", "low", "info"] },
74214
+ { name: "--severity", description: "Filter by severity", takesValue: true, values: ["critical", "high", "medium", "low", "info"] }
73054
74215
  ]
73055
74216
  },
73056
74217
  baseline: {
@@ -73059,7 +74220,7 @@ var init_command_router = __esm({
73059
74220
  { name: "create", description: "Create baseline from current scan" },
73060
74221
  { name: "show", description: "Display baseline summary" },
73061
74222
  { name: "clear", description: "Remove the baseline file" },
73062
- { name: "-d", description: "Scan depth for create", takesValue: true },
74223
+ { name: "-d", description: "Scan depth for create", takesValue: true, values: ["local", "verified"] },
73063
74224
  { name: "-q", description: "Minimal output" }
73064
74225
  ]
73065
74226
  },
@@ -73095,18 +74256,18 @@ async function promptLine(prompt, commands) {
73095
74256
  let suggestions = [];
73096
74257
  const cols = process.stdout.columns || 80;
73097
74258
  function getCommandSuggestions() {
73098
- if (!input.startsWith("/") || input.includes(" ")) return [];
73099
- const prefix = input.slice(1).toLowerCase();
74259
+ if (input.includes(" ")) return [];
74260
+ const prefix = (input.startsWith("/") ? input.slice(1) : input).toLowerCase();
73100
74261
  return commands.filter((c) => c.name.toLowerCase().startsWith(prefix)).slice(0, MAX_SUGGESTIONS).map((c) => ({
73101
- label: `/${c.name}`,
74262
+ label: c.name,
73102
74263
  description: c.description,
73103
- completion: `/${c.name} `
74264
+ completion: c.name + " "
73104
74265
  }));
73105
74266
  }
73106
74267
  function getFlagSuggestions() {
73107
- if (!input.startsWith("/") || !input.includes(" ")) return [];
74268
+ if (!input.includes(" ")) return [];
73108
74269
  const parts = input.split(/\s+/);
73109
- const cmdName = parts[0].slice(1).toLowerCase();
74270
+ const cmdName = (parts[0].startsWith("/") ? parts[0].slice(1) : parts[0]).toLowerCase();
73110
74271
  const cmd = commands.find((c) => c.name === cmdName);
73111
74272
  if (!cmd?.flags) return [];
73112
74273
  const lastSpace = input.lastIndexOf(" ");
@@ -73128,13 +74289,35 @@ async function promptLine(prompt, commands) {
73128
74289
  completion: f.name + (f.takesValue ? " " : " ")
73129
74290
  }));
73130
74291
  }
74292
+ function getValueSuggestions() {
74293
+ if (!input.includes(" ")) return [];
74294
+ const parts = input.split(/\s+/);
74295
+ const cmdName = (parts[0].startsWith("/") ? parts[0].slice(1) : parts[0]).toLowerCase();
74296
+ const cmd = commands.find((c) => c.name === cmdName);
74297
+ if (!cmd?.flags) return [];
74298
+ const lastSpace = input.lastIndexOf(" ");
74299
+ const currentToken = input.slice(lastSpace + 1).toLowerCase();
74300
+ if (cursor <= lastSpace) return [];
74301
+ const beforeCurrent = input.slice(0, lastSpace).trimEnd();
74302
+ const prevParts = beforeCurrent.split(/\s+/);
74303
+ const prevToken = prevParts[prevParts.length - 1];
74304
+ const flag = cmd.flags.find((f) => f.name === prevToken && f.takesValue && f.values);
74305
+ if (!flag?.values) return [];
74306
+ return flag.values.filter((v2) => !currentToken || v2.toLowerCase().startsWith(currentToken)).slice(0, MAX_SUGGESTIONS).map((v2) => ({
74307
+ label: v2,
74308
+ description: `${flag.name} ${v2}`,
74309
+ completion: v2 + " "
74310
+ }));
74311
+ }
73131
74312
  function getSuggestions() {
73132
74313
  const cmdSuggestions = getCommandSuggestions();
73133
74314
  if (cmdSuggestions.length > 0) return cmdSuggestions;
74315
+ const valueSuggestions = getValueSuggestions();
74316
+ if (valueSuggestions.length > 0) return valueSuggestions;
73134
74317
  return getFlagSuggestions();
73135
74318
  }
73136
74319
  function isCommandMode() {
73137
- return input.startsWith("/") && !input.includes(" ");
74320
+ return !input.includes(" ");
73138
74321
  }
73139
74322
  function render() {
73140
74323
  suggestions = getSuggestions();
@@ -73147,16 +74330,16 @@ async function promptLine(prompt, commands) {
73147
74330
  if (suggestions.length > 0) {
73148
74331
  for (let i = 0; i < suggestions.length; i++) {
73149
74332
  const sg = suggestions[i];
73150
- const label = ` ${sg.label}`;
74333
+ const label = sg.label;
73151
74334
  const desc = sg.description;
73152
- const pad = Math.max(1, 20 - label.length);
73153
- const maxDesc = cols - label.length - pad - 2;
74335
+ const pad = Math.max(1, 18 - label.length);
74336
+ const maxDesc = cols - label.length - pad - 6;
73154
74337
  const truncDesc = desc.length > maxDesc ? desc.slice(0, maxDesc - 1) + "\u2026" : desc;
73155
74338
  out += "\n";
73156
74339
  if (i === selectedIndex) {
73157
- out += `\x1B[7m${label}${" ".repeat(pad)}${truncDesc}\x1B[27m`;
74340
+ out += ` \x1B[36m\u25B8\x1B[0m \x1B[1m${label}\x1B[0m${" ".repeat(pad)}\x1B[2m${truncDesc}\x1B[22m`;
73158
74341
  } else {
73159
- out += `\x1B[2m${label}${" ".repeat(pad)}${truncDesc}\x1B[22m`;
74342
+ out += ` \x1B[2m${label}${" ".repeat(pad)}${truncDesc}\x1B[22m`;
73160
74343
  }
73161
74344
  }
73162
74345
  out += `\x1B[${suggestions.length}A`;
@@ -73208,17 +74391,9 @@ async function promptLine(prompt, commands) {
73208
74391
  }
73209
74392
  if (s === "\r" || s === "\n") {
73210
74393
  if (suggestions.length > 0 && isCommandMode()) {
73211
- const typed = input.slice(1).toLowerCase();
73212
- const exactMatch = commands.some((c) => c.name.toLowerCase() === typed);
73213
- if (exactMatch) {
73214
- submit(input);
73215
- return;
73216
- }
73217
74394
  completeSuggestion();
73218
- render();
73219
- return;
73220
74395
  }
73221
- submit(input);
74396
+ submit(input.trimEnd());
73222
74397
  return;
73223
74398
  }
73224
74399
  if (s === " ") {
@@ -73274,6 +74449,19 @@ async function promptLine(prompt, commands) {
73274
74449
  render();
73275
74450
  return;
73276
74451
  }
74452
+ if (s === "") {
74453
+ if (cursor > 0) {
74454
+ let end = cursor;
74455
+ while (end > 0 && input[end - 1] === " ") end--;
74456
+ let start = end;
74457
+ while (start > 0 && input[start - 1] !== " ") start--;
74458
+ input = input.slice(0, start) + input.slice(cursor);
74459
+ cursor = start;
74460
+ selectedIndex = 0;
74461
+ }
74462
+ render();
74463
+ return;
74464
+ }
73277
74465
  if (s === "") {
73278
74466
  input = "";
73279
74467
  cursor = 0;
@@ -73331,7 +74519,7 @@ async function startREPL() {
73331
74519
  renderLogo();
73332
74520
  renderHeader();
73333
74521
  if (isFirstTimeUser()) {
73334
- console.log(source_default.dim("Welcome to Oculum! Type /help to see commands, or /scan to get started."));
74522
+ console.log(source_default.dim("Welcome to Oculum! Type help to see commands, or scan to get started."));
73335
74523
  console.log("");
73336
74524
  }
73337
74525
  const commandList = Object.entries(COMMANDS).filter(([name]) => name !== "quit").map(([name, def]) => ({ name, description: def.description, flags: def.flags }));
@@ -73471,7 +74659,7 @@ async function status() {
73471
74659
  console.log(source_default.yellow(" Status: ") + source_default.white("Not logged in"));
73472
74660
  console.log("");
73473
74661
  console.log(source_default.dim(" You can use Oculum without logging in for free local scans."));
73474
- console.log(source_default.dim(" Login to unlock AI-powered validation and deep analysis."));
74662
+ console.log(source_default.dim(" Login to unlock AI-powered validation."));
73475
74663
  console.log("");
73476
74664
  console.log(source_default.bold(" Quick Start:"));
73477
74665
  console.log(source_default.cyan(" oculum scan .") + source_default.dim(" Free pattern-based scan"));
@@ -73504,13 +74692,7 @@ async function status() {
73504
74692
  console.log(source_default.bold(" Available Scan Depths:"));
73505
74693
  console.log("");
73506
74694
  console.log(source_default.green(" \u2713 ") + source_default.white("local") + source_default.dim(" Fast pattern matching (always free)"));
73507
- if (normalizedTier === "pro" || normalizedTier === "max") {
73508
- console.log(source_default.green(" \u2713 ") + source_default.white("verified") + source_default.dim(" AI validation (~70% fewer false positives)"));
73509
- console.log(source_default.dim(" ") + source_default.white("deep") + source_default.dim(" Multi-agent analysis (coming soon)"));
73510
- } else {
73511
- console.log(source_default.dim(" ") + source_default.white("verified") + source_default.dim(" AI validation (requires Pro)"));
73512
- console.log(source_default.dim(" ") + source_default.white("deep") + source_default.dim(" Multi-agent analysis (requires Pro)"));
73513
- }
74695
+ console.log(source_default.green(" \u2713 ") + source_default.white("verified") + source_default.dim(" AI validation (~70% fewer false positives)"));
73514
74696
  console.log("");
73515
74697
  console.log(source_default.dim(" Manage subscription: ") + source_default.cyan("https://oculum.dev/billing"));
73516
74698
  console.log("");
@@ -73687,7 +74869,7 @@ function showTopicList() {
73687
74869
  console.log(source_default.bold("\nOculum Help Topics\n"));
73688
74870
  console.log(source_default.dim("\u2500".repeat(50)));
73689
74871
  console.log();
73690
- console.log(source_default.cyan(" scan-modes ") + source_default.white("Compare local, verified, and deep scans"));
74872
+ console.log(source_default.cyan(" scan-modes ") + source_default.white("Compare local and verified scans"));
73691
74873
  console.log(source_default.cyan(" ci-setup ") + source_default.white("GitHub Actions and GitLab CI examples"));
73692
74874
  console.log(source_default.cyan(" config ") + source_default.white("Configuration file documentation"));
73693
74875
  console.log(source_default.cyan(" troubleshooting ") + source_default.white("Common issues and solutions"));
@@ -73699,7 +74881,7 @@ function showTopicList() {
73699
74881
  function showScanModes() {
73700
74882
  console.log(source_default.bold("\nScan Modes Comparison\n"));
73701
74883
  console.log(source_default.dim("\u2500".repeat(60) + "\n"));
73702
- console.log(source_default.green.bold(" LOCAL (Quick Scan)"));
74884
+ console.log(source_default.green.bold(" LOCAL (Pattern Matching)"));
73703
74885
  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"));
73704
74886
  console.log(source_default.white(" Cost: ") + source_default.green("Free"));
73705
74887
  console.log(source_default.white(" Speed: ") + source_default.white("~1000 files/second"));
@@ -73713,18 +74895,10 @@ function showScanModes() {
73713
74895
  console.log(source_default.white(" How: ") + source_default.dim("Pattern matching + AI validation"));
73714
74896
  console.log(source_default.white(" Best for: ") + source_default.dim("Pre-commit checks, PR reviews"));
73715
74897
  console.log(source_default.white(" Benefit: ") + source_default.dim("~70% fewer false positives\n"));
73716
- console.log(source_default.magenta.bold(" DEEP"));
73717
- 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"));
73718
- console.log(source_default.white(" Cost: ") + source_default.yellow("~$0.10 per 300 files"));
73719
- console.log(source_default.white(" Speed: ") + source_default.white("~60 seconds for 300 files"));
73720
- console.log(source_default.white(" How: ") + source_default.dim("Full semantic analysis with AI reasoning"));
73721
- console.log(source_default.white(" Best for: ") + source_default.dim("Security audits, release checks"));
73722
- console.log(source_default.white(" Benefit: ") + source_default.dim("Deepest analysis, remediation advice\n"));
73723
74898
  console.log(source_default.dim("\u2500".repeat(60)));
73724
74899
  console.log(source_default.bold("\nUsage Examples:\n"));
73725
74900
  console.log(source_default.dim(" $ oculum scan . ") + source_default.white("# Quick scan (free)"));
73726
74901
  console.log(source_default.dim(" $ oculum scan . -d verified ") + source_default.white("# AI-verified scan"));
73727
- console.log(source_default.dim(" $ oculum scan . -d deep ") + source_default.white("# Deep semantic analysis"));
73728
74902
  console.log();
73729
74903
  }
73730
74904
  function showCISetup() {
@@ -73787,7 +74961,7 @@ function showConfig() {
73787
74961
  }`));
73788
74962
  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"));
73789
74963
  console.log(source_default.bold(" Config Options\n"));
73790
- console.log(source_default.white(" depth ") + source_default.dim("Scan depth: local | verified | deep"));
74964
+ console.log(source_default.white(" depth ") + source_default.dim("Scan depth: local | verified"));
73791
74965
  console.log(source_default.white(" failOn ") + source_default.dim("Exit code 1 on: critical | high | medium | low | none"));
73792
74966
  console.log(source_default.white(" format ") + source_default.dim("Output format: terminal | json | sarif | markdown"));
73793
74967
  console.log(source_default.white(" output ") + source_default.dim("Output file path"));
@@ -73813,7 +74987,7 @@ function showTroubleshooting() {
73813
74987
  console.log(source_default.dim(" \u2192 Run `oculum login` to re-authenticate"));
73814
74988
  console.log(source_default.dim(" \u2192 Check key at https://oculum.dev/dashboard/api-keys\n"));
73815
74989
  console.log(source_default.white(' "Insufficient tier"'));
73816
- console.log(source_default.dim(" \u2192 Validated/deep scans require Pro subscription"));
74990
+ console.log(source_default.dim(" \u2192 Validated scans use AI credits from your plan"));
73817
74991
  console.log(source_default.dim(" \u2192 Visit https://oculum.dev/billing to upgrade\n"));
73818
74992
  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"));
73819
74993
  console.log(source_default.yellow.bold(" Network Errors\n"));
@@ -74109,7 +75283,7 @@ function shouldRunUI() {
74109
75283
  return isOcAlias || isUICommand;
74110
75284
  }
74111
75285
  var program2 = new Command();
74112
- program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.19").addHelpText("after", `
75286
+ program2.name("oculum").description("AI-native security scanner for detecting vulnerabilities in LLM-generated code").version("1.0.20").addHelpText("after", `
74113
75287
  Quick Start:
74114
75288
  $ oculum scan . Scan current directory (free)
74115
75289
  $ oculum show 1 View finding details