From 82f2cfaa3e3ee712cc2a2083e74efe62d544141f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Sandstr=C3=B6m?= Date: Mon, 12 Oct 2026 15:45:15 +0200 Subject: [PATCH 1/2] fix(check): count Cargo workspace members as covered by the scan scope A root Cargo.toml with [workspace] members made the scan scope row warn that each member crate was an uncovered nested project, although osv-scanner and cargo audit read the root Cargo.lock, which pins every member. declaresWorkspaces only looked at package.json workspaces. Members (globs such as crates/* included, exclude honored) now count as covered when a root Cargo.lock exists. Non-members, and members without a lockfile, still warn. A root pnpm-workspace.yaml now counts as declaring workspaces too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013ft6L7xbdazPw8xqvxfPzs --- CHANGELOG.md | 8 ++++ src/check-nested-projects.test.ts | 60 ++++++++++++++++++++++++++ src/check-nested-projects.ts | 70 ++++++++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f04a40c..06f2bb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ### Fixed +- **`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 diff --git a/src/check-nested-projects.test.ts b/src/check-nested-projects.test.ts index d0e77cf1..db7c27e7 100644 --- a/src/check-nested-projects.test.ts +++ b/src/check-nested-projects.test.ts @@ -74,6 +74,66 @@ 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 }); + } + }); + + // 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 => + 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 }); + } + }); + it("warns at medium when a root project has uncovered siblings under it", async () => { const dir = tree({ "package.json": JSON.stringify({ name: "root", dependencies: {} }), diff --git a/src/check-nested-projects.ts b/src/check-nested-projects.ts index 4c80f3c6..2bc3792a 100644 --- a/src/check-nested-projects.ts +++ b/src/check-nested-projects.ts @@ -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"; @@ -103,8 +104,17 @@ export async function findNestedProjects(root: string, maxDepth = 2): Promise { + 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; @@ -115,6 +125,39 @@ async function declaresWorkspaces(root: string): Promise { } } +/** `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 { + 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; @@ -122,6 +165,8 @@ export interface ScanScope { 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. */ @@ -131,6 +176,7 @@ export async function scanScopeFacts(root: string): Promise { rootManifest, nested, workspaces: rootManifest ? await declaresWorkspaces(root) : false, + cargoMembers: rootManifest ? await cargoWorkspaceMembers(root, nested) : [], }; } @@ -191,7 +237,8 @@ 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. */ @@ -199,7 +246,12 @@ export async function checkScanScope( root: string, facts?: ScanScope, ): Promise { - const { rootManifest, nested, workspaces } = facts ?? (await scanScopeFacts(root)); + const { + rootManifest, + nested, + workspaces, + cargoMembers = [], + } = facts ?? (await scanScopeFacts(root)); if (nested.length === 0) { return say( @@ -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 && kit check --category security` }, ); } From 3749540b40b0d7a80900df7dd4dcb4cb405756ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20Sandstr=C3=B6m?= Date: Wed, 14 Oct 2026 15:36:54 +0200 Subject: [PATCH 2/2] test: split the checkScanScope describe so no function passes 80 lines Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01RSop9MyLof8XxLTkFnF4KG --- src/check-nested-projects.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/check-nested-projects.test.ts b/src/check-nested-projects.test.ts index db7c27e7..91dff019 100644 --- a/src/check-nested-projects.test.ts +++ b/src/check-nested-projects.test.ts @@ -88,7 +88,9 @@ describe("checkScanScope", () => { 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. @@ -133,7 +135,9 @@ describe("checkScanScope", () => { 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: {} }),