pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
350 lines (347 loc) • 9.67 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
getScratchTreeGlobPatterns
} from "./chunk-TO6WL3GM.js";
import {
SecurityScanClient
} from "./chunk-7F72PXY2.js";
import {
safeSpawnAsync
} from "./chunk-VN2AXLEX.js";
import {
loadPiLensProjectConfig
} from "./chunk-DQ2NZ7C4.js";
// dist/clients/trivy-client.js
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { mkdtempSync } from "node:fs";
var EMPTY_RESULT = {
success: false,
findings: [],
secrets: [],
licenses: []
};
var SCAN_TIMEOUT_MS = 18e4;
var DEPENDENCY_MANIFESTS = [
// JS / TS
"package.json",
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"bun.lockb",
// Python
"pyproject.toml",
"requirements.txt",
"poetry.lock",
"Pipfile.lock",
// Java / JVM
"pom.xml",
"build.gradle",
"build.gradle.kts",
"gradle.lockfile",
// Go
"go.mod",
"go.sum",
// Rust
"Cargo.toml",
"Cargo.lock",
// .NET
"packages.lock.json",
// PHP
"composer.json",
"composer.lock",
// Ruby
"Gemfile",
"Gemfile.lock"
];
function hasAnyDependencyManifest(cwd) {
for (const manifest of DEPENDENCY_MANIFESTS) {
try {
if (fs.existsSync(path.join(cwd, manifest)))
return true;
} catch {
}
}
return false;
}
function isTrivyEnabled(cwd) {
try {
const config = loadPiLensProjectConfig(cwd);
const trivy = config.raw?.trivy;
return trivy?.enabled === true;
} catch {
return false;
}
}
function shouldScanTrivy(cwd) {
return isTrivyEnabled(cwd) && hasAnyDependencyManifest(cwd);
}
var SEVERITY_RANK = {
LOW: 0,
MEDIUM: 1,
HIGH: 2,
CRITICAL: 3
};
function resolveSeverityFloor(cwd) {
let raw;
try {
const config = loadPiLensProjectConfig(cwd);
raw = config.raw?.trivy?.minSeverity;
} catch {
raw = void 0;
}
const requested = typeof raw === "string" ? SEVERITY_RANK[raw.toUpperCase()] : void 0;
const floor = Math.min(requested ?? SEVERITY_RANK.HIGH, SEVERITY_RANK.HIGH);
return ["LOW", "MEDIUM", "HIGH", "CRITICAL"].filter((s) => SEVERITY_RANK[s] >= floor);
}
var TrivyClient = class extends SecurityScanClient {
constructor(verbose = false) {
super("trivy", verbose);
}
/** Static gates so callers can skip before constructing. */
static hasAnyDependencyManifest(cwd) {
return hasAnyDependencyManifest(cwd);
}
/** Full opt-in gate (config opt-in AND a dependency manifest). */
static shouldScan(cwd) {
return shouldScanTrivy(cwd);
}
/**
* Auto-install via the GitHub-release path (registered in
* `clients/installer/index.ts`) when trivy isn't already on PATH.
*/
doEnsureAvailable() {
return this.ensureViaInstaller(["--version"]);
}
/**
* Scan a directory tree for dependency CVEs.
*
* Skips early when the opt-in gate isn't satisfied. When trivy is
* unavailable, returns an empty (but successful) result rather than failing
* the session_start task. Re-entrancy safe: concurrent calls against the
* same root share a single process.
*/
async scan(cwd) {
const targetDir = path.resolve(cwd);
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
if (!shouldScanTrivy(targetDir)) {
return {
...EMPTY_RESULT,
success: true,
scannedAt,
summary: hasAnyDependencyManifest(targetDir) ? "trivy not enabled (set trivy.enabled in .pi-lens.json)" : "no dependency manifest at analysis root"
};
}
if (!await this.ensureAvailable()) {
return { ...EMPTY_RESULT, scannedAt, summary: "trivy not installed" };
}
return this.dedupeScan(targetDir, () => this.runScan(targetDir));
}
async runScan(cwd) {
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
const bin = this.binaryPath ?? "trivy";
const severities = resolveSeverityFloor(cwd);
const outDir = mkdtempSync(path.join(os.tmpdir(), "pi-lens-trivy-"));
const reportPath = path.join(outDir, "trivy-report.json");
try {
const result = await safeSpawnAsync(bin, [
"fs",
"--scanners",
"vuln,secret,license",
"--severity",
severities.join(","),
"--format",
"json",
"--output",
reportPath,
"--quiet",
"--no-progress",
...getScratchTreeGlobPatterns().flatMap((glob) => [
"--skip-dirs",
glob
]),
cwd
], { cwd, timeout: SCAN_TIMEOUT_MS });
if (result.error) {
this.log(`Scan error: ${result.error.message}`);
return {
...EMPTY_RESULT,
scannedAt,
summary: result.error.message.slice(0, 200)
};
}
if (!fs.existsSync(reportPath)) {
return {
...EMPTY_RESULT,
success: true,
scannedAt,
summary: (result.stderr ?? "").trim().split("\n")[0] || "no report produced"
};
}
const raw = fs.readFileSync(reportPath, "utf-8");
const findings = parseTrivyReport(raw);
const secrets = parseTrivySecrets(raw);
const licenses = parseTrivyLicenses(raw);
return {
success: true,
analyzed: true,
findings,
secrets,
licenses,
scannedAt
};
} catch (err) {
return {
...EMPTY_RESULT,
scannedAt,
summary: err instanceof Error ? err.message.slice(0, 200) : String(err)
};
} finally {
try {
fs.rmSync(outDir, { recursive: true, force: true });
} catch {
}
}
}
};
function normalizeSeverity(raw) {
const s = typeof raw === "string" ? raw.toUpperCase() : "";
if (s === "CRITICAL" || s === "HIGH" || s === "MEDIUM" || s === "LOW") {
return s;
}
return "UNKNOWN";
}
function parseTrivyReport(raw) {
if (!raw.trim())
return [];
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
const results = parsed?.Results;
if (!Array.isArray(results))
return [];
const findings = [];
for (const resultEntry of results) {
if (!resultEntry || typeof resultEntry !== "object")
continue;
const r = resultEntry;
const target = typeof r.Target === "string" ? r.Target : void 0;
const vulns = r.Vulnerabilities;
if (!Array.isArray(vulns))
continue;
for (const vulnEntry of vulns) {
if (!vulnEntry || typeof vulnEntry !== "object")
continue;
const v = vulnEntry;
const vulnerabilityId = typeof v.VulnerabilityID === "string" ? v.VulnerabilityID : void 0;
const pkgName = typeof v.PkgName === "string" ? v.PkgName : void 0;
if (!vulnerabilityId || !pkgName)
continue;
findings.push({
vulnerabilityId,
pkgName,
installedVersion: typeof v.InstalledVersion === "string" ? v.InstalledVersion : void 0,
fixedVersion: typeof v.FixedVersion === "string" && v.FixedVersion.trim() ? v.FixedVersion : void 0,
severity: normalizeSeverity(v.Severity),
title: typeof v.Title === "string" ? v.Title : void 0,
primaryUrl: typeof v.PrimaryURL === "string" ? v.PrimaryURL : void 0,
target
});
}
}
return findings;
}
function parseTrivySecrets(raw) {
if (!raw.trim())
return [];
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
const results = parsed?.Results;
if (!Array.isArray(results))
return [];
const secrets = [];
for (const resultEntry of results) {
if (!resultEntry || typeof resultEntry !== "object")
continue;
const r = resultEntry;
const target = typeof r.Target === "string" ? r.Target : void 0;
const rows = r.Secrets;
if (!target || !Array.isArray(rows))
continue;
for (const row of rows) {
if (!row || typeof row !== "object")
continue;
const s = row;
const ruleId = typeof s.RuleID === "string" ? s.RuleID : void 0;
const line = typeof s.StartLine === "number" ? s.StartLine : void 0;
if (!ruleId || line == null)
continue;
secrets.push({
ruleId,
file: target,
line,
title: typeof s.Title === "string" ? s.Title : void 0
});
}
}
return secrets;
}
function parseTrivyLicenses(raw) {
if (!raw.trim())
return [];
let parsed;
try {
parsed = JSON.parse(raw);
} catch {
return [];
}
const results = parsed?.Results;
if (!Array.isArray(results))
return [];
const licenses = [];
for (const resultEntry of results) {
if (!resultEntry || typeof resultEntry !== "object")
continue;
const r = resultEntry;
const target = typeof r.Target === "string" ? r.Target : void 0;
const rows = r.Licenses;
if (!Array.isArray(rows))
continue;
for (const row of rows) {
if (!row || typeof row !== "object")
continue;
const l = row;
const license = typeof l.Name === "string" ? l.Name : void 0;
if (!license)
continue;
const filePath = typeof l.FilePath === "string" && l.FilePath ? l.FilePath : target;
const pkgName = typeof l.PkgName === "string" && l.PkgName ? l.PkgName : filePath ?? "unknown";
licenses.push({
license,
pkgName,
severity: normalizeSeverity(l.Severity),
category: typeof l.Category === "string" ? l.Category : void 0,
filePath
});
}
}
return licenses;
}
export {
hasAnyDependencyManifest,
isTrivyEnabled,
shouldScanTrivy,
resolveSeverityFloor,
TrivyClient,
parseTrivyReport,
parseTrivySecrets,
parseTrivyLicenses
};