* fix(domain): bound JSON depth and oversized payloads at parsing boundaries jsonValueSchema re-walked hostile input through z.json()'s recursive core, so a deeply nested payload from an MCP tool argument, inline evidence, provider result, or keyed archive threw RangeError: Maximum call stack size exceeded instead of a validation issue. The schema now runs an iterative depth guard first and keeps z.json() for full JSON semantics behind it, which also turns cyclic values into validation errors. The 1000-edge limit stays far below the range where the recursive core overflows its stack (measured between 2000 and 3000 on Node 24) while leaving room for legitimately deep target-derived trees such as compiled NIB view hierarchies, and the guard narrows with a typed record check instead of any or unchecked casts. The non-recursive JSON Schema advertisement stays intact on both the public and standard projection paths. Two payload sizes get explicit budgets tied to their producer constraints: web text artifacts reject beyond the 16 MiB source budget the web tooling already advertises, and native UI screenshot base64 beyond the 64 MiB helper output budget. Regenerated product catalog, skill catalog digest, and completion-ledger manifest follow the schema-boundary change. * fix(validation): preserve depth diagnostics across process inputs --------- Co-authored-by: N0zoM1z0 <1705004398@qq.com>
283 lines
9.6 KiB
TypeScript
283 lines
9.6 KiB
TypeScript
import { fc, it } from "@fast-check/vitest";
|
|
import { describe, expect } from "vitest";
|
|
|
|
import { createAnalysisProfile } from "./analysisProfile.js";
|
|
import type { BinaryTarget } from "./binaryTarget.js";
|
|
import { createEvidence, evidenceSchema, parseEvidence } from "./evidence.js";
|
|
import { MAX_JSON_DEPTH } from "./jsonValue.js";
|
|
import { createEvidenceBundle } from "./evidenceBundle.js";
|
|
|
|
const TARGET: BinaryTarget = {
|
|
path: "/tmp/fixture",
|
|
sha256: "a".repeat(64),
|
|
kind: "executable",
|
|
format: "mach-o",
|
|
architecture: "arm64",
|
|
availableArchitectures: ["arm64"],
|
|
};
|
|
const PROVIDER = { id: "fixture", name: "Fixture provider", version: "1" };
|
|
const PROFILE = createAnalysisProfile(PROVIDER, { loader: "default" });
|
|
|
|
it("snapshots caller-owned payloads before deriving their identity", () => {
|
|
const result = { nested: { values: ["observed"] } };
|
|
const rawResult = { entries: [{ value: "raw" }] };
|
|
const parameters = { selected: ["target"] };
|
|
const evidence = createEvidence(TARGET, PROVIDER, {
|
|
operation: "inspect",
|
|
parameters,
|
|
result,
|
|
rawResult,
|
|
});
|
|
result.nested.values.push("later");
|
|
const rawEntry = rawResult.entries[0];
|
|
if (rawEntry !== undefined) rawEntry.value = "changed";
|
|
parameters.selected.push("other");
|
|
expect(evidence.normalized_result).toEqual({
|
|
nested: { values: ["observed"] },
|
|
});
|
|
expect(evidence.raw_result).toEqual({ entries: [{ value: "raw" }] });
|
|
expect(evidence.parameters).toEqual({ selected: ["target"] });
|
|
expect(parseEvidence(evidence)).toEqual(evidence);
|
|
expect(() =>
|
|
parseEvidence({ ...evidence, normalized_result: result }),
|
|
).toThrow(/semantic identifier/u);
|
|
});
|
|
|
|
describe("analysis evidence identity", () => {
|
|
it("normalizes prototype-named parameter keys", () => {
|
|
const evidence = createEvidence(TARGET, PROVIDER, {
|
|
operation: "health",
|
|
parameters: Object.fromEntries([["__proto__", false]]),
|
|
result: true,
|
|
});
|
|
expect(parseEvidence(evidence)).toEqual(evidence);
|
|
});
|
|
|
|
it.prop([
|
|
fc.dictionary(
|
|
fc.string({ minLength: 1, maxLength: 12 }),
|
|
fc.oneof(fc.string(), fc.integer(), fc.boolean(), fc.constant(null)),
|
|
),
|
|
])("canonicalizes parameter key order", (parameters) => {
|
|
const reversed = Object.fromEntries(Object.entries(parameters).reverse());
|
|
const first = createEvidence(TARGET, PROVIDER, {
|
|
operation: "health",
|
|
parameters,
|
|
result: true,
|
|
});
|
|
const second = createEvidence(TARGET, PROVIDER, {
|
|
operation: "health",
|
|
parameters: reversed,
|
|
result: true,
|
|
});
|
|
expect(second.evidence_id).toBe(first.evidence_id);
|
|
});
|
|
|
|
it.prop([fc.string({ minLength: 1 }), fc.string({ minLength: 1 })])(
|
|
"keeps identity path-independent and provider-sensitive",
|
|
(firstPath, secondPath) => {
|
|
const first = createEvidence(
|
|
{ ...TARGET, path: `/first/${firstPath}/artifact` },
|
|
PROVIDER,
|
|
{ operation: "health", parameters: {}, result: true },
|
|
);
|
|
const moved = createEvidence(
|
|
{ ...TARGET, path: `/second/${secondPath}/artifact` },
|
|
PROVIDER,
|
|
{ operation: "health", parameters: {}, result: true },
|
|
);
|
|
const changedProvider = createEvidence(
|
|
{ ...TARGET, path: `/second/${secondPath}/artifact` },
|
|
{ ...PROVIDER, id: `${PROVIDER.id}-other` },
|
|
{ operation: "health", parameters: {}, result: true },
|
|
);
|
|
expect(moved.evidence_id).toBe(first.evidence_id);
|
|
expect(changedProvider.evidence_id).not.toBe(first.evidence_id);
|
|
},
|
|
);
|
|
|
|
it("creates deterministic provider-neutral Evidence", () => {
|
|
const observation = {
|
|
operation: "procedure_info",
|
|
parameters: { document: null, procedure: "0x1000" },
|
|
result: { name: "main" },
|
|
rawResult: { token: "<redacted:token>" },
|
|
} as const;
|
|
const evidence = createEvidence(TARGET, PROVIDER, observation);
|
|
expect(evidenceSchema.parse(evidence)).toEqual(evidence);
|
|
expect(parseEvidence(evidence)).toEqual(evidence);
|
|
expect(evidence).toMatchObject({
|
|
provider: PROVIDER,
|
|
subject: { digest: { sha256: "a".repeat(64) } },
|
|
confidence: "observed",
|
|
authority: "shipped-artifact",
|
|
raw_result: { token: "<redacted:token>" },
|
|
normalized_result: { name: "main" },
|
|
});
|
|
expect(evidence.evidence_id).toMatch(/^ev_[a-f0-9]{64}$/u);
|
|
expect(createEvidence(TARGET, PROVIDER, observation)).toEqual(evidence);
|
|
});
|
|
|
|
it("preserves legacy records while binding profiled Evidence to its profile", () => {
|
|
const legacy = createEvidence(TARGET, PROVIDER, {
|
|
operation: "procedure_info",
|
|
parameters: { procedure: "main" },
|
|
result: { name: "main" },
|
|
});
|
|
expect(parseEvidence(legacy)).toEqual(legacy);
|
|
|
|
const profiled = createEvidence(TARGET, PROVIDER, {
|
|
operation: "procedure_info",
|
|
parameters: { procedure: "main" },
|
|
result: { name: "main" },
|
|
analysisProfile: PROFILE,
|
|
});
|
|
expect(profiled).toMatchObject({ analysis_profile: PROFILE });
|
|
expect(profiled.evidence_id).not.toBe(legacy.evidence_id);
|
|
expect(() =>
|
|
createEvidence(
|
|
TARGET,
|
|
{ ...PROVIDER, id: "other" },
|
|
{
|
|
operation: "procedure_info",
|
|
parameters: {},
|
|
result: null,
|
|
analysisProfile: PROFILE,
|
|
},
|
|
),
|
|
).toThrow(/profile provider/u);
|
|
});
|
|
|
|
it("excludes local paths but includes redacted raw results in identity", () => {
|
|
const observation = {
|
|
operation: "health",
|
|
parameters: {},
|
|
result: true,
|
|
rawResult: { pid: 100 },
|
|
} as const;
|
|
const first = createEvidence(TARGET, PROVIDER, observation);
|
|
const moved = createEvidence(
|
|
{ ...TARGET, path: "/other/renamed-fixture" },
|
|
PROVIDER,
|
|
observation,
|
|
);
|
|
expect(moved.evidence_id).toBe(first.evidence_id);
|
|
expect(
|
|
createEvidence(TARGET, PROVIDER, {
|
|
...observation,
|
|
rawResult: { pid: 200 },
|
|
}).evidence_id,
|
|
).not.toBe(first.evidence_id);
|
|
});
|
|
|
|
it("rejects semantic tampering", () => {
|
|
const evidence = createEvidence(TARGET, PROVIDER, {
|
|
operation: "health",
|
|
parameters: {},
|
|
result: true,
|
|
});
|
|
expect(() =>
|
|
parseEvidence({ ...evidence, normalized_result: false }),
|
|
).toThrow("semantic identifier");
|
|
});
|
|
});
|
|
|
|
it("binds an explicit unknown-subject reason into the semantic Evidence identity", () => {
|
|
const reason = "Caller excluded the observed digest.";
|
|
const observation = { operation: "health", parameters: {}, result: true };
|
|
const explicit = createEvidence(undefined, PROVIDER, {
|
|
...observation,
|
|
subjectUnavailableReason: reason,
|
|
});
|
|
const defaulted = createEvidence(undefined, PROVIDER, observation);
|
|
expect(explicit.subject).toBeNull();
|
|
expect(explicit.limitations).toEqual([reason]);
|
|
expect(defaulted.limitations).toEqual([
|
|
"Artifact identity is unavailable for this observation.",
|
|
]);
|
|
expect(explicit.evidence_id).not.toBe(defaulted.evidence_id);
|
|
expect(parseEvidence(explicit)).toEqual(explicit);
|
|
});
|
|
|
|
it("derives byte-stable bundle manifests independent of record order", () => {
|
|
const artifactEvidence = createEvidence(TARGET, PROVIDER, {
|
|
operation: "health",
|
|
parameters: {},
|
|
result: true,
|
|
});
|
|
const captureEvidence = createEvidence(
|
|
undefined,
|
|
{ id: "process", name: "Process capture", version: "1" },
|
|
{
|
|
predicateType: "rea.process-capture",
|
|
operation: "capture_process_scenario",
|
|
parameters: {},
|
|
result: { exit: 0 },
|
|
authority: "controlled-replay",
|
|
environment: {
|
|
id: "linux-x64",
|
|
platform: "linux",
|
|
architecture: "x64",
|
|
isolation: "process",
|
|
},
|
|
},
|
|
);
|
|
const forward = createEvidenceBundle([artifactEvidence, captureEvidence]);
|
|
const reverse = createEvidenceBundle([captureEvidence, artifactEvidence]);
|
|
expect(JSON.stringify(reverse)).toBe(JSON.stringify(forward));
|
|
expect(forward).toMatchObject({
|
|
artifacts: [{ digest: { sha256: TARGET.sha256 }, format: "mach-o" }],
|
|
providers: [{ id: "fixture" }, { id: "process" }],
|
|
environments: [{ id: "linux-x64" }],
|
|
scenarios: [{ evidence_id: captureEvidence.evidence_id }],
|
|
captures: [{ evidence_id: captureEvidence.evidence_id }],
|
|
});
|
|
});
|
|
|
|
describe("DOS analysis evidence identity", () => {
|
|
it("retains DOS MZ identity independently of PE identity", () => {
|
|
const target = {
|
|
path: TARGET.path,
|
|
sha256: TARGET.sha256,
|
|
kind: "executable" as const,
|
|
format: "dos-mz" as const,
|
|
architecture: "x86" as const,
|
|
availableArchitectures: ["x86" as const],
|
|
};
|
|
const observation = { operation: "health", parameters: {}, result: true };
|
|
const evidence = createEvidence(target, PROVIDER, observation);
|
|
expect(parseEvidence(evidence).subject).toMatchObject({
|
|
format: "dos-mz",
|
|
architecture: "x86",
|
|
});
|
|
expect(evidence.evidence_id).not.toBe(
|
|
createEvidence({ ...target, format: "pe" }, PROVIDER, observation)
|
|
.evidence_id,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("evidence parameter depth bound", () => {
|
|
it("rejects evidence whose parameters nest past the JSON depth limit", () => {
|
|
const evidence = createEvidence(TARGET, PROVIDER, {
|
|
operation: "health",
|
|
parameters: {},
|
|
result: true,
|
|
});
|
|
let value: unknown = 1;
|
|
for (let index = 0; index <= MAX_JSON_DEPTH; index += 1)
|
|
value = { nested: value };
|
|
const result = evidenceSchema.safeParse({
|
|
...evidence,
|
|
parameters: { attack: value },
|
|
});
|
|
expect(result.success).toBe(false);
|
|
if (result.success) return;
|
|
expect(
|
|
result.error.issues.some((issue) =>
|
|
issue.message.includes("maximum nesting depth"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|