Files
rea/src/domain/evidence.test.ts
T

258 lines
8.8 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 { 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,
);
});
});