Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,18 @@ export function parseArgs(argv: string[]): {
options.checkMaintenance = true;
continue;
}
if (arg === "--incomplete-policy") {
const val = argv[++i];
if (val !== "warn" && val !== "error") throw new Error("--incomplete-policy requires 'warn' or 'error'");
options.incompletePolicy = val;
continue;
}
if (arg.startsWith("--incomplete-policy=")) {
const val = arg.slice("--incomplete-policy=".length);
if (val !== "warn" && val !== "error") throw new Error("--incomplete-policy requires 'warn' or 'error'");
options.incompletePolicy = val;
continue;
}
if (arg.startsWith("-")) {
throw new Error(`Unknown option: ${arg}`);
}
Expand Down
10 changes: 7 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import { getCliVersion } from "./utils/version-info.js";
import { getNetworkErrorHint, offlineDbSyncHint, createCertAwareFetch } from "./utils/network.js";
import { formatAdvisoryDbFreshness } from "./utils/time.js";
import { pluralize, nearestCommand } from "./utils/string.js";
import type { FetchLike, ParsedOptions } from "./types.js";
import { EXIT_ERROR } from "./types.js";
import type { FetchLike, ParsedOptions, ExitCode } from "./types.js";
import { EXIT_ERROR, EXIT_FINDINGS, EXIT_OK } from "./types.js";
import {
formatAdvisorySourceLine,
formatHintLines,
Expand Down Expand Up @@ -781,7 +781,11 @@ if (parsedArgs) {
reachesFailOn(maintenanceFindings, options.failOn);
// In fix mode, remaining transitive findings cannot be auto-fixed.
// Exiting non-zero would prevent the Action PR step from running.
const exitCode = shouldFail && !options.fix ? 1 : 0;
let exitCode: ExitCode = shouldFail && !options.fix ? EXIT_FINDINGS : EXIT_OK;

if (options.incompletePolicy === "error" && !scanState.completeness.complete) {
exitCode = EXIT_ERROR;
}

// Emit scan.finished event and close audit-log
auditLogHandle.emit({
Expand Down
6 changes: 5 additions & 1 deletion src/scan/multi-folder-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,11 @@ export async function handleMultiFolderScan(params: {
allSorted.some(f => severityOrder[f.severity] >= severityOrder[failLevel]) ||
reachesFailOn(allOverrideFindings, params.options.failOn) ||
reachesFailOn(allMaintenanceFindings, params.options.failOn);
const exitCode = shouldFail ? EXIT_FINDINGS : EXIT_OK;
let exitCode: ExitCode = shouldFail ? EXIT_FINDINGS : EXIT_OK;

if (params.options.incompletePolicy === "error" && !results.every(r => r.completeness.complete)) {
exitCode = EXIT_ERROR;
}

auditLog.emit({
ts: new Date(scanFinishedAt).toISOString(),
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ export type ParsedOptions = {
rule?: string;
/** --allow-private-osv-url - allow --osv-url to resolve to private/reserved IPs. */
allowPrivateOsvUrl?: boolean;
incompletePolicy?: string;
};

/**
Expand Down
13 changes: 8 additions & 5 deletions tests/e2e/commands-and-exit-codes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,26 +228,27 @@ describe("commands + meta", () => {

it("config set/show/unset round-trips under a temp HOME", () => {
// getConfigDir() = path.join(os.homedir(), ".cve-lite-cli"); on Linux
// os.homedir() honors $HOME, so a temp HOME isolates this from the real
// os.homedir() honors $HOME (and USERPROFILE on Windows), so a temp HOME isolates this from the real
// user config. Verified that nothing lands under the real ~/.cve-lite-cli.
const home = scratch();
const certDir = mkProject({
"cert.pem": "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n",
});
const certPath = join(certDir, "cert.pem");
try {
const set = runCli(["config", "set", "ca-cert", certPath], { env: { HOME: home } });
const env = { HOME: home, USERPROFILE: home };
const set = runCli(["config", "set", "ca-cert", certPath], { env });
expect(set.status).toBe(0);
expect(existsSync(join(home, ".cve-lite-cli", "config.json"))).toBe(true);

const show = runCli(["config", "show"], { env: { HOME: home } });
const show = runCli(["config", "show"], { env });
expect(show.status).toBe(0);
expect(show.stdout).toContain(certPath);

const unset = runCli(["config", "unset", "ca-cert"], { env: { HOME: home } });
const unset = runCli(["config", "unset", "ca-cert"], { env });
expect(unset.status).toBe(0);

const showAfter = runCli(["config", "show"], { env: { HOME: home } });
const showAfter = runCli(["config", "show"], { env });
expect(showAfter.status).toBe(0);
expect(showAfter.stdout).toMatch(/No configuration set/);
} finally {
Expand All @@ -268,6 +269,8 @@ describe("exit codes", () => {
}
});



it("1: orphan override (OA001) above the fail-on threshold", () => {
// package.json declares an override for `gone`, but the lockfile resolves a
// different package, so OA001 (override target not in resolved tree) fires
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/detectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ describe("e2e detectors OA001-OA008 fire through the real binary", () => {
const pd001 = findings.find((f: any) => f.ruleId === "PD001");
expect(pd001.package.name).toBe("js-yaml");
expect(pd001.severity).toBe("high");
expect(pd001.details).toContain("src/index.ts");
expect(pd001.details.replace(/\\/g, "/")).toContain("src/index.ts");
});

it("PD002 transitive-only phantom: import backed only by transitive dep fires PD002", () => {
Expand All @@ -312,6 +312,6 @@ describe("e2e detectors OA001-OA008 fire through the real binary", () => {
const pd002 = findings.find((f: any) => f.ruleId === "PD002");
expect(pd002.package.name).toBe("semver");
expect(pd002.severity).toBe("medium");
expect(pd002.details).toContain("src/index.ts");
expect(pd002.details.replace(/\\/g, "/")).toContain("src/index.ts");
});
});
2 changes: 1 addition & 1 deletion tests/e2e/global-setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default function build() {
existsSync(distEntry) &&
statSync(distEntry).mtimeMs >= newestMtime(join(repoRoot, "src"));
if (!fresh) {
execFileSync("npm", ["run", "build"], { cwd: repoRoot, stdio: "inherit" });
execFileSync("npm", ["run", "build"], { cwd: repoRoot, stdio: "inherit", shell: true });
}

const seededDb = join(repoRoot, "tests", "fixtures", "advisories", "seeded.db");
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,5 @@ export function installedManifest(dir: string, name: string, manifest: Record<st

/** Build dist if a test needs it explicitly (globalSetup already does this). */
export function ensureBuilt(): void {
execFileSync("npm", ["run", "build"], { cwd: join(here, "..", ".."), stdio: "ignore" });
execFileSync("npm", ["run", "build"], { cwd: join(here, "..", ".."), stdio: "ignore", shell: true });
}
4 changes: 2 additions & 2 deletions tests/multi-folder-scan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,8 +606,8 @@ describe("handleMultiFolderScan - ratchet / baseline", () => {
});

expect(writeBaselineMock).toHaveBeenCalledTimes(2);
expect(writeBaselineMock).toHaveBeenCalledWith("/project/a", expect.any(Array));
expect(writeBaselineMock).toHaveBeenCalledWith("/project/b", expect.any(Array));
expect(writeBaselineMock).toHaveBeenCalledWith(path.join("/project", "a"), expect.any(Array));
expect(writeBaselineMock).toHaveBeenCalledWith(path.join("/project", "b"), expect.any(Array));
expect(exitCode).toBe(EXIT_OK);
const output = consoleLogMock.mock.calls.flat().join("\n");
expect(output).toMatch(/a\/: Baseline saved/i);
Expand Down