fix(housekeeping): harden audited command dispatch
This commit is contained in:
1 parent
6aed71a8b2
commit
796d009c07
7 files changed
+775
-124
No files matched your search
@@ -122,12 +122,8 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
error: { code: "FORBIDDEN" },
|
||||
});
|
||||
expect(executed).toBe(false);
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual([
|
||||
"intent",
|
||||
"denied",
|
||||
]);
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual(["denied"]);
|
||||
expect(audit.entries[0]?.correlationId).toBe(result.correlationId);
|
||||
expect(audit.entries[1]?.correlationId).toBe(result.correlationId);
|
||||
});
|
||||
|
||||
it("rejects invalid input before the command can execute", async () => {
|
||||
@@ -182,10 +178,7 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
},
|
||||
});
|
||||
expect(executed).toBe(false);
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual([
|
||||
"intent",
|
||||
"denied",
|
||||
]);
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual(["denied"]);
|
||||
});
|
||||
|
||||
it("uses actor, IP, command ID, and the approved command limit", async () => {
|
||||
@@ -392,4 +385,310 @@ describe("dispatchHousekeepingCommand", () => {
|
||||
]);
|
||||
expect(audit.entries[1]?.correlationId).toBe(result.correlationId);
|
||||
});
|
||||
it("runs sensitive validation and rate preflight before intent and execution", async () => {
|
||||
const events: string[] = [];
|
||||
const audit = auditRecorder(events);
|
||||
const tracedContext = {
|
||||
...capabilityContext(),
|
||||
hasAny: (...slugs: string[]) => {
|
||||
events.push("capability");
|
||||
return slugs.includes("admin.settings.edit");
|
||||
},
|
||||
};
|
||||
register(
|
||||
baseCommand("system.dispatch.preflight-order", {
|
||||
risk: "sensitive",
|
||||
requiresReason: true,
|
||||
input: z.object({ enabled: z.boolean() }).superRefine(() => {
|
||||
events.push("input");
|
||||
}),
|
||||
execute: async (context) => {
|
||||
events.push("execute");
|
||||
return ok(null, context.correlationId);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await dispatchHousekeepingCommand(
|
||||
{
|
||||
commandId: "system.dispatch.preflight-order",
|
||||
input: { enabled: true },
|
||||
reason: "Approved change",
|
||||
},
|
||||
dependencies({
|
||||
context: tracedContext,
|
||||
audit: audit.writer,
|
||||
rateLimit: async () => {
|
||||
events.push("rate");
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(events).toEqual([
|
||||
"capability",
|
||||
"input",
|
||||
"rate",
|
||||
"audit:intent",
|
||||
"execute",
|
||||
"audit:success",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["capability", "system.dispatch.preflight-denied"],
|
||||
["input", "system.dispatch.preflight-invalid"],
|
||||
["rate", "system.dispatch.preflight-limited"],
|
||||
] as const)(
|
||||
"writes only denied evidence when sensitive %s preflight rejects",
|
||||
async (boundary, commandId) => {
|
||||
const audit = auditRecorder();
|
||||
let executed = false;
|
||||
register(
|
||||
baseCommand(commandId, {
|
||||
risk: "sensitive",
|
||||
input: z.object({ count: z.number().positive() }),
|
||||
execute: async (context) => {
|
||||
executed = true;
|
||||
return ok(null, context.correlationId);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{
|
||||
commandId,
|
||||
input: { count: boundary === "input" ? -1 : 1 },
|
||||
},
|
||||
dependencies({
|
||||
context:
|
||||
boundary === "capability"
|
||||
? capabilityContext([])
|
||||
: capabilityContext(),
|
||||
audit: audit.writer,
|
||||
rateLimit: async () => boundary !== "rate",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ ok: false });
|
||||
expect(executed).toBe(false);
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual(["denied"]);
|
||||
expect(audit.entries[0]?.correlationId).toBe(result.correlationId);
|
||||
},
|
||||
);
|
||||
|
||||
it("writes generic server-owned evidence for a validated unknown command", async () => {
|
||||
const audit = auditRecorder();
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{ commandId: "system.dispatch.unknown-audited", input: {} },
|
||||
dependencies({ audit: audit.writer }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "NOT_FOUND" },
|
||||
});
|
||||
expect(audit.entries).toEqual([
|
||||
{
|
||||
userId: 42,
|
||||
action: "housekeeping.command.dispatch",
|
||||
target: "system.dispatch.unknown-audited",
|
||||
correlationId: result.correlationId,
|
||||
outcome: "denied",
|
||||
ipAddress: "198.51.100.8",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
null,
|
||||
[],
|
||||
Object.assign(Object.create({ inherited: true }), {
|
||||
commandId: "system.dispatch.unknown",
|
||||
input: {},
|
||||
}),
|
||||
{ commandId: "system.dispatch.unknown", input: {}, risk: "safe" },
|
||||
{ commandId: `system.${"x".repeat(200)}`, input: {} },
|
||||
{
|
||||
commandId: "system.dispatch.unknown",
|
||||
input: {},
|
||||
reason: "x".repeat(1001),
|
||||
},
|
||||
])(
|
||||
"returns a correlated validation result for malformed runtime request %#",
|
||||
async (request) => {
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
request as never,
|
||||
dependencies(),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
code: "VALIDATION",
|
||||
messageKey: "errors.housekeeping.validation",
|
||||
},
|
||||
});
|
||||
expect(result.correlationId).toMatch(/^[0-9a-f-]{36}$/i);
|
||||
},
|
||||
);
|
||||
|
||||
it("sanitizes a capability exception and writes failure evidence", async () => {
|
||||
const audit = auditRecorder();
|
||||
register(
|
||||
baseCommand("system.dispatch.capability-exception", {
|
||||
input: z.object({}),
|
||||
execute: async (context) => ok(null, context.correlationId),
|
||||
}),
|
||||
);
|
||||
const brokenContext = {
|
||||
...capabilityContext(),
|
||||
hasAny: () => {
|
||||
throw new Error("capability secret exposed");
|
||||
},
|
||||
};
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{ commandId: "system.dispatch.capability-exception", input: {} },
|
||||
dependencies({ context: brokenContext, audit: audit.writer }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTERNAL", messageKey: "errors.housekeeping.internal" },
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("secret exposed");
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual(["failure"]);
|
||||
});
|
||||
|
||||
it("sanitizes a schema exception and writes failure evidence", async () => {
|
||||
const audit = auditRecorder();
|
||||
register(
|
||||
baseCommand("system.dispatch.schema-exception", {
|
||||
input: z.preprocess(() => {
|
||||
throw new Error("schema secret exposed");
|
||||
}, z.object({})),
|
||||
execute: async (context) => ok(null, context.correlationId),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{ commandId: "system.dispatch.schema-exception", input: {} },
|
||||
dependencies({ audit: audit.writer }),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTERNAL", messageKey: "errors.housekeeping.internal" },
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("secret exposed");
|
||||
expect(audit.entries.map((entry) => entry.outcome)).toEqual(["failure"]);
|
||||
});
|
||||
|
||||
it("throws completed-operation evidence and persists partial when success audit rejects", async () => {
|
||||
const attempts: string[] = [];
|
||||
const persisted: string[] = [];
|
||||
register(
|
||||
baseCommand("system.dispatch.success-audit-rejection", {
|
||||
input: z.object({}),
|
||||
execute: async (context) =>
|
||||
ok({ changed: true }, context.correlationId),
|
||||
}),
|
||||
);
|
||||
|
||||
const dispatch = dispatchHousekeepingCommand(
|
||||
{ commandId: "system.dispatch.success-audit-rejection", input: {} },
|
||||
dependencies({
|
||||
audit: {
|
||||
write: async (entry) => {
|
||||
attempts.push(entry.outcome ?? "missing");
|
||||
if (entry.outcome === "success") {
|
||||
throw new Error("success audit unavailable");
|
||||
}
|
||||
persisted.push(entry.outcome ?? "missing");
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(dispatch).rejects.toMatchObject({
|
||||
name: "AuditOutcomePersistenceError",
|
||||
operationCompleted: true,
|
||||
operationResult: { ok: true, data: { changed: true } },
|
||||
});
|
||||
expect(attempts).toEqual(["success", "partial"]);
|
||||
expect(persisted).toEqual(["partial"]);
|
||||
});
|
||||
|
||||
it("preserves a returned command failure when failure audit rejects", async () => {
|
||||
const attempts: string[] = [];
|
||||
register(
|
||||
baseCommand("system.dispatch.returned-failure-audit-rejection", {
|
||||
input: z.object({}),
|
||||
execute: async () =>
|
||||
fail(
|
||||
"DEPENDENCY_UNAVAILABLE",
|
||||
"errors.housekeeping.dependencyUnavailable",
|
||||
"wrong-correlation",
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{
|
||||
commandId: "system.dispatch.returned-failure-audit-rejection",
|
||||
input: {},
|
||||
},
|
||||
dependencies({
|
||||
audit: {
|
||||
write: async (entry) => {
|
||||
attempts.push(entry.outcome ?? "missing");
|
||||
throw new Error("failure audit unavailable");
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||
});
|
||||
expect(result.correlationId).not.toBe("wrong-correlation");
|
||||
expect(attempts).toEqual(["failure"]);
|
||||
});
|
||||
|
||||
it("preserves a sanitized throwing-command failure when failure audit rejects", async () => {
|
||||
const attempts: string[] = [];
|
||||
register(
|
||||
baseCommand("system.dispatch.thrown-failure-audit-rejection", {
|
||||
input: z.object({}),
|
||||
execute: async () => {
|
||||
throw new Error("command database secret exposed");
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await dispatchHousekeepingCommand(
|
||||
{
|
||||
commandId: "system.dispatch.thrown-failure-audit-rejection",
|
||||
input: {},
|
||||
},
|
||||
dependencies({
|
||||
audit: {
|
||||
write: async (entry) => {
|
||||
attempts.push(entry.outcome ?? "missing");
|
||||
throw new Error("audit replacement secret exposed");
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { code: "INTERNAL", messageKey: "errors.housekeeping.internal" },
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("secret exposed");
|
||||
expect(attempts).toEqual(["failure"]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user