@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.
- package/dist/index.js +1282 -108
- 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
|
|
63578
|
-
|
|
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
|
|
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 >=
|
|
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 (${
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
68120
|
-
if (
|
|
69279
|
+
const { batches, totalFiles, truncated } = batchFilesForBackend(files, config.tier);
|
|
69280
|
+
if (truncated) {
|
|
68121
69281
|
spinner.warn(source_default.yellow(
|
|
68122
|
-
`Truncating scan from ${
|
|
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:
|
|
69285
|
+
console.log(source_default.dim(" Tip: Upgrade to Pro or Max for higher limits"));
|
|
68126
69286
|
console.log("");
|
|
68127
69287
|
}
|
|
68128
69288
|
}
|
|
68129
|
-
|
|
68130
|
-
|
|
68131
|
-
|
|
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 (
|
|
68141
|
-
result.filesSkipped =
|
|
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
|
|
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" ? "
|
|
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
|
|
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
|
|
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.
|
|
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"
|
|
72463
|
-
console.log(source_default.red(`Invalid depth: ${depth}`) + source_default.dim(" (use local
|
|
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("
|
|
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:
|
|
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: "
|
|
72841
|
-
{ cmd: "
|
|
72842
|
-
{ cmd: "
|
|
72843
|
-
{ cmd: "
|
|
72844
|
-
{ cmd: "
|
|
72845
|
-
{ cmd: "
|
|
72846
|
-
{ cmd: "
|
|
72847
|
-
{ cmd: "
|
|
72848
|
-
{ cmd: "
|
|
72849
|
-
{ cmd: "
|
|
72850
|
-
{ cmd: "
|
|
72851
|
-
{ cmd: "
|
|
72852
|
-
{ cmd: "
|
|
72853
|
-
{ cmd: "
|
|
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
|
|
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
|
|
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
|
|
72995
|
-
{ name: "--depth", description: "Scan depth
|
|
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
|
|
73028
|
-
{ name: "--depth", description: "Scan depth
|
|
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
|
|
73053
|
-
{ name: "--severity", description: "Filter by severity
|
|
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 (
|
|
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:
|
|
74262
|
+
label: c.name,
|
|
73102
74263
|
description: c.description,
|
|
73103
|
-
completion:
|
|
74264
|
+
completion: c.name + " "
|
|
73104
74265
|
}));
|
|
73105
74266
|
}
|
|
73106
74267
|
function getFlagSuggestions() {
|
|
73107
|
-
if (!input.
|
|
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
|
|
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 =
|
|
74333
|
+
const label = sg.label;
|
|
73151
74334
|
const desc = sg.description;
|
|
73152
|
-
const pad = Math.max(1,
|
|
73153
|
-
const maxDesc = cols - label.length - pad -
|
|
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 +=
|
|
74340
|
+
out += ` \x1B[36m\u25B8\x1B[0m \x1B[1m${label}\x1B[0m${" ".repeat(pad)}\x1B[2m${truncDesc}\x1B[22m`;
|
|
73158
74341
|
} else {
|
|
73159
|
-
out +=
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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.
|
|
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
|