272 lines
8.7 KiB
TypeScript
272 lines
8.7 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
planManagedRuntimeCorrelationEvidence,
|
|
type ManagedRuntimePolicy,
|
|
} from "../src/application/ManagedRuntimeCorrelationService.js";
|
|
import { MANAGED_STATIC_PROVIDER } from "../src/application/InvestigationProviders.js";
|
|
import { createPermissionAuthority } from "../src/application/PermissionAuthority.js";
|
|
import { createEvidence } from "../src/domain/evidence.js";
|
|
import { managedRuntimeCorrelationResultSchema } from "../src/domain/managedRuntimeCorrelation.js";
|
|
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
|
|
import type { PermissionCeiling } from "../src/domain/permissionPolicy.js";
|
|
import { inspectManagedMembersBytes } from "../src/dotnet/ManagedMemberInspector.js";
|
|
import { buildManagedPeFixture } from "./fixtures/managedPe.js";
|
|
|
|
const memberLimits = {
|
|
typeOffset: 0,
|
|
typeLimit: 100,
|
|
methodOffset: 0,
|
|
methodLimit: 100,
|
|
fieldOffset: 0,
|
|
fieldLimit: 100,
|
|
memberRefOffset: 0,
|
|
memberRefLimit: 100,
|
|
edgeOffset: 0,
|
|
edgeLimit: 100,
|
|
instructionAnchorLimit: 100,
|
|
maxMetadataBytes: 1024 * 1024,
|
|
maxTableRows: 1_000,
|
|
maxHeapItemBytes: 1024 * 1024,
|
|
maxMethodBodyBytes: 1024 * 1024,
|
|
maxMethodInstructions: 1_000,
|
|
};
|
|
|
|
describe("managed runtime correlation planning", () => {
|
|
it("fails closed while the managed runtime authority is disabled", async () => {
|
|
const fixture = inspect(buildManagedPeFixture(), "/tmp/disabled.dll");
|
|
const result = await planManagedRuntimeCorrelationEvidence(
|
|
{
|
|
policy: disabledPolicy,
|
|
authority: undefined,
|
|
},
|
|
inputFor(fixture.evidence, fixture.method),
|
|
);
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error._tag).toBe("AnalysisCapabilityUnavailableError");
|
|
});
|
|
|
|
it("requires an explicit managed_runtime grant under an enabled ceiling", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "rea-managed-runtime-"));
|
|
const artifactPath = join(directory, "fixture.dll");
|
|
const executablePath = join(directory, "dotnet");
|
|
await writeFile(artifactPath, buildManagedPeFixture());
|
|
await writeFile(executablePath, "#!/bin/sh\n");
|
|
const fixture = inspect(buildManagedPeFixture(), artifactPath);
|
|
const authority = await authorityFor(directory, executablePath, false);
|
|
const result = await planManagedRuntimeCorrelationEvidence(
|
|
{
|
|
policy: { enabled: true, roots: [directory], executablePath },
|
|
authority,
|
|
},
|
|
inputFor(fixture.evidence, fixture.method),
|
|
);
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error._tag).toBe("PermissionRequiredError");
|
|
});
|
|
|
|
it("records an exact-build non-executing admission plan when authorized", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "rea-managed-runtime-"));
|
|
const artifactPath = join(directory, "fixture.dll");
|
|
const executablePath = join(directory, "dotnet");
|
|
await writeFile(artifactPath, buildManagedPeFixture());
|
|
await writeFile(executablePath, "#!/bin/sh\n");
|
|
const fixture = inspect(buildManagedPeFixture(), artifactPath);
|
|
const policy = { enabled: true, roots: [directory], executablePath };
|
|
const authority = await authorityFor(directory, executablePath, true);
|
|
const result = await planManagedRuntimeCorrelationEvidence(
|
|
{ policy, authority },
|
|
inputFor(fixture.evidence, fixture.method),
|
|
);
|
|
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) return;
|
|
expect(result.value.operation).toBe("plan_managed_runtime_correlation");
|
|
expect(result.value.confidence).toBe("derived");
|
|
expect(result.value.authority).toBe("analyst-inference");
|
|
const plan = managedRuntimeCorrelationResultSchema.parse(
|
|
result.value.normalized_result,
|
|
);
|
|
expect(plan).toMatchObject({
|
|
executed: false,
|
|
authority_model: {
|
|
capability: "managed_runtime",
|
|
default_enabled: false,
|
|
per_call_approval_required: true,
|
|
},
|
|
method_lock: {
|
|
token: "0x06000001",
|
|
exact_build_required: true,
|
|
},
|
|
requested_runtime: {
|
|
effect: "instrumentation",
|
|
executable_path: executablePath,
|
|
network: "none",
|
|
},
|
|
effect_taxonomy: {
|
|
instruments_code: true,
|
|
invokes_target_code: false,
|
|
},
|
|
unsupported_until_executor_exists: true,
|
|
});
|
|
});
|
|
|
|
it("rejects a method lock that no longer matches static Evidence", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "rea-managed-runtime-"));
|
|
const artifactPath = join(directory, "fixture.dll");
|
|
const executablePath = join(directory, "dotnet");
|
|
await writeFile(artifactPath, buildManagedPeFixture());
|
|
await writeFile(executablePath, "#!/bin/sh\n");
|
|
const fixture = inspect(buildManagedPeFixture(), artifactPath);
|
|
const authority = await authorityFor(directory, executablePath, true);
|
|
const result = await planManagedRuntimeCorrelationEvidence(
|
|
{
|
|
policy: { enabled: true, roots: [directory], executablePath },
|
|
authority,
|
|
},
|
|
{
|
|
...inputFor(fixture.evidence, fixture.method),
|
|
method: {
|
|
...inputFor(fixture.evidence, fixture.method).method,
|
|
normalized_il_sha256: "0".repeat(64),
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error._tag).toBe("AnalysisInputError");
|
|
});
|
|
|
|
it("rejects a runtime lock for instruction-limited CIL", async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "rea-managed-runtime-"));
|
|
const artifactPath = join(directory, "fixture.dll");
|
|
const executablePath = join(directory, "dotnet");
|
|
const bytes = buildManagedPeFixture();
|
|
await writeFile(artifactPath, bytes);
|
|
await writeFile(executablePath, "#!/bin/sh\n");
|
|
const fixture = inspect(bytes, artifactPath, {
|
|
...memberLimits,
|
|
maxMethodInstructions: 1,
|
|
});
|
|
const authority = await authorityFor(directory, executablePath, true);
|
|
const result = await planManagedRuntimeCorrelationEvidence(
|
|
{
|
|
policy: { enabled: true, roots: [directory], executablePath },
|
|
authority,
|
|
},
|
|
inputFor(fixture.evidence, fixture.method),
|
|
);
|
|
|
|
expect(fixture.method.body).toMatchObject({
|
|
status: "partial",
|
|
normalized_il_sha256: null,
|
|
});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) return;
|
|
expect(result.error._tag).toBe("AnalysisInputError");
|
|
});
|
|
});
|
|
|
|
const disabledPolicy: ManagedRuntimePolicy = {
|
|
enabled: false,
|
|
roots: [],
|
|
executablePath: "/usr/bin/dotnet",
|
|
};
|
|
|
|
const authorityFor = async (
|
|
root: string,
|
|
executablePath: string,
|
|
granted: boolean,
|
|
) => {
|
|
const ceiling: PermissionCeiling = {
|
|
capability: "managed_runtime",
|
|
roots: [root],
|
|
executables: [executablePath],
|
|
environment_names: [],
|
|
network: "none",
|
|
mount: false,
|
|
};
|
|
const authority = await createPermissionAuthority(
|
|
[ceiling],
|
|
granted
|
|
? [
|
|
{
|
|
...ceiling,
|
|
grant_id: "administrator:managed_runtime",
|
|
lifetime: "administrator",
|
|
operation_identity: null,
|
|
expires_at: null,
|
|
},
|
|
]
|
|
: [],
|
|
);
|
|
if (!authority.ok) throw authority.error;
|
|
return authority.value;
|
|
};
|
|
|
|
const inputFor = (
|
|
staticMembers: ReturnType<typeof createEvidence>,
|
|
method: ReturnType<typeof inspect>["method"],
|
|
) => ({
|
|
static_members: staticMembers,
|
|
method: {
|
|
token: method.token,
|
|
signature_sha256: method.signature.raw_sha256,
|
|
normalized_il_sha256: method.body.normalized_il_sha256,
|
|
},
|
|
requested_effect: "instrumentation" as const,
|
|
host: {
|
|
os: "linux" as const,
|
|
clr_family: "dotnet" as const,
|
|
architecture: "x86_64" as const,
|
|
},
|
|
bounds: {
|
|
timeout_ms: 5_000,
|
|
max_threads: 32,
|
|
max_output_bytes: 65_536,
|
|
allow_network: false as const,
|
|
allow_ui: false as const,
|
|
},
|
|
});
|
|
|
|
const inspect = (
|
|
bytes: Buffer,
|
|
path: string,
|
|
limits: typeof memberLimits = memberLimits,
|
|
) => {
|
|
const target: BinaryTarget = {
|
|
path,
|
|
sha256: hash(bytes),
|
|
kind: "executable",
|
|
format: "pe",
|
|
architecture: "x86",
|
|
};
|
|
const result = inspectManagedMembersBytes(bytes, target, limits);
|
|
const method = result.methods.items[0];
|
|
if (method === undefined) throw new Error("fixture has no method");
|
|
return {
|
|
result,
|
|
method,
|
|
evidence: createEvidence(target, MANAGED_STATIC_PROVIDER, {
|
|
operation: "inspect_managed_members",
|
|
parameters: {},
|
|
result,
|
|
rawResult: null,
|
|
limitations: result.limitations,
|
|
}),
|
|
};
|
|
};
|
|
|
|
const hash = (bytes: Buffer): string =>
|
|
createHash("sha256").update(bytes).digest("hex");
|