fix(security): harden scan runtime boundaries

This commit is contained in:
Kyle McCleary
2026-07-29 18:47:51 -07:00
parent 80bdfdcbd4
commit 9314e200fe
10 changed files with 174 additions and 43 deletions
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, test, vi } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
@@ -55,6 +55,7 @@ beforeEach(async () => {
});
afterEach(async () => {
vi.restoreAllMocks();
unregisterCustomApis(MOCK_SOURCE_ID);
settings.cancelPendingSaves();
credentialStore?.close();
@@ -124,7 +125,6 @@ describe("native security coordinator", () => {
const terminal = await coordinator.wait(started.operationId);
expect(terminal.phase).toBe("completed");
expect(terminal.findingCount).toBe(1);
expect(mock.calls.length).toBeGreaterThan(0);
const bundle = await (await storeFactory()).getBundle(terminal.scanId);
expect(bundle?.scan.status).toBe("completed");
expect(bundle?.findings).toHaveLength(1);
@@ -136,6 +136,35 @@ describe("native security coordinator", () => {
expect(reopened.getSessionId()).toBeTruthy();
});
test("records a terminal failure when initial scan persistence fails", async () => {
const mock = createMockModel({ id: "security-mock", provider: "openai-codex" });
const store = await storeFactory();
const coordinator = new SecurityCoordinator(
{
cwd: repositoryRoot,
settings,
authStorage,
modelRegistry: new ModelRegistry(authStorage, path.join(temporaryRoot, "models.yml")),
activeModel: mock.model,
},
{
openStore: async () => store,
gitAdapter,
createSession: async () => {
throw new Error("session must not launch when persistence fails");
},
},
);
const plan = await coordinator.preflight({ credentialId, model: mock.model });
vi.spyOn(store, "putBundle").mockRejectedValue(new Error("security store unavailable"));
const started = await coordinator.start({ planId: plan.id });
await expect(coordinator.wait(started.operationId)).rejects.toThrow("security store unavailable");
expect(coordinator.status(started.operationId)).toMatchObject({
phase: "failed",
error: "security store unavailable",
});
});
test("cancellation before session launch has no inference side effects", async () => {
let sessionCreations = 0;
const mock = createMockModel({ id: "security-mock", provider: "openai-codex" });
@@ -2,7 +2,8 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { importSarifFile, SecurityStore } from "../../src/security";
import { pathToFileURL } from "node:url";
import { exportSecurityBundleToSarif, importSarif, importSarifFile, SecurityStore } from "../../src/security";
const FIXTURE = path.join(import.meta.dir, "..", "fixtures", "security", "generic-results.sarif");
let temporaryRoot = "";
@@ -65,5 +66,50 @@ describe("security history and dispositions", () => {
actor: "test-operator",
});
expect((await store.getFinding(bundle.scan.id, original.id))?.disposition).toEqual(updated.disposition);
const persisted = await store.getBundle(bundle.scan.id);
const persistedResult = (
persisted?.sarif?.runs as Array<{ results: Array<{ properties?: Record<string, unknown> }> }> | undefined
)?.[0]?.results[0];
expect(persistedResult?.properties?.disposition).toBe("false_positive");
});
test("SARIF disposition round-trips without changing its finding identity", async () => {
const bundle = await importSarif(
{
version: "2.1.0",
runs: [
{
tool: { driver: { name: "Fixture scanner" } },
results: [
{
ruleId: "fixture.rule",
message: { text: "fixture finding" },
properties: { disposition: "false_positive" },
},
],
},
],
},
{ repositoryRoot, createScanId: () => "secscan_sarifdisposition" },
);
const finding = bundle.findings[0];
if (!finding) throw new Error("expected imported finding");
expect(finding.disposition.status).toBe("false_positive");
const exported = exportSecurityBundleToSarif(bundle);
const result = (exported.runs as Array<{ results: Array<{ properties?: Record<string, unknown> }> }>)[0]
?.results[0];
expect(result?.properties?.disposition).toBe("false_positive");
expect(finding.id).toBe(bundle.scan.findingIds[0]);
});
test("SARIF base URI escapes repository path characters", async () => {
const specialRoot = path.join(temporaryRoot, "repo with #hash");
await fs.mkdir(specialRoot);
const bundle = await importSarif({ version: "2.1.0", runs: [] }, { repositoryRoot: specialRoot });
const exported = exportSecurityBundleToSarif(bundle);
const run = (exported.runs as Array<{ originalUriBaseIds: Record<string, { uri: string }> }>)[0];
expect(run?.originalUriBaseIds["%SRCROOT%"]?.uri).toBe(
pathToFileURL(`${await fs.realpath(specialRoot)}${path.sep}`).href,
);
});
});
@@ -214,6 +214,19 @@ describe("security preflight", () => {
await expect(plan()).rejects.toThrow("symbolic link");
});
test("a root-dot scoped target includes repository descendants", async () => {
const scoped = await plan({ kind: "scoped_path", includePaths: ["."] });
const repository = await plan();
expect(scoped.target.includePaths).toEqual(["."]);
expect(scoped.target.treeDigest).toBe(repository.target.treeDigest);
});
test("an empty scoped target is rejected before planning", async () => {
await expect(plan({ kind: "scoped_path", includePaths: [] })).rejects.toThrow(
"scoped_path security scans require at least one include path",
);
});
test("scope traversal is rejected", async () => {
for (const candidate of ["../outside", "src/../outside", "C:\\outside", "src\\..\\outside"]) {
await expect(plan({ kind: "scoped_path", includePaths: [candidate] })).rejects.toThrow("repository-relative");