mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 23:06:18 +00:00
Move shared function, dossier, inventory, reference and output semantics from the legacy Hopper module to native domain owners. Migrate provider, adapter, test and verifier consumers directly.
603 lines
20 KiB
TypeScript
603 lines
20 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js";
|
|
import { INVESTIGATION_EXAMPLES } from "../contracts/investigationExamples.js";
|
|
import {
|
|
artifactComparisonResultSchema,
|
|
compareArtifacts,
|
|
} from "./artifactComparison.js";
|
|
import { findChangedBehavior } from "./changedBehavior.js";
|
|
import { ARTIFACT_COMPARISON_PROVIDER } from "./comparisonEvidenceIdentity.js";
|
|
import { createEvidence, type Evidence } from "./evidence.js";
|
|
import { functionComparisonResultSchema } from "./functionComparisonSchemas.js";
|
|
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
|
import { FUNCTION_COMPARISON_EXAMPLE } from "../contracts/functionComparisonExample.js";
|
|
import { functionDossierSchema } from "./native/functionDossier.js";
|
|
import { compareFunctions } from "./functionComparison.js";
|
|
import { functionSnapshotFixture } from "./functionDossierEvidence.fixture.js";
|
|
import {
|
|
correlateStaticAndRuntime,
|
|
staticRuntimeCorrelationInputSchema,
|
|
staticRuntimeCorrelationResultSchema,
|
|
} from "./staticRuntimeCorrelation.js";
|
|
|
|
const staticComparison =
|
|
INVESTIGATION_EXAMPLES.find_changed_behavior.comparisons[0];
|
|
if (staticComparison === undefined)
|
|
throw new Error("Missing static comparison fixture");
|
|
|
|
const artifactComparison = createEvidence(
|
|
undefined,
|
|
ARTIFACT_COMPARISON_PROVIDER,
|
|
{
|
|
predicateType: "rea.artifact-comparison",
|
|
operation: "compare_artifacts",
|
|
parameters: {
|
|
left_evidence_ids: [ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id],
|
|
right_evidence_ids: [ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id],
|
|
},
|
|
result: compareArtifacts(
|
|
ARTIFACT_COMPARISON_EXAMPLE.left,
|
|
ARTIFACT_COMPARISON_EXAMPLE.right,
|
|
),
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
evidenceLinks: [
|
|
ARTIFACT_COMPARISON_EXAMPLE.left.evidence_id,
|
|
ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id,
|
|
],
|
|
},
|
|
);
|
|
|
|
const capture = (digit: string): Evidence =>
|
|
createEvidence(
|
|
undefined,
|
|
{ id: "fixture", name: "Fixture", version: "1" },
|
|
{
|
|
predicateType: "rea.process-capture",
|
|
operation: "capture_process_scenario",
|
|
parameters: {},
|
|
result: { digit },
|
|
authority: "controlled-replay",
|
|
environment: {
|
|
id: `fixture-${digit}`,
|
|
platform: "test",
|
|
architecture: "test",
|
|
isolation: "process",
|
|
},
|
|
},
|
|
);
|
|
|
|
const leftCapture = capture("1");
|
|
const rightCapture = capture("2");
|
|
|
|
const processComparison = (
|
|
terminal: "unchanged" | "changed" | "unknown" | "truncated",
|
|
): Evidence => {
|
|
const result = {
|
|
status: terminal,
|
|
terminal,
|
|
interaction: terminal === "truncated" ? "truncated" : "unchanged",
|
|
exit: terminal === "truncated" ? "truncated" : "unchanged",
|
|
filesystem: terminal === "truncated" ? "truncated" : "unchanged",
|
|
process: terminal === "truncated" ? "truncated" : "unchanged",
|
|
first_divergence:
|
|
terminal === "changed"
|
|
? {
|
|
status: "found",
|
|
dimension: "terminal",
|
|
index: 0,
|
|
left_at_ms: 0,
|
|
right_at_ms: 0,
|
|
left: "left",
|
|
right: "right",
|
|
}
|
|
: terminal === "truncated" || terminal === "unknown"
|
|
? { status: "unknown", reason: "Incomplete fixture." }
|
|
: { status: "none" },
|
|
limitations: terminal === "unknown" ? ["Terminal unavailable."] : [],
|
|
} satisfies JsonValue;
|
|
return createEvidence(
|
|
undefined,
|
|
{
|
|
id: "rea-process",
|
|
name: "REA deterministic process harness",
|
|
version: "3",
|
|
},
|
|
{
|
|
predicateType: "rea.process-comparison",
|
|
operation: "compare_process_captures",
|
|
parameters: {
|
|
left_evidence_id: leftCapture.evidence_id,
|
|
right_evidence_id: rightCapture.evidence_id,
|
|
},
|
|
result,
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
evidenceLinks: [leftCapture.evidence_id, rightCapture.evidence_id],
|
|
},
|
|
);
|
|
};
|
|
|
|
const mapping = (
|
|
runtime: Evidence,
|
|
expected: "cochanged" | "static_only" | "runtime_only" | "both_unchanged",
|
|
dimension: string = "terminal",
|
|
) => ({
|
|
static_comparisons: [staticComparison],
|
|
runtime_comparisons: [runtime],
|
|
mappings: [
|
|
{
|
|
static: {
|
|
comparison_evidence_id: staticComparison.evidence_id,
|
|
selector: {
|
|
kind: "function" as const,
|
|
dimension: "pseudocode" as const,
|
|
},
|
|
},
|
|
runtime: {
|
|
comparison_evidence_id: runtime.evidence_id,
|
|
dimension,
|
|
},
|
|
side_alignment: "left_to_left" as const,
|
|
hypothesis: {
|
|
statement: "The static edit is associated with terminal behavior.",
|
|
expected_pattern: expected,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
it("keeps text, assembly and unavailable CFG caveats on their selected correlations", () => {
|
|
const source = FUNCTION_COMPARISON_EXAMPLE.left;
|
|
const left = createEvidence(
|
|
{ path: "/tmp/unavailable-cfg", sha256: "a".repeat(64), format: "mach-o" },
|
|
source.provider,
|
|
{
|
|
operation: source.operation,
|
|
parameters: source.parameters,
|
|
result: {
|
|
...functionDossierSchema.parse(source.normalized_result),
|
|
basic_blocks: [{ start: "0x1000", end: "0x1000", successors: [] }],
|
|
},
|
|
confidence: source.confidence,
|
|
authority: source.authority,
|
|
},
|
|
);
|
|
|
|
const right = FUNCTION_COMPARISON_EXAMPLE.right;
|
|
const computed = compareFunctions(
|
|
functionSnapshotFixture(left),
|
|
functionSnapshotFixture(right),
|
|
);
|
|
const comparison = createEvidence(undefined, staticComparison.provider, {
|
|
predicateType: "rea.function-comparison",
|
|
operation: "compare_functions",
|
|
parameters: {
|
|
left_evidence_id: left.evidence_id,
|
|
right_evidence_id: right.evidence_id,
|
|
},
|
|
result: computed,
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
limitations: computed.limitations,
|
|
evidenceLinks: [left.evidence_id, right.evidence_id],
|
|
});
|
|
const input = mapping(processComparison("unchanged"), "static_only");
|
|
const result = correlateStaticAndRuntime({
|
|
...input,
|
|
static_comparisons: [comparison],
|
|
mappings: ["pseudocode", "assembly", "cfg"].map((dimension) => ({
|
|
...input.mappings[0],
|
|
static: {
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
selector: { kind: "function", dimension },
|
|
},
|
|
})),
|
|
});
|
|
expect(result.status).toBe("unknown");
|
|
const caveats = new Map(
|
|
result.correlations.items.map((item) => [
|
|
item.static.selector.kind === "function"
|
|
? item.static.selector.dimension
|
|
: "artifact",
|
|
item.limitations.join(" "),
|
|
]),
|
|
);
|
|
expect(caveats.get("pseudocode")).toMatch(/generated-name normalization/u);
|
|
expect(caveats.get("pseudocode")).not.toMatch(
|
|
/Assembly is opaque|CFG addresses/u,
|
|
);
|
|
expect(caveats.get("assembly")).toMatch(/Assembly is opaque/u);
|
|
expect(caveats.get("assembly")).not.toMatch(/CFG addresses/u);
|
|
expect(caveats.get("cfg")).toMatch(/CFG addresses/u);
|
|
expect(caveats.get("cfg")).not.toMatch(
|
|
/generated-name normalization|Assembly is opaque/u,
|
|
);
|
|
});
|
|
|
|
describe("static/runtime correlation", () => {
|
|
it("accepts real runtime dimensions and rejects removed protocol dimensions", () => {
|
|
const runtime = processComparison("unchanged");
|
|
expect(
|
|
correlateStaticAndRuntime(mapping(runtime, "static_only", "interaction")),
|
|
).toMatchObject({ status: "correlated" });
|
|
for (const dimension of ["protocol", "shim"] as const)
|
|
expect(
|
|
staticRuntimeCorrelationInputSchema.safeParse(
|
|
mapping(runtime, "static_only", dimension),
|
|
).success,
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects duplicate static comparison selection", () => {
|
|
const runtime = processComparison("changed");
|
|
const base = mapping(runtime, "cochanged");
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
static_comparisons: [staticComparison, staticComparison],
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "AnalysisInputError",
|
|
issues: [expect.objectContaining({ path: ["static_comparisons", 1] })],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects a static comparison with an unsupported operation", () => {
|
|
const runtime = processComparison("changed");
|
|
const base = mapping(runtime, "cochanged");
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
static_comparisons: [leftCapture],
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "AnalysisInputError",
|
|
issues: [expect.objectContaining({ path: ["static_comparisons", 0] })],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("classifies malformed source Evidence as an integrity failure", () => {
|
|
const runtime = processComparison("changed");
|
|
const base = mapping(runtime, "cochanged");
|
|
const malformedEvidence = {
|
|
...staticComparison,
|
|
evidence_id: "ev_invalid",
|
|
};
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
static_comparisons: [malformedEvidence],
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "EvidenceIntegrityError",
|
|
cause: expect.any(Error),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("static/runtime correlation outcomes", () => {
|
|
it("keeps consistent cochange as a hypothesis with complete citations", () => {
|
|
const runtime = processComparison("changed");
|
|
const result = correlateStaticAndRuntime(mapping(runtime, "cochanged"));
|
|
expect(staticRuntimeCorrelationResultSchema.parse(result)).toMatchObject({
|
|
status: "correlated",
|
|
summary: { hypotheses: 1, contradictions: 0, unresolved: 0 },
|
|
correlations: {
|
|
items: [
|
|
{
|
|
observed_pattern: "cochanged",
|
|
classification: "hypothesis",
|
|
side_alignment: "left_to_left",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(result.correlations.items[0]?.evidence_links).toEqual(
|
|
expect.arrayContaining([
|
|
staticComparison.evidence_id,
|
|
runtime.evidence_id,
|
|
leftCapture.evidence_id,
|
|
rightCapture.evidence_id,
|
|
]),
|
|
);
|
|
expect(result.limitations.join(" ")).toMatch(
|
|
/does not establish causality/u,
|
|
);
|
|
expect(result.correlations.items[0]?.limitations.join(" ")).toMatch(
|
|
/generated-name normalization/u,
|
|
);
|
|
});
|
|
|
|
it("classifies an explicit expectation mismatch as contradiction", () => {
|
|
const result = correlateStaticAndRuntime(
|
|
mapping(processComparison("unchanged"), "cochanged"),
|
|
);
|
|
expect(result).toMatchObject({
|
|
status: "contradicted",
|
|
summary: { hypotheses: 0, contradictions: 1, unresolved: 0 },
|
|
correlations: {
|
|
items: [
|
|
{ observed_pattern: "static_only", classification: "contradiction" },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("lets unknown and truncation dominate rather than imply equivalence", () => {
|
|
const unknown = correlateStaticAndRuntime(
|
|
mapping(processComparison("unknown"), "cochanged"),
|
|
);
|
|
const truncated = correlateStaticAndRuntime(
|
|
mapping(processComparison("truncated"), "cochanged"),
|
|
);
|
|
expect(unknown).toMatchObject({
|
|
status: "unknown",
|
|
correlations: { items: [{ classification: "unresolved_branch" }] },
|
|
});
|
|
expect(truncated).toMatchObject({
|
|
status: "truncated",
|
|
correlations: { items: [{ observed_pattern: "truncated" }] },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("static/runtime correlation selection", () => {
|
|
it("returns all correlations order-stably and rejects duplicates and absent selectors", () => {
|
|
const runtime = processComparison("changed");
|
|
const base = mapping(runtime, "cochanged");
|
|
const second = {
|
|
...base.mappings[0],
|
|
runtime: { ...base.mappings[0]!.runtime, dimension: "exit" as const },
|
|
hypothesis: {
|
|
statement: "The edit is associated with exit behavior.",
|
|
expected_pattern: "static_only" as const,
|
|
},
|
|
};
|
|
const forward = correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [base.mappings[0], second],
|
|
});
|
|
const reversed = correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [second, base.mappings[0]],
|
|
});
|
|
expect(reversed).toEqual(forward);
|
|
expect(forward.correlations.items).toHaveLength(2);
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [base.mappings[0], base.mappings[0]],
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "AnalysisInputError",
|
|
operation: "correlate_static_and_runtime",
|
|
userMessage: expect.stringContaining("duplicate mappings"),
|
|
issues: [expect.objectContaining({ path: ["mappings", 1] })],
|
|
}),
|
|
);
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [
|
|
{
|
|
...base.mappings[0],
|
|
static: {
|
|
...base.mappings[0]!.static,
|
|
selector: { kind: "function", dimension: "assembly" },
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [
|
|
{
|
|
...base.mappings[0],
|
|
static: {
|
|
comparison_evidence_id: runtime.evidence_id,
|
|
selector: { kind: "function", dimension: "pseudocode" },
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "AnalysisInputError",
|
|
userMessage: expect.stringContaining("absent static comparison"),
|
|
issues: [
|
|
expect.objectContaining({
|
|
path: ["mappings", 0, "static", "comparison_evidence_id"],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [
|
|
{
|
|
...base.mappings[0],
|
|
runtime: {
|
|
...base.mappings[0]!.runtime,
|
|
comparison_evidence_id: staticComparison.evidence_id,
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "AnalysisInputError",
|
|
issues: [
|
|
expect.objectContaining({
|
|
path: ["mappings", 0, "runtime", "comparison_evidence_id"],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("static/runtime correlation Evidence validation", () => {
|
|
it("rejects comparison citations that disagree with source parameters", () => {
|
|
const runtime = processComparison("changed");
|
|
const forged = createEvidence(undefined, runtime.provider, {
|
|
predicateType: runtime.predicate_type,
|
|
operation: runtime.operation,
|
|
parameters: {
|
|
left_evidence_id: leftCapture.evidence_id,
|
|
right_evidence_id: staticComparison.evidence_id,
|
|
},
|
|
result: runtime.normalized_result,
|
|
confidence: runtime.confidence,
|
|
authority: runtime.authority,
|
|
evidenceLinks: runtime.evidence_links,
|
|
});
|
|
const input = mapping(forged, "cochanged");
|
|
expect(() => correlateStaticAndRuntime(input)).toThrowError(
|
|
expect.toSatisfy(
|
|
(error: { _tag?: unknown; userMessage?: unknown; cause?: unknown }) =>
|
|
error._tag === "EvidenceIntegrityError" &&
|
|
typeof error.userMessage === "string" &&
|
|
error.userMessage.includes(
|
|
"closure disagrees with its source parameters",
|
|
) &&
|
|
error.cause === undefined,
|
|
),
|
|
);
|
|
});
|
|
|
|
it("retains invalid authenticated result payloads as Evidence failures", () => {
|
|
const valid = processComparison("changed");
|
|
const malformed = createEvidence(undefined, valid.provider, {
|
|
predicateType: valid.predicate_type,
|
|
operation: valid.operation,
|
|
parameters: valid.parameters,
|
|
result: { status: "changed" },
|
|
confidence: valid.confidence,
|
|
authority: valid.authority,
|
|
evidenceLinks: valid.evidence_links,
|
|
});
|
|
expect(() =>
|
|
correlateStaticAndRuntime(mapping(malformed, "cochanged")),
|
|
).toThrowError(
|
|
expect.objectContaining({
|
|
_tag: "EvidenceIntegrityError",
|
|
cause: expect.any(Error),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("static/runtime correlation citation closure", () => {
|
|
it("accepts extra parent citations for changed behavior but rejects them for correlation", () => {
|
|
const runtime = processComparison("changed");
|
|
const extra = staticComparison.evidence_id;
|
|
const extended = createEvidence(undefined, runtime.provider, {
|
|
predicateType: runtime.predicate_type,
|
|
operation: runtime.operation,
|
|
parameters: runtime.parameters,
|
|
result: runtime.normalized_result,
|
|
confidence: runtime.confidence,
|
|
authority: runtime.authority,
|
|
evidenceLinks: [...runtime.evidence_links, extra],
|
|
});
|
|
const aggregated = findChangedBehavior({ comparisons: [extended] });
|
|
expect(aggregated.result.behavior_status).toBe("observed_changed");
|
|
expect(aggregated.result.evidence_links).toContain(extra);
|
|
expect(() =>
|
|
correlateStaticAndRuntime(mapping(extended, "cochanged")),
|
|
).toThrowError(
|
|
expect.toSatisfy(
|
|
(error: { _tag?: unknown; userMessage?: unknown; cause?: unknown }) =>
|
|
error._tag === "EvidenceIntegrityError" &&
|
|
error.userMessage ===
|
|
"Comparison Evidence closure disagrees with its source parameters" &&
|
|
error.cause === undefined,
|
|
),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
["function", staticComparison],
|
|
["artifact", artifactComparison],
|
|
] as const)(
|
|
"rejects authenticated %s citations outside the top-level Evidence closure",
|
|
(_kind, base) => {
|
|
const outside = `ev_${"f".repeat(64)}`;
|
|
let result: JsonValue;
|
|
if (base.operation === "compare_functions") {
|
|
const parsed = functionComparisonResultSchema.parse(
|
|
base.normalized_result,
|
|
);
|
|
const dimensions = parsed.dimensions.map((dimension) => ({
|
|
...dimension,
|
|
evidence_links: [...dimension.evidence_links, outside],
|
|
}));
|
|
result = jsonValueSchema.parse(
|
|
functionComparisonResultSchema.parse({
|
|
...parsed,
|
|
dimensions,
|
|
changes: dimensions.filter(({ status }) => status !== "unchanged"),
|
|
}),
|
|
);
|
|
} else {
|
|
const parsed = artifactComparisonResultSchema.parse(
|
|
base.normalized_result,
|
|
);
|
|
expect(parsed.changes.length).toBeGreaterThan(0);
|
|
result = jsonValueSchema.parse({
|
|
...parsed,
|
|
changes: parsed.changes.map((change) => ({
|
|
...change,
|
|
evidence_links: [...change.evidence_links, outside],
|
|
})),
|
|
});
|
|
}
|
|
const danglingNested = createEvidence(undefined, base.provider, {
|
|
predicateType: base.predicate_type,
|
|
operation: base.operation,
|
|
parameters: base.parameters,
|
|
result,
|
|
confidence: base.confidence,
|
|
authority: base.authority,
|
|
evidenceLinks: base.evidence_links,
|
|
});
|
|
const input = mapping(processComparison("changed"), "cochanged");
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...input,
|
|
static_comparisons: [danglingNested],
|
|
mappings: input.mappings.map((item) => ({
|
|
...item,
|
|
static: {
|
|
comparison_evidence_id: danglingNested.evidence_id,
|
|
selector:
|
|
base.operation === "compare_functions"
|
|
? { kind: "function", dimension: "pseudocode" }
|
|
: { kind: "artifact", logical_path: "." },
|
|
},
|
|
})),
|
|
}),
|
|
).toThrowError(
|
|
expect.toSatisfy(
|
|
(error: { _tag?: unknown; message?: unknown; cause?: unknown }) =>
|
|
error._tag === "EvidenceIntegrityError" &&
|
|
error.message ===
|
|
"Comparison finding cites Evidence outside its top-level closure" &&
|
|
error.cause === undefined,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
});
|