Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@
- **Windows CI failed at random on `EBUSY` while deleting a fake `.exe`.** Windows can hold a
just-run test binary for a moment, so cleanup in `cli.test.ts` and
`check-services-cwd.test.ts` now retries, as `infisical-status.test.ts` already did.
- **`kit check` reported Cargo workspace members as not covered.** In a repo whose
root `Cargo.toml` declares `[workspace] members`, the scan scope row warned that
each member crate was a separate project to scan on its own, although
osv-scanner and cargo audit read the root `Cargo.lock`, which pins every member.
Members (globs such as `crates/*` included, `exclude` honored) now count as
covered when a root `Cargo.lock` exists. A nested project that is not a member
still warns, and so do members without a lockfile. A root `pnpm-workspace.yaml`
now counts as declaring workspaces, as `package.json` `workspaces` already did.
- **`kit hooks add` promised to overwrite a hook it then left alone.** Over an
externally managed hook (husky, a committed `.githooks/pre-commit`) it warned
"kit will overwrite it", prompted, skipped the install, printed a success mark
Expand Down
64 changes: 64 additions & 0 deletions src/check-nested-projects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,70 @@ describe("checkScanScope", () => {
}
});

it("passes for a pnpm workspace root, declared in pnpm-workspace.yaml", async () => {
const dir = tree({
"package.json": JSON.stringify({ name: "root" }),
"pnpm-workspace.yaml": "packages:\n - 'packages/*'\n",
"packages/a/package.json": "{}",
});
try {
const r = await checkScanScope(dir);
assert.equal(r.status, "pass", r.detail);
assert.match(r.detail, /covered via workspaces/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

describe("checkScanScope: Cargo workspaces", () => {
// A Cargo workspace has one Cargo.lock at the root that pins every member, and that lockfile is
// what osv-scanner and cargo audit read. So the members are covered, but only the members, and
// only when the lockfile is there.
const cargoWorkspace = (extra: Record<string, string> = {}): string =>
tree({
"Cargo.toml": '[workspace]\nresolver = "2"\nmembers = ["crates/*", "src-tauri"]\n',
"crates/core/Cargo.toml": '[package]\nname = "core"\n',
"src-tauri/Cargo.toml": '[package]\nname = "app"\n',
...extra,
});

it("passes for a Cargo workspace whose members are locked in the root Cargo.lock", async () => {
const dir = cargoWorkspace({ "Cargo.lock": "version = 3\n" });
try {
const r = await checkScanScope(dir);
assert.equal(r.status, "pass", r.detail);
assert.match(r.detail, /Cargo workspace: crates\/core, src-tauri/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("still warns for Cargo workspace members when there is no root Cargo.lock to scan", async () => {
const dir = cargoWorkspace();
try {
const r = await checkScanScope(dir);
assert.equal(r.status, "warn", r.detail);
assert.match(r.detail, /2 nested project\(s\) NOT covered/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});

it("still warns for a nested project that is not a Cargo workspace member", async () => {
const dir = cargoWorkspace({ "Cargo.lock": "version = 3\n", "tools/web/package.json": "{}" });
try {
const r = await checkScanScope(dir);
assert.equal(r.status, "warn", r.detail);
// Only the non-member is named: the members are covered and listing them would mislead.
assert.match(r.detail, /1 nested project\(s\) NOT covered by this scan: tools\/web /);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});

describe("checkScanScope: sibling projects", () => {
it("warns at medium when a root project has uncovered siblings under it", async () => {
const dir = tree({
"package.json": JSON.stringify({ name: "root", dependencies: {} }),
Expand Down
70 changes: 65 additions & 5 deletions src/check-nested-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import { readdir, readFile, access } from "node:fs/promises";
import { join } from "node:path";
import { parse as parseToml } from "smol-toml";

import type { SecurityCheckResult } from "./check-security.js";

Expand Down Expand Up @@ -103,8 +104,17 @@ export async function findNestedProjects(root: string, maxDepth = 2): Promise<st
return found.sort();
}

/** True when the root package.json declares workspaces, so a root-level scan does cover the children. */
/**
* True when the root declares npm/yarn workspaces in package.json, or pnpm workspaces in
* pnpm-workspace.yaml, so a root-level scan does cover the children.
*/
async function declaresWorkspaces(root: string): Promise<boolean> {
try {
await access(join(root, "pnpm-workspace.yaml"));
return true;
} catch {
/* not pnpm, read package.json */
}
try {
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf-8")) as {
workspaces?: unknown;
Expand All @@ -115,13 +125,48 @@ async function declaresWorkspaces(root: string): Promise<boolean> {
}
}

/** `crates/*` style member patterns: `*` and `?` match within one path segment. */
function memberMatcher(pattern: string): RegExp {
const clean = pattern.replace(/^\.\//, "").replace(/\/+$/, "");
const body = clean
.split("")
.map((c) => (c === "*" ? "[^/]*" : c === "?" ? "[^/]" : c.replace(/[.+^${}()|[\]\\]/g, "\\$&")))
.join("");
return new RegExp(`^${body}$`);
}

/**
* Nested paths that a root Cargo workspace lists in `[workspace] members` (minus `exclude`).
*
* Only when a root Cargo.lock exists: osv-scanner and cargo audit read the lockfile, and a
* workspace's single Cargo.lock pins every member. Without it the root scan has nothing to read
* for those crates, so they stay uncovered.
*/
async function cargoWorkspaceMembers(root: string, nested: string[]): Promise<string[]> {
try {
await access(join(root, "Cargo.lock"));
const toml = parseToml(await readFile(join(root, "Cargo.toml"), "utf-8")) as {
workspace?: { members?: unknown; exclude?: unknown };
};
const list = (v: unknown): RegExp[] =>
Array.isArray(v) ? v.filter((m) => typeof m === "string").map(memberMatcher) : [];
const members = list(toml.workspace?.members);
const excluded = list(toml.workspace?.exclude);
return nested.filter((n) => members.some((m) => m.test(n)) && !excluded.some((m) => m.test(n)));
} catch {
return [];
}
}

export interface ScanScope {
/** Does the directory kit ran in have a manifest of its own? */
rootManifest: boolean;
/** Project directories found below it, relative paths. */
nested: string[];
/** Does the root manifest declare workspaces, so a root-level scan covers the children? */
workspaces: boolean;
/** Nested paths a root Cargo workspace locks in its Cargo.lock, so the root scan reads them. */
cargoMembers?: string[];
}

/** The facts, gathered once: the row below and the skip escalation both read them. */
Expand All @@ -131,6 +176,7 @@ export async function scanScopeFacts(root: string): Promise<ScanScope> {
rootManifest,
nested,
workspaces: rootManifest ? await declaresWorkspaces(root) : false,
cargoMembers: rootManifest ? await cargoWorkspaceMembers(root, nested) : [],
};
}

Expand Down Expand Up @@ -191,15 +237,21 @@ function say(
*
* The four cases are distinct, and only one of them is a problem:
* - nothing nested: the scope is this project, say so;
* - a root manifest declaring workspaces: the children are covered by the root scan;
* - a root manifest declaring workspaces: the children are covered by the root scan (a Cargo
* workspace covers only its locked members, so any other child still falls to the next case);
* - a root manifest without workspaces: the children are separate projects, not covered;
* - no root manifest at all: this verdict covers an empty directory. This is the false green.
*/
export async function checkScanScope(
root: string,
facts?: ScanScope,
): Promise<SecurityCheckResult> {
const { rootManifest, nested, workspaces } = facts ?? (await scanScopeFacts(root));
const {
rootManifest,
nested,
workspaces,
cargoMembers = [],
} = facts ?? (await scanScopeFacts(root));

if (nested.length === 0) {
return say(
Expand All @@ -208,16 +260,24 @@ export async function checkScanScope(
);
}

const list = nested.slice(0, 4).join(", ") + (nested.length > 4 ? `, +${nested.length - 4}` : "");
const show = (paths: string[]): string =>
paths.slice(0, 4).join(", ") + (paths.length > 4 ? `, +${paths.length - 4}` : "");
const list = show(nested);

if (rootManifest && workspaces) {
return say("pass", `${nested.length} nested package(s) covered via workspaces: ${list}`);
}

const uncovered = rootManifest ? nested.filter((n) => !cargoMembers.includes(n)) : nested;

if (rootManifest && uncovered.length === 0) {
return say("pass", `${nested.length} nested crate(s) covered via the Cargo workspace: ${list}`);
}

if (rootManifest) {
return say(
"warn",
`${nested.length} nested project(s) NOT covered by this scan: ${list} — run kit check in each`,
`${uncovered.length} nested project(s) NOT covered by this scan: ${show(uncovered)} — run kit check in each`,
{ severity: "medium", suggestion: `cd <project> && kit check --category security` },
);
}
Expand Down
Loading