352 lines
13 KiB
TypeScript
352 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { projectManagedApplicationGraphEvidence } from "../../../src/application/managed/ManagedApplicationGraphService.js";
|
|
import { managedApplicationGraphReferenceInputSchema } from "../../../src/contracts/managed/managedWorkflowToolContracts.js";
|
|
import { MANAGED_APPLICATION_GRAPH_EXAMPLE } from "../../../src/contracts/managed/managedWorkflowExamples.js";
|
|
import { traceApplicationFeatureEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js";
|
|
import { MANAGED_STATIC_PROVIDER } from "../../../src/application/InvestigationProviders.js";
|
|
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { parseJavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js";
|
|
import { managedApplicationGraphResultSchema } from "../../../src/domain/managed/managedApplicationGraph.js";
|
|
import { inspectManagedArtifactBytes } from "../../../src/dotnet/ManagedArtifactInspector.js";
|
|
import { inspectManagedMembersBytes } from "../../../src/dotnet/ManagedMemberInspector.js";
|
|
import { inspectManagedNativeBoundariesBytes } from "../../../src/dotnet/ManagedNativeBoundaryInspector.js";
|
|
import {
|
|
buildManagedPeFixture,
|
|
managedPeFixtureTarget,
|
|
} from "../../../src/dotnet/ManagedPe.fixture.js";
|
|
|
|
const createManagedInteropEvidence = () => {
|
|
const bytes = buildManagedPeFixture({
|
|
pinvoke: {
|
|
moduleName: "user32.dll",
|
|
importName: "MessageBoxW",
|
|
mappingFlags: 0x0345,
|
|
},
|
|
});
|
|
const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe");
|
|
const managedArtifact = inspectManagedArtifactBytes(bytes, binary);
|
|
const members = inspectManagedMembersBytes(bytes, binary);
|
|
const boundaries = inspectManagedNativeBoundariesBytes(bytes, binary);
|
|
|
|
return {
|
|
artifactEvidence: createEvidence(binary, MANAGED_STATIC_PROVIDER, {
|
|
operation: "inspect_managed_artifact",
|
|
parameters: {},
|
|
result: managedArtifact,
|
|
rawResult: null,
|
|
limitations: managedArtifact.limitations,
|
|
locations: [{ kind: "artifact-path", path: binary.path }],
|
|
}),
|
|
memberEvidence: createEvidence(binary, MANAGED_STATIC_PROVIDER, {
|
|
operation: "inspect_managed_members",
|
|
parameters: {},
|
|
result: members,
|
|
rawResult: null,
|
|
limitations: members.limitations,
|
|
locations: [{ kind: "artifact-path", path: binary.path }],
|
|
}),
|
|
boundaryEvidence: createEvidence(binary, MANAGED_STATIC_PROVIDER, {
|
|
operation: "inspect_managed_native_boundaries",
|
|
parameters: {},
|
|
result: boundaries,
|
|
rawResult: null,
|
|
limitations: boundaries.limitations,
|
|
locations: [{ kind: "artifact-path", path: binary.path }],
|
|
}),
|
|
};
|
|
};
|
|
|
|
describe("managed application graph projection", () => {
|
|
it("projects managed metadata and native declarations into authenticated graph Evidence", () => {
|
|
const { artifactEvidence, memberEvidence, boundaryEvidence } =
|
|
createManagedInteropEvidence();
|
|
|
|
const evidence = projectManagedApplicationGraphEvidence({
|
|
managed_artifact: artifactEvidence,
|
|
managed_members: memberEvidence,
|
|
managed_native_boundaries: boundaryEvidence,
|
|
});
|
|
|
|
expect(
|
|
evidence.ok,
|
|
evidence.ok
|
|
? undefined
|
|
: `${JSON.stringify(evidence.error)} cause=${String(evidence.error.cause)}`,
|
|
).toBe(true);
|
|
if (!evidence.ok) throw new Error("projection failed");
|
|
const parsed = parseEvidence(evidence.value);
|
|
expect(parsed).toMatchObject({
|
|
operation: "project_managed_application_graph",
|
|
predicate_type: "rea.managed-application-graph",
|
|
provider: { id: "rea-dotnet-workflows" },
|
|
confidence: "inferred",
|
|
authority: "analyst-inference",
|
|
evidence_links: [
|
|
artifactEvidence.evidence_id,
|
|
memberEvidence.evidence_id,
|
|
boundaryEvidence.evidence_id,
|
|
],
|
|
});
|
|
const result = managedApplicationGraphResultSchema.parse(
|
|
parsed.normalized_result,
|
|
);
|
|
const graph = parseJavaScriptApplicationGraph(result.graph);
|
|
expect(result.summary).toMatchObject({
|
|
assemblies: 1,
|
|
modules: 1,
|
|
types: 1,
|
|
methods: 1,
|
|
fields: 1,
|
|
pinvoke_imports: 1,
|
|
});
|
|
expect(graph.nodes.map(({ kind }) => kind)).toEqual(
|
|
expect.arrayContaining([
|
|
"artifact",
|
|
"managed-assembly",
|
|
"managed-module",
|
|
"managed-type",
|
|
"managed-method",
|
|
"managed-field",
|
|
"managed-pinvoke-import",
|
|
]),
|
|
);
|
|
const allEvidenceIds = [
|
|
artifactEvidence.evidence_id,
|
|
memberEvidence.evidence_id,
|
|
boundaryEvidence.evidence_id,
|
|
].sort();
|
|
const observedEvidenceIds = (kind: string): readonly string[] => {
|
|
const observation = graph.nodes.find((node) => node.kind === kind)
|
|
?.observations[0];
|
|
if (observation === undefined)
|
|
throw new Error(`Expected ${kind} observation`);
|
|
return observation.evidence.evidence_ids;
|
|
};
|
|
expect(observedEvidenceIds("artifact")).toEqual(allEvidenceIds);
|
|
expect(observedEvidenceIds("managed-assembly")).toEqual([
|
|
artifactEvidence.evidence_id,
|
|
]);
|
|
expect(observedEvidenceIds("managed-module")).toEqual([
|
|
artifactEvidence.evidence_id,
|
|
]);
|
|
for (const kind of ["managed-type", "managed-method", "managed-field"])
|
|
expect(observedEvidenceIds(kind)).toEqual([memberEvidence.evidence_id]);
|
|
expect(observedEvidenceIds("managed-pinvoke-import")).toEqual([
|
|
boundaryEvidence.evidence_id,
|
|
]);
|
|
for (const edge of graph.edges.filter(
|
|
({ relation }) => relation === "contains",
|
|
))
|
|
expect(edge.evidence.evidence_ids).toEqual(allEvidenceIds);
|
|
const method = graph.nodes.find(
|
|
({ kind, observations }) =>
|
|
kind === "managed-method" &&
|
|
observations[0]?.label === "Fixture.Program.Main",
|
|
);
|
|
expect(method).toBeDefined();
|
|
const importEdge = graph.edges.find(
|
|
({ source_node_id, relation, target_node_id }) =>
|
|
source_node_id === method?.node_id &&
|
|
relation === "imports" &&
|
|
graph.nodes.find(({ node_id: id }) => id === target_node_id)?.kind ===
|
|
"managed-pinvoke-import",
|
|
);
|
|
expect(importEdge).toBeDefined();
|
|
expect(importEdge?.evidence.evidence_ids).toEqual(
|
|
[memberEvidence.evidence_id, boundaryEvidence.evidence_id].sort(),
|
|
);
|
|
expect(
|
|
graph.nodes.flatMap(({ observations }) =>
|
|
observations.map(({ evidence }) => evidence.authority),
|
|
),
|
|
).toContain("managed-static-analysis");
|
|
|
|
const trace = traceApplicationFeatureEvidence({
|
|
application: parsed,
|
|
native_observations: [],
|
|
seed: {
|
|
kind: "string",
|
|
value: "MessageBoxW",
|
|
match: "exact",
|
|
case_sensitive: true,
|
|
},
|
|
direction: "incoming",
|
|
});
|
|
expect(trace.ok, trace.ok ? undefined : JSON.stringify(trace.error)).toBe(
|
|
true,
|
|
);
|
|
if (!trace.ok) throw new Error("trace failed");
|
|
expect(trace.value.normalized_result).toMatchObject({
|
|
source_evidence_id: parsed.evidence_id,
|
|
summary: { matched_seeds: 1 },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("managed application graph request", () => {
|
|
it("accepts any nonempty set of inline sources and rejects duplicates", () => {
|
|
const evidence = MANAGED_APPLICATION_GRAPH_EXAMPLE.managed_members;
|
|
expect(
|
|
managedApplicationGraphReferenceInputSchema.safeParse({
|
|
managed_members: evidence,
|
|
}).success,
|
|
).toBe(true);
|
|
expect(
|
|
managedApplicationGraphReferenceInputSchema.safeParse({}).success,
|
|
).toBe(false);
|
|
expect(
|
|
managedApplicationGraphReferenceInputSchema.safeParse({
|
|
managed_members: evidence,
|
|
managed_native_boundaries: evidence,
|
|
}).success,
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("managed application graph coverage", () => {
|
|
it("rejects managed Evidence whose artifact digest differs from its subject", () => {
|
|
const bytes = buildManagedPeFixture();
|
|
const binary = managedPeFixtureTarget(bytes, "/fixture/Mismatch.dll");
|
|
const members = inspectManagedMembersBytes(bytes, binary);
|
|
const mismatched = createEvidence(binary, MANAGED_STATIC_PROVIDER, {
|
|
operation: "inspect_managed_members",
|
|
parameters: {},
|
|
result: {
|
|
...members,
|
|
artifact: { ...members.artifact, sha256: "b".repeat(64) },
|
|
identity_scope: {
|
|
...members.identity_scope,
|
|
requires_artifact_sha256: "b".repeat(64),
|
|
},
|
|
},
|
|
});
|
|
const rejected = projectManagedApplicationGraphEvidence({
|
|
managed_members: mismatched,
|
|
});
|
|
expect(rejected.ok).toBe(false);
|
|
if (rejected.ok) throw new Error("mismatched managed Evidence accepted");
|
|
expect(JSON.stringify(rejected.error)).toContain(
|
|
`Managed Evidence inspect_managed_members (${mismatched.evidence_id}) subject SHA-256 ${binary.sha256} does not match normalized artifact SHA-256 ${"b".repeat(64)}`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("managed application graph identity facts", () => {
|
|
it.each([
|
|
["managed_members", "memberEvidence"],
|
|
["managed_native_boundaries", "boundaryEvidence"],
|
|
] as const)(
|
|
"binds a module from %s to its supplying Evidence",
|
|
(inputKey, evidenceKey) => {
|
|
const source = createManagedInteropEvidence()[evidenceKey];
|
|
const projected = projectManagedApplicationGraphEvidence({
|
|
[inputKey]: source,
|
|
});
|
|
if (!projected.ok) throw projected.error;
|
|
const result = managedApplicationGraphResultSchema.parse(
|
|
parseEvidence(projected.value).normalized_result,
|
|
);
|
|
const graph = parseJavaScriptApplicationGraph(result.graph);
|
|
expect(result.summary).toMatchObject({ assemblies: 0, modules: 1 });
|
|
expect(
|
|
graph.nodes.find(({ kind }) => kind === "managed-module")
|
|
?.observations[0],
|
|
).toMatchObject({
|
|
evidence: {
|
|
extractor: { operation: source.operation },
|
|
evidence_ids: [source.evidence_id],
|
|
},
|
|
});
|
|
expect(result.evidence_links).toEqual([source.evidence_id]);
|
|
expect(result.limitations).toContain(
|
|
"Managed artifact Evidence was not supplied; assembly identity observations are absent.",
|
|
);
|
|
},
|
|
);
|
|
|
|
it("reports no assembly or module when the inspector observes no identity rows", () => {
|
|
const bytes = buildManagedPeFixture({
|
|
moduleRowCount: 0,
|
|
assemblyRowCount: 0,
|
|
});
|
|
const binary = managedPeFixtureTarget(bytes, "/fixture/NoIdentityRows.exe");
|
|
const inspection = inspectManagedArtifactBytes(bytes, binary);
|
|
expect(inspection).toMatchObject({ module: null, assembly: null });
|
|
const artifactEvidence = createEvidence(binary, MANAGED_STATIC_PROVIDER, {
|
|
operation: "inspect_managed_artifact",
|
|
parameters: {},
|
|
result: inspection,
|
|
rawResult: null,
|
|
limitations: inspection.limitations,
|
|
locations: [{ kind: "artifact-path", path: binary.path }],
|
|
});
|
|
|
|
const projected = projectManagedApplicationGraphEvidence({
|
|
managed_artifact: artifactEvidence,
|
|
});
|
|
if (!projected.ok) throw projected.error;
|
|
const result = managedApplicationGraphResultSchema.parse(
|
|
parseEvidence(projected.value).normalized_result,
|
|
);
|
|
const graph = parseJavaScriptApplicationGraph(result.graph);
|
|
expect(result.summary).toMatchObject({ assemblies: 0, modules: 0 });
|
|
expect(
|
|
graph.nodes.filter(
|
|
({ kind }) => kind === "managed-assembly" || kind === "managed-module",
|
|
),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("managed application graph coverage", () => {
|
|
it("preserves partial parser coverage in graph and per-fact coverage", () => {
|
|
const bytes = buildManagedPeFixture();
|
|
const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe");
|
|
const members = inspectManagedMembersBytes(bytes, binary);
|
|
const parserPartialMembers = {
|
|
...members,
|
|
coverage: {
|
|
state: "partial" as const,
|
|
issues: [
|
|
{
|
|
code: "invalid-blob" as const,
|
|
scope: "metadata.#Blob",
|
|
offset: 0x0a00,
|
|
detail: "Blob content leaves #Blob",
|
|
},
|
|
],
|
|
},
|
|
};
|
|
const parserPartialEvidence = createEvidence(
|
|
binary,
|
|
MANAGED_STATIC_PROVIDER,
|
|
{
|
|
operation: "inspect_managed_members",
|
|
parameters: {},
|
|
result: parserPartialMembers,
|
|
rawResult: null,
|
|
limitations: parserPartialMembers.limitations,
|
|
},
|
|
);
|
|
const parserPartialProjection = projectManagedApplicationGraphEvidence({
|
|
managed_members: parserPartialEvidence,
|
|
});
|
|
|
|
if (!parserPartialProjection.ok)
|
|
throw new Error("partial projection failed");
|
|
const parserPartialResult = managedApplicationGraphResultSchema.parse(
|
|
parseEvidence(parserPartialProjection.value).normalized_result,
|
|
);
|
|
expect(parserPartialResult.coverage).toMatchObject({
|
|
status: "partial",
|
|
});
|
|
expect(parserPartialResult.graph.coverage).toEqual({
|
|
status: "partial",
|
|
truncated: false,
|
|
omitted_count: null,
|
|
limits: [],
|
|
});
|
|
});
|
|
});
|