refactor(test): keep coverage at behavioral boundaries (#474)
* refactor(test): keep coverage at behavioral boundaries * fix(ci): restore package workflow temp-root classification
This commit is contained in:
parent
d65730c4c2
commit
d4dc940bd8
|
|
@ -25,10 +25,10 @@ registries but do not cross production filesystem, process, socket, or browser b
|
|||
Boundary tests cross one production boundary. Only acceptance tests assemble
|
||||
the complete runtime or invoke the compiled product surface.
|
||||
|
||||
Focused immutable builders shared by a domain test family live beside their
|
||||
production owner as `src/domain/*.fixture.ts`. They are typechecked with the
|
||||
suite and excluded from package builds; broader runtime and provider fixtures
|
||||
remain under `tests/fixtures/**`.
|
||||
Focused immutable builders and recording ports shared by one test family live
|
||||
beside their production owner as `src/**/*.fixture.ts`. They are typechecked
|
||||
with the suite and excluded from package builds; broader runtime and provider
|
||||
fixtures remain under `tests/fixtures/**`.
|
||||
|
||||
`tests/process-global/**` is reserved for cases with a demonstrated dependency
|
||||
on process-global state. Those tests run without file parallelism. Reusable,
|
||||
|
|
@ -70,8 +70,10 @@ Local full-suite Vitest runs are intentionally capped at one worker and one
|
|||
project at a time. Boundary fixtures own real subprocesses, and this local cap
|
||||
keeps aggregate memory predictable; CI retains the existing two-worker budget.
|
||||
The pure domain/contracts and recording-port service projects share one worker
|
||||
module context because their tests own no mutable runtime resources; adapter,
|
||||
composition, and boundary projects retain per-file isolation.
|
||||
module context because their tests own no mutable runtime resources. MCP
|
||||
boundary files also share the immutable server module graph while creating and
|
||||
closing independent in-memory sessions. Adapter, composition, acceptance,
|
||||
process-global, and other boundary projects retain per-file isolation.
|
||||
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
|
||||
repository-local locks and fail fast when the same class of command is already
|
||||
running. The `npm test` build is inside that lock. `check:pr` runs its test task
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": ["src/main.ts", "src/cli.ts"],
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
AnalysisProviderRegistry,
|
||||
type AnalysisProviderBinding,
|
||||
} from "../../../src/application/AnalysisProviderRegistry.js";
|
||||
} from "./AnalysisProviderRegistry.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisClient,
|
||||
|
|
@ -12,15 +12,15 @@ import {
|
|||
type ProviderAvailability,
|
||||
type ProviderIdentity,
|
||||
type ProviderTargetSupport,
|
||||
} from "../../../src/application/AnalysisProvider.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../../src/domain/binaryTarget.js";
|
||||
} from "./AnalysisProvider.js";
|
||||
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import {
|
||||
ProviderAdapterError,
|
||||
ProviderSelectionError,
|
||||
projectAnalysisError,
|
||||
} from "../../../src/domain/errors.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
} from "../domain/errors.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
|
||||
const DATABASE_TARGET: BinaryTarget = {
|
||||
path: "/tmp/fixture.hop",
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import type { DoctorHost } from "./Doctor.js";
|
||||
|
||||
/** Healthy recording host for focused doctor and projection tests. */
|
||||
export const createDoctorHostFixture = (
|
||||
overrides: Partial<DoctorHost> = {},
|
||||
): DoctorHost => ({
|
||||
platform: "darwin",
|
||||
architecture: "x64",
|
||||
nodeVersion: "24.18.0",
|
||||
macosVersion: () => Promise.resolve("14.0"),
|
||||
linuxDistribution: () => Promise.resolve(undefined),
|
||||
validTarget: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
executable: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
supportedLinuxHopper: () => Promise.resolve(true),
|
||||
linuxDemoRuntimeCheck: () =>
|
||||
Promise.resolve({
|
||||
name: "hopper-demo-runtime",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
}),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
...overrides,
|
||||
});
|
||||
|
|
@ -1,34 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runDoctor, type DoctorHost } from "../../../src/application/Doctor.js";
|
||||
import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
|
||||
|
||||
const host = (overrides: Partial<DoctorHost> = {}): DoctorHost => ({
|
||||
platform: "darwin",
|
||||
architecture: "x64",
|
||||
nodeVersion: "24.18.0",
|
||||
macosVersion: () => Promise.resolve("12.0"),
|
||||
linuxDistribution: () => Promise.resolve(undefined),
|
||||
validTarget: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
executable: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
supportedLinuxHopper: () => Promise.resolve(true),
|
||||
linuxDemoRuntimeCheck: () =>
|
||||
Promise.resolve({
|
||||
name: "hopper-demo-runtime",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
}),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
...overrides,
|
||||
});
|
||||
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
|
||||
import { runDoctor } from "./Doctor.js";
|
||||
|
||||
describe("doctor runtime executable diagnostics", () => {
|
||||
it("reports a broken shadowed Node candidate without hiding the healthy launcher", async () => {
|
||||
|
|
@ -1,34 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { ClientRegistrationStatus } from "../../../src/application/ClientRegistrationStatus.js";
|
||||
import { runDoctor, type DoctorHost } from "../../../src/application/Doctor.js";
|
||||
import { CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../../../src/identity.js";
|
||||
|
||||
const host = (overrides: Partial<DoctorHost> = {}): DoctorHost => ({
|
||||
platform: "darwin",
|
||||
architecture: "x64",
|
||||
nodeVersion: "24.18.0",
|
||||
macosVersion: () => Promise.resolve("12.0"),
|
||||
linuxDistribution: () => Promise.resolve(undefined),
|
||||
validTarget: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
executable: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
supportedLinuxHopper: () => Promise.resolve(true),
|
||||
linuxDemoRuntimeCheck: () =>
|
||||
Promise.resolve({
|
||||
name: "hopper-demo-runtime",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
}),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
...overrides,
|
||||
});
|
||||
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
|
||||
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
|
||||
import { runDoctor } from "./Doctor.js";
|
||||
|
||||
describe("doctor", () => {
|
||||
it("returns exact recovery for every failed diagnostic", async () => {
|
||||
|
|
@ -12,7 +12,7 @@ import {
|
|||
type LinuxHopperInstallHost,
|
||||
type LinuxHopperLauncherStatus,
|
||||
type LinuxPackageFamily,
|
||||
} from "../../../../src/application/LinuxHopper.js";
|
||||
} from "./LinuxHopper.js";
|
||||
|
||||
class RecordingLinuxHost implements LinuxHopperInstallHost {
|
||||
distributionValue: LinuxDistribution | undefined = {
|
||||
|
|
@ -6,7 +6,7 @@ import {
|
|||
installMacHopper,
|
||||
macHopperInstallDisclosure,
|
||||
type MacHopperInstallHost,
|
||||
} from "../../../../src/application/MacHopper.js";
|
||||
} from "./MacHopper.js";
|
||||
|
||||
class FakeMacHopperHost implements MacHopperInstallHost {
|
||||
readonly archive = new TextEncoder().encode("hopper-dmg");
|
||||
|
|
@ -2,14 +2,14 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { importManagedReconstructionEvidence } from "../../../../src/application/ManagedReconstructionService.js";
|
||||
import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../../../src/contracts/managedWorkflowExamples.js";
|
||||
import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../contracts/managedWorkflowExamples.js";
|
||||
import {
|
||||
importManagedReconstruction,
|
||||
managedReconstructionImportInputSchema,
|
||||
} from "../../../../src/domain/managedReconstruction.js";
|
||||
import { managedMemberInspectionSchema } from "../../../../src/domain/managedArtifact.js";
|
||||
import { createEvidence } from "../../../../src/domain/evidence.js";
|
||||
} from "../domain/managedReconstruction.js";
|
||||
import { managedMemberInspectionSchema } from "../domain/managedArtifact.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { importManagedReconstructionEvidence } from "./ManagedReconstructionService.js";
|
||||
|
||||
const exampleInput = () =>
|
||||
managedReconstructionImportInputSchema.parse(
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { projectNativeApiInspection } from "../../../../src/application/NativeApiInspection.js";
|
||||
import { functionDossierSchema } from "../../../../src/domain/hopperValues.js";
|
||||
import { nativeApiBoundarySchema } from "../../../../src/domain/nativeApiBoundary.js";
|
||||
import { ghidraFunctionDossier } from "../../../fixtures/ghidraFunction.js";
|
||||
import { functionDossierSchema } from "../domain/hopperValues.js";
|
||||
import { ghidraFunctionDossier } from "../domain/hopperValues.fixture.js";
|
||||
import { nativeApiBoundarySchema } from "../domain/nativeApiBoundary.js";
|
||||
import { projectNativeApiInspection } from "./NativeApiInspection.js";
|
||||
|
||||
describe("native API inspection", () => {
|
||||
it("projects structured boundary evidence through inspectable substeps", () => {
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { projectAnalysisError } from "../domain/errors.js";
|
||||
import {
|
||||
ProcessCaptureError,
|
||||
processCaptureCancelled,
|
||||
} from "./ProcessCaptureError.js";
|
||||
|
||||
describe("process capture error projection", () => {
|
||||
it("reports incomplete cleanup resources without exposing its cause", () => {
|
||||
const projected = projectAnalysisError(
|
||||
new ProcessCaptureError("terminal cleanup failed", {
|
||||
cause: new Error("secret-token"),
|
||||
reason: "cleanup_incomplete",
|
||||
cleanupResources: ["process_group"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(projected).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
details: { cleanup: "incomplete", resources: ["process_group"] },
|
||||
});
|
||||
expect(JSON.stringify(projected)).not.toContain("secret-token");
|
||||
});
|
||||
|
||||
it("projects caller cancellation as completed cleanup", () => {
|
||||
expect(projectAnalysisError(processCaptureCancelled())).toMatchObject({
|
||||
code: "cancelled",
|
||||
category: "cancelled",
|
||||
details: { operation: "process_capture", cleanup: "complete" },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import type { ProcessCaptureEventJournalEntry } from "../domain/processCapture.js";
|
||||
import { createProcessCaptureJournal } from "./ProcessCaptureJournal.js";
|
||||
|
||||
it("publishes journal entries in capture order and supports nested recording", () => {
|
||||
const journal = createProcessCaptureJournal();
|
||||
const observed: ProcessCaptureEventJournalEntry[] = [];
|
||||
const unsubscribe = journal.subscribe((entry) => observed.push(entry));
|
||||
journal.recordEvent("lifecycle", 0);
|
||||
const settlementEntry = journal.record("lifecycle", 1);
|
||||
unsubscribe();
|
||||
journal.recordEvent("frames", 0);
|
||||
|
||||
expect(journal.entries).toEqual([
|
||||
{ capture_order: 0, collection: "lifecycle", index: 0 },
|
||||
{ capture_order: 1, collection: "lifecycle", index: 1 },
|
||||
{ capture_order: 2, collection: "frames", index: 0 },
|
||||
]);
|
||||
expect(observed).toEqual(journal.entries.slice(0, 2));
|
||||
expect(settlementEntry).toEqual(journal.entries[1]);
|
||||
|
||||
const nestedJournal = createProcessCaptureJournal();
|
||||
const notifications: string[] = [];
|
||||
nestedJournal.subscribe((entry) => {
|
||||
notifications.push(`a:${String(entry.capture_order)}`);
|
||||
if (entry.capture_order === 0) nestedJournal.record("frames", 1);
|
||||
});
|
||||
nestedJournal.subscribe((entry) =>
|
||||
notifications.push(`b:${String(entry.capture_order)}`),
|
||||
);
|
||||
nestedJournal.record("frames", 0);
|
||||
|
||||
expect(notifications).toEqual(["a:0", "b:0", "a:1", "b:1"]);
|
||||
});
|
||||
|
|
@ -1,26 +1,23 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { snapshotRoots } from "../../../src/application/FilesystemSnapshot.js";
|
||||
import { snapshotRoots } from "./FilesystemSnapshot.js";
|
||||
import {
|
||||
buildCaptureResult,
|
||||
prepareProcessCapture,
|
||||
type ProcessPreparationHost,
|
||||
} from "../../../src/application/ProcessCaptureLifecycle.js";
|
||||
import { ProcessCheckpoints } from "../../../src/application/ProcessCheckpoints.js";
|
||||
import { normalizeProcessSamples } from "../../../src/application/ProcessNormalization.js";
|
||||
import {
|
||||
isInitializedPtyRoot,
|
||||
readLinuxChildren,
|
||||
} from "../../../src/application/ProcessSampling.js";
|
||||
import { TerminalRenderer } from "../../../src/application/TerminalRenderer.js";
|
||||
} from "./ProcessCaptureLifecycle.js";
|
||||
import { ProcessCheckpoints } from "./ProcessCheckpoints.js";
|
||||
import { normalizeProcessSamples } from "./ProcessNormalization.js";
|
||||
import { isInitializedPtyRoot, readLinuxChildren } from "./ProcessSampling.js";
|
||||
import { TerminalRenderer } from "./TerminalRenderer.js";
|
||||
import {
|
||||
authorizeProcessScenario,
|
||||
compareProcessCaptures,
|
||||
parseProcessCapture,
|
||||
parseProcessScenario,
|
||||
type ProcessCapture,
|
||||
} from "../../../src/domain/processCapture.js";
|
||||
import { emptyProcessCapture as emptyCapture } from "../../fixtures/processCapture.js";
|
||||
} from "../domain/processCapture.js";
|
||||
import { emptyProcessCapture as emptyCapture } from "../domain/processCapture.fixture.js";
|
||||
|
||||
const base = {
|
||||
approved: true as const,
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
runPairedProcessExperiment,
|
||||
type ProcessCapturePort,
|
||||
} from "./ProcessPairedExperiment.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js";
|
||||
import {
|
||||
parseProcessCapture,
|
||||
type EnabledProcessExecutionPolicy,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "./ReconstructionObligationLedgerService.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "./ReconstructionObligationLedgerService.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCapture.fixture.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runSetup } from "../../../src/application/Setup.js";
|
||||
import { FakeSetupHost, options } from "./setupTestSupport.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
import { runSetup } from "./Setup.js";
|
||||
|
||||
describe("setup workflow", () => {
|
||||
it("omits an aligned managed skill from an otherwise empty plan", async () => {
|
||||
|
|
@ -6,14 +6,11 @@ import {
|
|||
type SetupOptions,
|
||||
type SetupHopperInstallResult,
|
||||
type SetupProviderEnvironment,
|
||||
} from "../../../src/application/Setup.js";
|
||||
import type {
|
||||
DoctorCheck,
|
||||
DoctorReport,
|
||||
DoctorScope,
|
||||
} from "../../../src/application/Doctor.js";
|
||||
import type { LinuxDistribution } from "../../../src/application/LinuxHopper.js";
|
||||
} from "./Setup.js";
|
||||
import type { DoctorCheck, DoctorReport, DoctorScope } from "./Doctor.js";
|
||||
import type { LinuxDistribution } from "./LinuxHopper.js";
|
||||
|
||||
/** Recording setup host for service-level planning and recovery tests. */
|
||||
export class FakeSetupHost implements SetupHost {
|
||||
readonly platform: NodeJS.Platform;
|
||||
nodeVersion = "25.1.0";
|
||||
|
|
@ -160,6 +157,7 @@ export class FakeSetupHost implements SetupHost {
|
|||
};
|
||||
}
|
||||
|
||||
/** Build setup options for approved or planning-only service tests. */
|
||||
export const options = (
|
||||
approved: boolean,
|
||||
installHopper = false,
|
||||
|
|
@ -4,8 +4,8 @@ import {
|
|||
registrationPermissionEnvironment,
|
||||
runSetup,
|
||||
type SetupProgressEvent,
|
||||
} from "../../../src/application/Setup.js";
|
||||
import { FakeSetupHost, options } from "./setupTestSupport.js";
|
||||
} from "./Setup.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
|
||||
describe("setup workflow", () => {
|
||||
it("propagates only the explicit non-secret investigation root policy", () => {
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runSetup } from "../../../src/application/Setup.js";
|
||||
import { FakeSetupHost, options } from "./setupTestSupport.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
import { runSetup } from "./Setup.js";
|
||||
|
||||
describe("setup workflow", () => {
|
||||
it("records every detected client outcome after an earlier failure", async () => {
|
||||
|
|
@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
setupInstallFailure,
|
||||
type HopperInstallFailureReason,
|
||||
} from "../../../src/application/SetupInstallFailure.js";
|
||||
} from "./SetupInstallFailure.js";
|
||||
|
||||
describe("setup installer failure projection", () => {
|
||||
it.each([
|
||||
|
|
@ -1,35 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runCapabilityStatus, runProviderStatus } from "./DirectAnalysis.js";
|
||||
import { createDoctorHostFixture } from "./Doctor.fixture.js";
|
||||
import { projectDoctorReport } from "./DoctorProjection.js";
|
||||
import { runDoctor, type DoctorHost } from "./Doctor.js";
|
||||
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
|
||||
const doctorHost = (): DoctorHost => ({
|
||||
platform: "darwin",
|
||||
architecture: "x64",
|
||||
nodeVersion: "24.18.0",
|
||||
macosVersion: () => Promise.resolve("14.0"),
|
||||
linuxDistribution: () => Promise.resolve(undefined),
|
||||
validTarget: () => Promise.resolve(true),
|
||||
executable: (path) => Promise.resolve(path.includes("Hopper")),
|
||||
supportedLinuxHopper: () => Promise.resolve(true),
|
||||
linuxDemoRuntimeCheck: () =>
|
||||
Promise.resolve({
|
||||
name: "hopper-demo-runtime",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
}),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
});
|
||||
import { runDoctor } from "./Doctor.js";
|
||||
|
||||
describe("purpose-specific CLI projections", () => {
|
||||
it("keeps provider and capability summaries concise with explicit full detail", async () => {
|
||||
|
|
@ -59,7 +33,10 @@ describe("purpose-specific CLI projections", () => {
|
|||
});
|
||||
|
||||
it("omits the catalog tool array only from the default doctor projection", async () => {
|
||||
const report = await runDoctor(undefined, doctorHost());
|
||||
const report = await runDoctor(
|
||||
undefined,
|
||||
createDoctorHostFixture({ validTarget: () => Promise.resolve(true) }),
|
||||
);
|
||||
const summary = projectDoctorReport(report, "summary");
|
||||
const full = projectDoctorReport(report, "full");
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import {
|
|||
type UpgradeHost,
|
||||
type UpgradeOutput,
|
||||
type NpmInstallationHost,
|
||||
} from "../../../src/application/Upgrade.js";
|
||||
} from "./Upgrade.js";
|
||||
|
||||
class FakeUpgradeHost implements UpgradeHost {
|
||||
latest: string | undefined = "0.6.0";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { CdpCaptureCompleteness } from "../../../src/browser/CdpCaptureCompleteness.js";
|
||||
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
|
||||
describe("CdpCaptureCompleteness", () => {
|
||||
it("reports attach-window limitations without claiming a complete history", () => {
|
||||
|
|
@ -5,8 +5,8 @@ import {
|
|||
captureDom,
|
||||
captureFrames,
|
||||
captureResources,
|
||||
} from "../../../src/browser/CdpCaptureDocuments.js";
|
||||
import { inspectWebPageInputSchema } from "../../../src/domain/browserObservation.js";
|
||||
} from "./CdpCaptureDocuments.js";
|
||||
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
|
||||
|
||||
const origin = "https://app.example.test";
|
||||
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BrowserScenarioSessionPort } from "../../../src/browser/BrowserScenarioSessionPort.js";
|
||||
import { PlaywrightBrowserScenarioProvider } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js";
|
||||
import { BrowserScenarioCaptureBudget } from "../../../src/browser/PlaywrightScenarioArtifacts.js";
|
||||
import { sanitizeBrowserUrl } from "../../../src/domain/browserObservation.js";
|
||||
import type { BrowserScenarioSessionPort } from "./BrowserScenarioSessionPort.js";
|
||||
import { PlaywrightBrowserScenarioProvider } from "./PlaywrightBrowserScenarioProvider.js";
|
||||
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
|
||||
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
||||
import {
|
||||
browserScenarioSchema,
|
||||
type BrowserScenario,
|
||||
type BrowserScenarioAction,
|
||||
} from "../../../src/domain/browserScenario.js";
|
||||
} from "../domain/browserScenario.js";
|
||||
import {
|
||||
browserStepArtifactsSchema,
|
||||
type BrowserScenarioEvent,
|
||||
type BrowserStepArtifacts,
|
||||
} from "../../../src/domain/browserScenarioCapture.js";
|
||||
} from "../domain/browserScenarioCapture.js";
|
||||
|
||||
const scenario = (
|
||||
options: {
|
||||
|
|
@ -3,9 +3,9 @@ import { expect, it } from "vitest";
|
|||
import {
|
||||
electronActiveObservationInputSchema,
|
||||
electronActiveObservationResultSchema,
|
||||
} from "../../../src/domain/electronActiveObservation.js";
|
||||
import { runElectronActions } from "../../../src/browser/PlaywrightElectronActiveActions.js";
|
||||
import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js";
|
||||
} from "../domain/electronActiveObservation.js";
|
||||
import { createElectronActiveObservationFixtureResult } from "../domain/electronActiveObservation.fixture.js";
|
||||
import { runElectronActions } from "./PlaywrightElectronActiveActions.js";
|
||||
|
||||
it("parses bounded window, renderer, and deep-link actions for agents", () => {
|
||||
const input = electronActiveObservationInputSchema.parse({
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { withPlaywrightExecutionBoundary } from "../../../src/browser/PlaywrightExecutionBoundary.js";
|
||||
import { withPlaywrightExecutionBoundary } from "./PlaywrightExecutionBoundary.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../../../src/domain/errors.js";
|
||||
} from "../domain/errors.js";
|
||||
|
||||
describe("withPlaywrightExecutionBoundary", () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { Page } from "playwright-core";
|
||||
|
||||
import { BrowserScenarioSecrets } from "../../../src/browser/BrowserScenarioSecrets.js";
|
||||
import { BrowserScenarioCaptureBudget } from "../../../src/browser/PlaywrightScenarioArtifacts.js";
|
||||
import { PlaywrightScenarioEvents } from "../../../src/browser/PlaywrightScenarioEvents.js";
|
||||
import { browserScenarioSchema } from "../../../src/domain/browserScenario.js";
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
import { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
|
||||
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
|
||||
import { PlaywrightScenarioEvents } from "./PlaywrightScenarioEvents.js";
|
||||
|
||||
describe("PlaywrightScenarioEvents", () => {
|
||||
it("bounds oversized page errors before validating captured events", () => {
|
||||
|
|
@ -1,18 +1,8 @@
|
|||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
isCliOperationFailure,
|
||||
logCliCommand,
|
||||
} from "../../../src/cliLogging.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { isCliOperationFailure } from "./cliLogging.js";
|
||||
|
||||
const originalExitCode = process.exitCode;
|
||||
|
||||
afterEach(() => {
|
||||
process.exitCode = originalExitCode;
|
||||
});
|
||||
|
||||
describe("CLI operation status", () => {
|
||||
describe("CLI operation status classification", () => {
|
||||
it.each([
|
||||
["typed error", { error: "Analysis failed", category: "timeout" }],
|
||||
["unhealthy diagnostic", { healthy: false, checks: [] }],
|
||||
|
|
@ -34,20 +24,4 @@ describe("CLI operation status", () => {
|
|||
])("keeps %s successful", (_label, value) => {
|
||||
expect(isCliOperationFailure(value)).toBe(false);
|
||||
});
|
||||
|
||||
it("sets a nonzero process status without replacing structured output", async () => {
|
||||
const output = {
|
||||
error: "Analysis failed",
|
||||
category: "integrity_mismatch",
|
||||
message: "Artifact integrity check failed.",
|
||||
details: { logical_path: "main.js" },
|
||||
};
|
||||
|
||||
await expect(
|
||||
logCliCommand(silentLogger, "inventory-artifact", () =>
|
||||
Promise.resolve(output),
|
||||
),
|
||||
).resolves.toBe(output);
|
||||
expect(process.exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseConfig } from "../../../src/config.js";
|
||||
import { parseConfig } from "./config.js";
|
||||
|
||||
describe("configuration parsing integration", () => {
|
||||
it("returns defaults when no environment variables are set", () => {
|
||||
|
|
@ -1,72 +1,2 @@
|
|||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { digestProcessCommitment } from "../domain/processCapture.js";
|
||||
|
||||
const normalization = {
|
||||
paths: true,
|
||||
pids: true,
|
||||
ports: true,
|
||||
time_bucket_ms: 10,
|
||||
patterns: [],
|
||||
};
|
||||
const scenario = { executable_sha256: "0".repeat(64) };
|
||||
const comparisonContract = {};
|
||||
const shimPlan: JsonValue[] = [];
|
||||
const replayPlan = {};
|
||||
|
||||
/** Minimal valid process capture used only in public contract examples. */
|
||||
export const EMPTY_PROCESS_CAPTURE_EXAMPLE = {
|
||||
schema_version: 4,
|
||||
manifest: {
|
||||
rea_version: "1.1.0",
|
||||
provider_version: "3",
|
||||
platform: "fixture",
|
||||
architecture: "fixture",
|
||||
pty_backend: "node-pty",
|
||||
started_at: "2026-01-01T00:00:00.000Z",
|
||||
completed_at: "2026-01-01T00:00:00.001Z",
|
||||
scenario,
|
||||
comparison_contract: comparisonContract,
|
||||
shim_plan: shimPlan,
|
||||
replay_plan: replayPlan,
|
||||
full_scenario_sha256: digestProcessCommitment(scenario),
|
||||
comparison_contract_sha256: digestProcessCommitment(comparisonContract),
|
||||
executable_sha256: "0".repeat(64),
|
||||
normalization_sha256: digestProcessCommitment(normalization),
|
||||
shim_plan_sha256: digestProcessCommitment(shimPlan),
|
||||
replay_plan_sha256: digestProcessCommitment(replayPlan),
|
||||
},
|
||||
normalization,
|
||||
frames: [],
|
||||
rendered_frames: [],
|
||||
interaction_events: [],
|
||||
exit: { code: 0, signal: null, reason: "exited" },
|
||||
settlement: {
|
||||
state: "quiesced",
|
||||
elapsed_ms: 50,
|
||||
cleanup_outcome: "not_required",
|
||||
},
|
||||
process_samples: [],
|
||||
filesystem_checkpoints: [
|
||||
{ name: "before", at_ms: 0, files: [], effects: [], truncated: false },
|
||||
{
|
||||
name: "after_settlement",
|
||||
at_ms: 50,
|
||||
files: [],
|
||||
effects: [],
|
||||
truncated: false,
|
||||
},
|
||||
],
|
||||
shim_events: [],
|
||||
protocol_events: [],
|
||||
replay_transitions: [],
|
||||
files_before: [],
|
||||
files_after: [],
|
||||
filesystem_effects: [],
|
||||
truncated: false,
|
||||
limitations: [],
|
||||
residual_unknowns: [],
|
||||
cleanup: {
|
||||
owned_process_group: "verified",
|
||||
temporary_root: "removed",
|
||||
},
|
||||
} satisfies JsonValue;
|
||||
/** Minimal valid process capture retained as a public contract example. */
|
||||
export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/processCaptureExample.js";
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
ArtifactOperationError,
|
||||
BinaryTargetError,
|
||||
HopperProcessError,
|
||||
PermissionRequiredError,
|
||||
ProviderAdapterError,
|
||||
ReplayPlanStaleError,
|
||||
UnknownRegistryError,
|
||||
projectAnalysisError,
|
||||
} from "./errors.js";
|
||||
|
||||
describe("analysis error projection: provider failures", () => {
|
||||
it("preserves detached, actionable provider diagnostics", () => {
|
||||
const diagnostics = {
|
||||
runtime_root: "/tmp/rea-ghidra-fixture",
|
||||
profile_digest: "a".repeat(64),
|
||||
exit_code: 1,
|
||||
};
|
||||
const error = new ProviderAdapterError("ghidra", "health", {
|
||||
diagnostics,
|
||||
});
|
||||
|
||||
expect(error.diagnostics).not.toBe(diagnostics);
|
||||
expect(projectAnalysisError(error)).toMatchObject({
|
||||
details: {
|
||||
provider_id: "ghidra",
|
||||
operation: "health",
|
||||
diagnostics,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("maps the Linux startup compatibility exit codes without host internals", () => {
|
||||
const expected = [
|
||||
[70, "private_display_unavailable"],
|
||||
[71, "x11_authorization_failed"],
|
||||
[72, "unsupported_hopper_build"],
|
||||
[73, "invalid_launch_command"],
|
||||
[74, "process_ownership_mismatch"],
|
||||
[75, "hopper_exited_during_startup"],
|
||||
[76, "unsupported_demo_dialog"],
|
||||
[77, "unexpected_display_geometry"],
|
||||
[78, "x11_input_failed"],
|
||||
[79, "runtime_dependency_unavailable"],
|
||||
[80, "x11_socket_directory_unusable"],
|
||||
] as const;
|
||||
|
||||
for (const [exitCode, code] of expected) {
|
||||
const projected = projectAnalysisError(new HopperProcessError(exitCode));
|
||||
expect(projected).toMatchObject({
|
||||
code: "provider_unavailable",
|
||||
details: { failure_code: code, exit_code: exitCode },
|
||||
});
|
||||
expect(projected.message.length).toBeGreaterThan(20);
|
||||
expect(JSON.stringify(projected)).not.toContain("/proc/");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("analysis error projection: caller contract", () => {
|
||||
it("maps representative failures without exposing causes", () => {
|
||||
const secretCause = new Error("secret-token");
|
||||
const projected = [
|
||||
projectAnalysisError(
|
||||
new AnalysisInputError("overview", { cause: secretCause }),
|
||||
),
|
||||
projectAnalysisError(
|
||||
new AnalysisCapabilityUnavailableError("fixture", "overview", "absent"),
|
||||
),
|
||||
projectAnalysisError(
|
||||
new BinaryTargetError("/local/targets/app", "invalid", {
|
||||
cause: secretCause,
|
||||
}),
|
||||
),
|
||||
projectAnalysisError(
|
||||
new ReplayPlanStaleError("a".repeat(64), "b".repeat(64)),
|
||||
),
|
||||
];
|
||||
|
||||
expect(projected.map(({ code }) => code)).toEqual([
|
||||
"invalid_request",
|
||||
"capability_unavailable",
|
||||
"target_unavailable",
|
||||
"plan_stale",
|
||||
]);
|
||||
expect(projected[1]).toMatchObject({ category: "unsupported_provider" });
|
||||
expect(projected[2]).toMatchObject({
|
||||
details: { path: "/local/targets/app" },
|
||||
});
|
||||
expect(JSON.stringify(projected)).not.toContain("secret-token");
|
||||
});
|
||||
|
||||
it("preserves exact artifact-integrity coordinates", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
new ArtifactOperationError("inventory_artifact", "integrity", {
|
||||
logicalPath: "main.js",
|
||||
declaredSha256: "a".repeat(64),
|
||||
calculatedSha256: "b".repeat(64),
|
||||
unpacked: true,
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
category: "integrity_mismatch",
|
||||
details: {
|
||||
logical_path: "main.js",
|
||||
declared_sha256: "a".repeat(64),
|
||||
calculated_sha256: "b".repeat(64),
|
||||
unpacked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"configure",
|
||||
false,
|
||||
false,
|
||||
"Add the exact missing scope beneath the administrator ceiling, then retry.",
|
||||
],
|
||||
[
|
||||
"elicit",
|
||||
false,
|
||||
true,
|
||||
"Approve the exact missing scope, then retry the operation.",
|
||||
],
|
||||
[
|
||||
"restart",
|
||||
true,
|
||||
false,
|
||||
"Add the exact missing scope to the administrator configuration, then restart the registered MCP server or client.",
|
||||
],
|
||||
] as const)(
|
||||
"projects %s permission remediation into stable protocol fields",
|
||||
(remediation, restartRequired, elicitationSupported, action) => {
|
||||
const projected = projectAnalysisError(
|
||||
new PermissionRequiredError({
|
||||
requested: {
|
||||
capability: "evidence_read",
|
||||
roots: ["/workspace/evidence.json"],
|
||||
executables: [],
|
||||
environment_names: [],
|
||||
network: "none",
|
||||
mount: false,
|
||||
operation_identity: "read:evidence",
|
||||
},
|
||||
missing: { roots: ["/workspace/evidence.json"] },
|
||||
ceiling: null,
|
||||
remediation,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(projected.remediation).toEqual({
|
||||
action,
|
||||
restart_required: restartRequired,
|
||||
elicitation_supported: elicitationSupported,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("gives missing residual unknowns lookup-specific remediation", () => {
|
||||
expect(
|
||||
projectAnalysisError(new UnknownRegistryError("not-found")),
|
||||
).toMatchObject({
|
||||
code: "execution_failure",
|
||||
message:
|
||||
"The requested residual unknown does not exist in this session. Check the unknown_id and try again.",
|
||||
remediation: {
|
||||
action:
|
||||
"Check that the unknown_id belongs to this session, then retry.",
|
||||
},
|
||||
details: { reason: "not-found" },
|
||||
});
|
||||
});
|
||||
|
||||
it("retains schema-authored input guidance", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
new AnalysisInputError("find_changed_behavior", undefined, [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run",
|
||||
},
|
||||
]),
|
||||
),
|
||||
).toMatchObject({
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: [],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"Supply either existing comparisons or one investigation_run",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes disabled integrity continuation from format support", () => {
|
||||
expect(
|
||||
projectAnalysisError(
|
||||
new ArtifactOperationError("inventory_artifact", "policy"),
|
||||
),
|
||||
).toMatchObject({
|
||||
code: "artifact_operation_failed",
|
||||
category: "unavailable",
|
||||
message: expect.stringContaining(
|
||||
"REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true",
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,8 +7,7 @@ import {
|
|||
detectArtifactFormat,
|
||||
detectBytecodeFamily,
|
||||
type BytecodeArtifact,
|
||||
type BytecodeSymbol,
|
||||
} from "../../../../src/domain/bytecodeProvider.js";
|
||||
} from "./bytecodeProvider.js";
|
||||
|
||||
describe("bytecode provider", () => {
|
||||
it("detects JVM family from extensions", () => {
|
||||
|
|
@ -137,16 +136,4 @@ describe("bytecode provider", () => {
|
|||
const parsed = bytecodeAnalysisSchema.safeParse(result);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it("uses BytecodeSymbol type", () => {
|
||||
const sym: BytecodeSymbol = {
|
||||
name: "com.example.Test",
|
||||
raw_location: "0x100",
|
||||
kind: "class",
|
||||
bytecode_version: 52,
|
||||
provenance: "application",
|
||||
is_public: true,
|
||||
};
|
||||
expect(sym.provenance).toBe("application");
|
||||
});
|
||||
});
|
||||
|
|
@ -37,7 +37,6 @@ export const bytecodeSymbolSchema = z.strictObject({
|
|||
/** Whether the symbol is accessible (public). */
|
||||
is_public: z.boolean().default(false),
|
||||
});
|
||||
export type BytecodeSymbol = z.infer<typeof bytecodeSymbolSchema>;
|
||||
|
||||
/** A discovered bytecode artifact (class file, wheel, etc). */
|
||||
export const bytecodeArtifactSchema = z.strictObject({
|
||||
|
|
|
|||
|
|
@ -10,12 +10,8 @@ import {
|
|||
looksLikeCredentialFrame,
|
||||
successfulAuthEvents,
|
||||
type AuthFlowEvent,
|
||||
type FrameDirection,
|
||||
type AuthStage,
|
||||
type TokenLifecycleEvent,
|
||||
type CustomProtocolCapture,
|
||||
type ProtocolFrame,
|
||||
} from "../../../src/domain/customProtocolCapture.js";
|
||||
} from "./customProtocolCapture.js";
|
||||
|
||||
const sampleFrames: ProtocolFrame[] = [
|
||||
{
|
||||
|
|
@ -137,21 +133,4 @@ describe("custom protocol capture", () => {
|
|||
it("returns null for empty auth events", () => {
|
||||
expect(authFlowDuration([])).toBeNull();
|
||||
});
|
||||
|
||||
it("uses all exported types", () => {
|
||||
const dir: FrameDirection = "sent";
|
||||
expect(dir).toBe("sent");
|
||||
const stage: AuthStage = "init";
|
||||
expect(stage).toBe("init");
|
||||
const event: TokenLifecycleEvent = "issued";
|
||||
expect(event).toBe("issued");
|
||||
const capture: CustomProtocolCapture = {
|
||||
transport: "tcp",
|
||||
frames: sampleFrames,
|
||||
auth_events: sampleAuthEvents,
|
||||
has_truncated: false,
|
||||
credentials_detected: false,
|
||||
};
|
||||
expect(capture.transport).toBe("tcp");
|
||||
});
|
||||
});
|
||||
|
|
@ -15,7 +15,6 @@ export type TransportType = z.infer<typeof transportTypeSchema>;
|
|||
|
||||
/** Direction of a protocol frame. */
|
||||
export const frameDirectionSchema = z.enum(["sent", "received", "intercepted"]);
|
||||
export type FrameDirection = z.infer<typeof frameDirectionSchema>;
|
||||
|
||||
/** A captured protocol frame. */
|
||||
export const protocolFrameSchema = z.strictObject({
|
||||
|
|
@ -57,7 +56,6 @@ export const authStageSchema = z.enum([
|
|||
"failure",
|
||||
"success",
|
||||
]);
|
||||
export type AuthStage = z.infer<typeof authStageSchema>;
|
||||
|
||||
/** Token lifecycle event. */
|
||||
export const tokenLifecycleEventSchema = z.enum([
|
||||
|
|
@ -67,7 +65,6 @@ export const tokenLifecycleEventSchema = z.enum([
|
|||
"revoked",
|
||||
"renewed",
|
||||
]);
|
||||
export type TokenLifecycleEvent = z.infer<typeof tokenLifecycleEventSchema>;
|
||||
|
||||
/** A captured authentication flow event. */
|
||||
export const authFlowEventSchema = z.strictObject({
|
||||
|
|
@ -107,7 +104,6 @@ export const customProtocolCaptureSchema = z.strictObject({
|
|||
/** Whether credentials were detected. */
|
||||
credentials_detected: z.boolean().default(false),
|
||||
});
|
||||
export type CustomProtocolCapture = z.infer<typeof customProtocolCaptureSchema>;
|
||||
|
||||
/** Correlate a frame with process identity. */
|
||||
export function correlateProcessIdentity(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { electronActiveObservationResultSchema } from "../../src/domain/electronActiveObservation.js";
|
||||
import { electronActiveObservationResultSchema } from "./electronActiveObservation.js";
|
||||
|
||||
/** Create a bounded active-capture result for MCP contract tests. */
|
||||
export const createElectronActiveObservationFixtureResult = (
|
||||
|
|
@ -6,10 +6,7 @@ import {
|
|||
maxTreeDepth,
|
||||
reconstructProcessTree,
|
||||
type ProcessTreeEvent,
|
||||
type ProcessEventType,
|
||||
type ProcessNode,
|
||||
type ProcessTreeReconstruction,
|
||||
} from "../../../src/domain/eventProcessTree.js";
|
||||
} from "./eventProcessTree.js";
|
||||
|
||||
describe("event-backed process tree reconstruction", () => {
|
||||
it("reconstructs a simple process tree", () => {
|
||||
|
|
@ -388,37 +385,4 @@ describe("event-backed process tree depth and types", () => {
|
|||
const result = reconstructProcessTree(events, 1);
|
||||
expect(maxTreeDepth(result.processes, 1)).toBe(2);
|
||||
});
|
||||
|
||||
it("uses exported types", () => {
|
||||
const node: ProcessNode = {
|
||||
pid: 1,
|
||||
ppid: null,
|
||||
process_name: "test",
|
||||
executable: "/test",
|
||||
arguments: [],
|
||||
children: [],
|
||||
spawn_time_ms: 0,
|
||||
exit_time_ms: null,
|
||||
exit_code: null,
|
||||
is_re_exec: false,
|
||||
is_reparented: false,
|
||||
previous_ppid: null,
|
||||
};
|
||||
expect(node.pid).toBe(1);
|
||||
const recon: ProcessTreeReconstruction = {
|
||||
root_pid: 1,
|
||||
processes: [node],
|
||||
events_consumed: 1,
|
||||
events_unmatched: 0,
|
||||
short_lived_descendants: 0,
|
||||
has_re_exec: false,
|
||||
has_reparenting: false,
|
||||
};
|
||||
expect(recon.root_pid).toBe(1);
|
||||
});
|
||||
|
||||
it("uses ProcessEventType", () => {
|
||||
const t: ProcessEventType = "spawn";
|
||||
expect(t).toBe("spawn");
|
||||
});
|
||||
});
|
||||
|
|
@ -11,7 +11,6 @@ export const processEventTypeSchema = z.enum([
|
|||
"thread_create",
|
||||
"thread_exit",
|
||||
]);
|
||||
export type ProcessEventType = z.infer<typeof processEventTypeSchema>;
|
||||
|
||||
/** A single event-backed process-tree event. */
|
||||
export const processTreeEventSchema = z.strictObject({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { JsonValue } from "../../src/domain/jsonValue.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
export const ghidraFunctionClassification = () => ({
|
||||
external: false,
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
parseProcedures,
|
||||
parseRelatedAddresses,
|
||||
parseSegments,
|
||||
} from "../../../../src/domain/hopperValues.js";
|
||||
} from "./hopperValues.js";
|
||||
|
||||
describe("Hopper boundary values", () => {
|
||||
it("parses Hopper's address-keyed name and string maps", () => {
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
compareManagedMembers,
|
||||
managedMemberComparisonResultSchema,
|
||||
} from "./managedMemberComparison.js";
|
||||
import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
} from "../dotnet/ManagedPe.fixture.js";
|
||||
|
||||
const comparisonLimits = {
|
||||
max_method_matches: 100,
|
||||
max_field_matches: 100,
|
||||
max_candidates: 10,
|
||||
};
|
||||
|
||||
describe("managed member comparison", () => {
|
||||
it("remaps renamed methods by exact CIL/signature without using names", () => {
|
||||
const leftBytes = buildManagedPeFixture();
|
||||
const rightBytes = buildManagedPeFixture({
|
||||
mvid: Buffer.from("00112233445566778899aabbccddeefe", "hex"),
|
||||
typeName: "A",
|
||||
typeNamespace: "X",
|
||||
methodName: "b",
|
||||
fieldName: "c",
|
||||
});
|
||||
const left = inspect(leftBytes, "/tmp/left.dll");
|
||||
const right = inspect(rightBytes, "/tmp/right.dll");
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidenceId, result: left.result },
|
||||
{ evidenceId: right.evidenceId, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.algorithm.name_matching).toBe("not-used");
|
||||
expect(result.left.mvid).toBe("00112233-4455-6677-8899-aabbccddeeff");
|
||||
expect(result.right.mvid).toBe("33221100-5544-7766-8899-aabbccddeefe");
|
||||
expect(result.matching.exact_il_signature).toBe(1);
|
||||
expect(result.methods).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "unchanged",
|
||||
left: expect.objectContaining({ token: "0x06000001", name: "Main" }),
|
||||
right: expect.objectContaining({ token: "0x06000001", name: "b" }),
|
||||
match: expect.objectContaining({
|
||||
status: "matched",
|
||||
basis: "exact-il-signature",
|
||||
confidence: "exact",
|
||||
}),
|
||||
dimensions: [],
|
||||
}),
|
||||
]);
|
||||
expect(result.limitations).toContain(
|
||||
"Names are reported as observations but are not used as a matching basis.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses structural method shape when build-local token operands drift", () => {
|
||||
const leftBytes = buildManagedPeFixture();
|
||||
const rightBytes = buildManagedPeFixture({
|
||||
ilBody: Buffer.from([
|
||||
0x32, 0x02, 0x7b, 0x02, 0x00, 0x00, 0x04, 0x28, 0x02, 0x00, 0x00, 0x0a,
|
||||
0x2a,
|
||||
]),
|
||||
});
|
||||
const left = inspect(leftBytes, "/tmp/left.dll");
|
||||
const right = inspect(rightBytes, "/tmp/right.dll");
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidenceId, result: left.result },
|
||||
{ evidenceId: right.evidenceId, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.matching.exact_il_signature).toBe(0);
|
||||
expect(result.matching.structural_method_shape).toBe(1);
|
||||
expect(result.methods[0]).toMatchObject({
|
||||
status: "changed",
|
||||
match: { status: "matched", basis: "structural-method-shape" },
|
||||
dimensions: ["cil"],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects member states that disagree with their observed sides or match", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/left.dll");
|
||||
const right = inspect(buildManagedPeFixture(), "/tmp/right.dll");
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidenceId, result: left.result },
|
||||
{ evidenceId: right.evidenceId, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
const method = result.methods[0];
|
||||
expect(method).toBeDefined();
|
||||
if (method === undefined) return;
|
||||
|
||||
expect(
|
||||
managedMemberComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
methods: [{ ...method, status: "unchanged", right: null }],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
managedMemberComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
methods: [
|
||||
{
|
||||
...method,
|
||||
match: {
|
||||
...method.match,
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not match identical instruction-limited method prefixes", () => {
|
||||
const limited = {
|
||||
...MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
maxMethodInstructions: 1,
|
||||
};
|
||||
const left = inspect(
|
||||
buildManagedPeFixture(),
|
||||
"/tmp/left-partial.dll",
|
||||
limited,
|
||||
);
|
||||
const right = inspect(
|
||||
buildManagedPeFixture(),
|
||||
"/tmp/right-partial.dll",
|
||||
limited,
|
||||
);
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidenceId, result: left.result },
|
||||
{ evidenceId: right.evidenceId, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.matching.exact_il_signature).toBe(0);
|
||||
expect(result.matching.structural_method_shape).toBe(0);
|
||||
expect(result.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
left_status: "partial",
|
||||
right_status: "partial",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member comparison uncertainty", () => {
|
||||
it("keeps unmatched members unknown when the opposite page is incomplete", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/left-paged.dll");
|
||||
const right = inspect(buildManagedPeFixture(), "/tmp/right-paged.dll");
|
||||
const leftPartial = {
|
||||
...left.result,
|
||||
fields: {
|
||||
...left.result.fields,
|
||||
items: [],
|
||||
returned: 0,
|
||||
dropped: 1,
|
||||
complete: false,
|
||||
},
|
||||
};
|
||||
const rightPartial = {
|
||||
...right.result,
|
||||
methods: {
|
||||
...right.result.methods,
|
||||
items: [],
|
||||
returned: 0,
|
||||
dropped: 1,
|
||||
complete: false,
|
||||
},
|
||||
};
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidenceId, result: leftPartial },
|
||||
{ evidenceId: right.evidenceId, result: rightPartial },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.summary).toMatchObject({
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: 2,
|
||||
});
|
||||
expect(result.methods[0]).toMatchObject({
|
||||
status: "unknown",
|
||||
left: { token: "0x06000001" },
|
||||
right: null,
|
||||
limitations: [expect.stringContaining("unknown-within-unobserved-page")],
|
||||
});
|
||||
expect(result.fields[0]).toMatchObject({
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: { token: "0x04000001" },
|
||||
limitations: [expect.stringContaining("unknown-within-unobserved-page")],
|
||||
});
|
||||
expect(result.coverage).toEqual({
|
||||
status: "truncated",
|
||||
left_status: "partial",
|
||||
right_status: "partial",
|
||||
omitted_methods: 1,
|
||||
omitted_fields: 1,
|
||||
omitted_candidates: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not guess ambiguous field signature matches", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/left.dll");
|
||||
const leftField = left.result.fields.items[0];
|
||||
expect(leftField).toBeDefined();
|
||||
if (leftField === undefined) return;
|
||||
const duplicatedLeft = {
|
||||
...left.result,
|
||||
fields: {
|
||||
...left.result.fields,
|
||||
items: [
|
||||
leftField,
|
||||
{
|
||||
...leftField,
|
||||
token: "0x04000002",
|
||||
name: "other",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
returned: 2,
|
||||
},
|
||||
};
|
||||
const right = inspect(buildManagedPeFixture(), "/tmp/right.dll");
|
||||
const rightField = right.result.fields.items[0];
|
||||
expect(rightField).toBeDefined();
|
||||
if (rightField === undefined) return;
|
||||
const duplicatedRight = {
|
||||
...right.result,
|
||||
fields: {
|
||||
...right.result.fields,
|
||||
items: [
|
||||
rightField,
|
||||
{
|
||||
...rightField,
|
||||
token: "0x04000002",
|
||||
name: "renamed",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
returned: 2,
|
||||
},
|
||||
};
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidenceId, result: duplicatedLeft },
|
||||
{ evidenceId: right.evidenceId, result: duplicatedRight },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.matching.ambiguous).toBe(1);
|
||||
expect(result.fields).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "unknown",
|
||||
match: expect.objectContaining({
|
||||
status: "ambiguous",
|
||||
basis: "field-signature",
|
||||
candidate_left_tokens: ["0x04000001", "0x04000002"],
|
||||
candidate_right_tokens: ["0x04000001", "0x04000002"],
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const inspect = (
|
||||
bytes: Buffer,
|
||||
path: string,
|
||||
limits: typeof MANAGED_MEMBER_FIXTURE_LIMITS = MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
) => {
|
||||
const target = managedPeFixtureTarget(bytes, path);
|
||||
const result = inspectManagedMembersBytes(bytes, target, limits);
|
||||
return {
|
||||
result,
|
||||
evidenceId: `ev_${hash(Buffer.from(path))}`,
|
||||
};
|
||||
};
|
||||
|
||||
const hash = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
|
@ -13,7 +13,7 @@ import {
|
|||
type SigningInfo,
|
||||
type NativeLibrary,
|
||||
type MobileResource,
|
||||
} from "../../../src/domain/mobileApplicationInvestigation.js";
|
||||
} from "./mobileApplicationInvestigation.js";
|
||||
|
||||
describe("mobile application investigation", () => {
|
||||
it("detects mobile format from extension", () => {
|
||||
|
|
@ -10,14 +10,10 @@ import {
|
|||
swiftDeclsByKind,
|
||||
type ObjcClass,
|
||||
type ObjcMethod,
|
||||
type ObjcProperty,
|
||||
type DbOperation,
|
||||
type ObjcSwiftMetadata,
|
||||
type ObjcProtocol,
|
||||
type ObjcPropertyAttribute,
|
||||
type SwiftDecl,
|
||||
type DbSaveResult,
|
||||
} from "../../../../src/domain/objcSwiftMetadata.js";
|
||||
} from "./objcSwiftMetadata.js";
|
||||
|
||||
describe("ObjC/Swift metadata", () => {
|
||||
it("identifies getter selectors", () => {
|
||||
|
|
@ -105,25 +101,4 @@ describe("ObjC/Swift metadata", () => {
|
|||
const result = objcSwiftMetadataSchema.safeParse(meta);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("uses all exported types", () => {
|
||||
const prop: ObjcProperty = {
|
||||
name: "title",
|
||||
type_encoding: 'T@"NSString"&,N,V_title',
|
||||
attributes: [] as ObjcPropertyAttribute[],
|
||||
is_readonly: false,
|
||||
getter: "title",
|
||||
setter: "setTitle:",
|
||||
};
|
||||
expect(prop.name).toBe("title");
|
||||
const op: DbOperation = "save";
|
||||
expect(op).toBe("save");
|
||||
const meta: ObjcSwiftMetadata = {
|
||||
objc_classes: [],
|
||||
objc_protocols: [],
|
||||
swift_decls: [],
|
||||
db_save_result: null,
|
||||
};
|
||||
expect(meta.objc_classes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -10,8 +10,7 @@ import {
|
|||
resourcesByType,
|
||||
totalResourceSize,
|
||||
type PackageManifest,
|
||||
type DigitalSignature,
|
||||
} from "../../../src/domain/packageAnalysis.js";
|
||||
} from "./packageAnalysis.js";
|
||||
|
||||
describe("package analysis", () => {
|
||||
it("detects package format from extension", () => {
|
||||
|
|
@ -99,9 +98,4 @@ describe("package analysis", () => {
|
|||
};
|
||||
expect(hasValidSignature(unsigned)).toBe(false);
|
||||
});
|
||||
|
||||
it("uses DigitalSignature type", () => {
|
||||
const sig: DigitalSignature = validManifest.signature;
|
||||
expect(sig.is_signed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -44,7 +44,6 @@ export const digitalSignatureSchema = z.strictObject({
|
|||
/** Timestamp signer if present. */
|
||||
timestamp_signer: z.string().nullable(),
|
||||
});
|
||||
export type DigitalSignature = z.infer<typeof digitalSignatureSchema>;
|
||||
|
||||
/** A Windows package manifest for static analysis. */
|
||||
export const packageManifestSchema = z.strictObject({
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@ import {
|
|||
parseSectionCharacteristics,
|
||||
peManifestSchema,
|
||||
type PeManifest,
|
||||
type PeSection,
|
||||
type PeImport,
|
||||
type PeExport,
|
||||
type PeResource,
|
||||
type PeDebugEntry,
|
||||
type PeTlsCallback,
|
||||
} from "../../../../src/domain/peInspection.js";
|
||||
} from "./peInspection.js";
|
||||
|
||||
const validManifest: PeManifest = {
|
||||
dos_magic: 0x5a4d,
|
||||
|
|
@ -196,19 +190,4 @@ describe("PE/COFF static inspection", () => {
|
|||
it("counts executable sections", () => {
|
||||
expect(countExecutableSections(validManifest)).toBe(1);
|
||||
});
|
||||
|
||||
it("uses all exported types", () => {
|
||||
const section: PeSection = validManifest.sections[0]!;
|
||||
expect(section.name).toBe(".text");
|
||||
const imp: PeImport = validManifest.imports[0]!;
|
||||
expect(imp.dll).toBe("KERNEL32.DLL");
|
||||
const exp: PeExport = validManifest.exports[0]!;
|
||||
expect(exp.name).toBe("DllMain");
|
||||
const res: PeResource = validManifest.resources[0]!;
|
||||
expect(res.type).toBe("RT_VERSION");
|
||||
const dbg: PeDebugEntry = validManifest.debug_entries[0]!;
|
||||
expect(dbg.type).toBe("CODEVIEW");
|
||||
const tls: PeTlsCallback = validManifest.tls_callbacks[0]!;
|
||||
expect(tls.index).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -25,7 +25,6 @@ export const peSectionSchema = z.strictObject({
|
|||
raw_offset: z.number().int().nonnegative(),
|
||||
characteristics: sectionCharacteristicsSchema,
|
||||
});
|
||||
export type PeSection = z.infer<typeof peSectionSchema>;
|
||||
|
||||
/** PE import entry. */
|
||||
export const peImportSchema = z.strictObject({
|
||||
|
|
@ -34,7 +33,6 @@ export const peImportSchema = z.strictObject({
|
|||
ordinal: z.number().int().nullable(),
|
||||
is_delay_import: z.boolean().default(false),
|
||||
});
|
||||
export type PeImport = z.infer<typeof peImportSchema>;
|
||||
|
||||
/** PE export entry. */
|
||||
export const peExportSchema = z.strictObject({
|
||||
|
|
@ -43,7 +41,6 @@ export const peExportSchema = z.strictObject({
|
|||
rva: z.number().int().nonnegative(),
|
||||
forwarder: z.string().nullable(),
|
||||
});
|
||||
export type PeExport = z.infer<typeof peExportSchema>;
|
||||
|
||||
/** PE/COFF resource entry. */
|
||||
export const peResourceSchema = z.strictObject({
|
||||
|
|
@ -53,7 +50,6 @@ export const peResourceSchema = z.strictObject({
|
|||
rva: z.number().int().nonnegative(),
|
||||
size: z.number().int().nonnegative(),
|
||||
});
|
||||
export type PeResource = z.infer<typeof peResourceSchema>;
|
||||
|
||||
/** PE debug directory entry. */
|
||||
export const peDebugEntrySchema = z.strictObject({
|
||||
|
|
@ -67,14 +63,12 @@ export const peDebugEntrySchema = z.strictObject({
|
|||
/** PDB path if this is a CodeView entry. */
|
||||
pdb_path: z.string().nullable(),
|
||||
});
|
||||
export type PeDebugEntry = z.infer<typeof peDebugEntrySchema>;
|
||||
|
||||
/** TLS callback entry. */
|
||||
export const peTlsCallbackSchema = z.strictObject({
|
||||
callback_rva: z.number().int().nonnegative(),
|
||||
index: z.number().int().nonnegative(),
|
||||
});
|
||||
export type PeTlsCallback = z.infer<typeof peTlsCallbackSchema>;
|
||||
|
||||
/** Architecture/machine type. */
|
||||
export const peMachineTypeSchema = z.enum([
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { expect, it } from "vitest";
|
|||
import {
|
||||
compareUnverifiedProcessCaptures as compareProcessCaptures,
|
||||
emptyProcessCapture as emptyCapture,
|
||||
} from "../../fixtures/processCapture.js";
|
||||
} from "./processCapture.fixture.js";
|
||||
|
||||
it("classifies missing observations as unknown and one-sided evidence as added", () => {
|
||||
const base = {
|
||||
|
|
@ -1,17 +1,22 @@
|
|||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../src/contracts/processCaptureExample.js";
|
||||
import {
|
||||
compareProcessCaptures,
|
||||
parseProcessCapture,
|
||||
processCaptureSchema,
|
||||
type UnverifiedProcessCapture,
|
||||
} from "../../src/domain/processCapture.js";
|
||||
} from "./processCapture.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js";
|
||||
|
||||
export { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCaptureExample.js";
|
||||
|
||||
/** Parse a detached valid empty capture for tests that need trusted evidence. */
|
||||
export const emptyProcessCapture = () =>
|
||||
parseProcessCapture(EMPTY_PROCESS_CAPTURE_EXAMPLE);
|
||||
|
||||
/** Parse a mutable unverified capture for boundary-validation tests. */
|
||||
export const emptyUnverifiedProcessCapture = (): UnverifiedProcessCapture =>
|
||||
processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE);
|
||||
|
||||
/** Project validation failures without coupling tests to Zod internals. */
|
||||
export const processCaptureIssues = (
|
||||
capture: UnverifiedProcessCapture,
|
||||
): readonly { readonly path: string; readonly message: string }[] => {
|
||||
|
|
@ -24,6 +29,7 @@ export const processCaptureIssues = (
|
|||
}));
|
||||
};
|
||||
|
||||
/** Compare unverified fixture values through the production parser. */
|
||||
export const compareUnverifiedProcessCaptures = (
|
||||
left: UnverifiedProcessCapture,
|
||||
right: UnverifiedProcessCapture,
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { digestProcessCommitment } from "../../../src/domain/processCapture.js";
|
||||
import { digestProcessCommitment } from "./processCapture.js";
|
||||
import {
|
||||
compareUnverifiedProcessCaptures as compareProcessCaptures,
|
||||
emptyProcessCapture as emptyCapture,
|
||||
} from "../../fixtures/processCapture.js";
|
||||
} from "./processCapture.fixture.js";
|
||||
|
||||
it("detects changes in normalized process sample metadata", () => {
|
||||
const capture = {
|
||||
|
|
@ -1,19 +1,17 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { createProcessCaptureJournal } from "../../../src/application/ProcessCaptureJournal.js";
|
||||
import {
|
||||
digestProcessCommitment,
|
||||
LEGACY_PROCESS_CAPTURE_MESSAGE,
|
||||
parseProcessCapture,
|
||||
processCaptureSchema,
|
||||
type ProcessCaptureEventJournalEntry,
|
||||
type UnverifiedProcessCapture,
|
||||
} from "../../../src/domain/processCapture.js";
|
||||
} from "./processCapture.js";
|
||||
import {
|
||||
compareUnverifiedProcessCaptures as compareProcessCaptures,
|
||||
emptyUnverifiedProcessCapture as emptyCapture,
|
||||
processCaptureIssues,
|
||||
} from "../../fixtures/processCapture.js";
|
||||
} from "./processCapture.fixture.js";
|
||||
|
||||
it("never considers truncated captures equivalent", () => {
|
||||
const capture = {
|
||||
|
|
@ -102,33 +100,6 @@ it("rejects settlement and cleanup combinations that cannot occur", () => {
|
|||
});
|
||||
|
||||
it("accepts old captures without a journal and validates complete journals", () => {
|
||||
const journal = createProcessCaptureJournal();
|
||||
const observed: ProcessCaptureEventJournalEntry[] = [];
|
||||
const unsubscribe = journal.subscribe((entry) => observed.push(entry));
|
||||
journal.recordEvent("lifecycle", 0);
|
||||
const settlementEntry = journal.record("lifecycle", 1);
|
||||
unsubscribe();
|
||||
journal.recordEvent("frames", 0);
|
||||
expect(journal.entries).toEqual([
|
||||
{ capture_order: 0, collection: "lifecycle", index: 0 },
|
||||
{ capture_order: 1, collection: "lifecycle", index: 1 },
|
||||
{ capture_order: 2, collection: "frames", index: 0 },
|
||||
]);
|
||||
expect(observed).toEqual(journal.entries.slice(0, 2));
|
||||
expect(settlementEntry).toEqual(journal.entries[1]);
|
||||
|
||||
const nestedJournal = createProcessCaptureJournal();
|
||||
const notifications: string[] = [];
|
||||
nestedJournal.subscribe((entry) => {
|
||||
notifications.push(`a:${String(entry.capture_order)}`);
|
||||
if (entry.capture_order === 0) nestedJournal.record("frames", 1);
|
||||
});
|
||||
nestedJournal.subscribe((entry) =>
|
||||
notifications.push(`b:${String(entry.capture_order)}`),
|
||||
);
|
||||
nestedJournal.record("frames", 0);
|
||||
expect(notifications).toEqual(["a:0", "b:0", "a:1", "b:1"]);
|
||||
|
||||
const capture = emptyCapture();
|
||||
const { event_journal: _eventJournal, ...oldCapture } = capture;
|
||||
expect(parseProcessCapture(oldCapture).event_journal).toEqual([]);
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import type { JsonValue } from "./jsonValue.js";
|
||||
import { digestProcessCommitment } from "./processCapture.js";
|
||||
|
||||
const normalization = {
|
||||
paths: true,
|
||||
pids: true,
|
||||
ports: true,
|
||||
time_bucket_ms: 10,
|
||||
patterns: [],
|
||||
};
|
||||
const scenario = { executable_sha256: "0".repeat(64) };
|
||||
const comparisonContract = {};
|
||||
const shimPlan: JsonValue[] = [];
|
||||
const replayPlan = {};
|
||||
|
||||
/** Minimal valid process capture retained as a public contract example. */
|
||||
export const EMPTY_PROCESS_CAPTURE_EXAMPLE = {
|
||||
schema_version: 4,
|
||||
manifest: {
|
||||
rea_version: "1.1.0",
|
||||
provider_version: "3",
|
||||
platform: "fixture",
|
||||
architecture: "fixture",
|
||||
pty_backend: "node-pty",
|
||||
started_at: "2026-01-01T00:00:00.000Z",
|
||||
completed_at: "2026-01-01T00:00:00.001Z",
|
||||
scenario,
|
||||
comparison_contract: comparisonContract,
|
||||
shim_plan: shimPlan,
|
||||
replay_plan: replayPlan,
|
||||
full_scenario_sha256: digestProcessCommitment(scenario),
|
||||
comparison_contract_sha256: digestProcessCommitment(comparisonContract),
|
||||
executable_sha256: "0".repeat(64),
|
||||
normalization_sha256: digestProcessCommitment(normalization),
|
||||
shim_plan_sha256: digestProcessCommitment(shimPlan),
|
||||
replay_plan_sha256: digestProcessCommitment(replayPlan),
|
||||
},
|
||||
normalization,
|
||||
frames: [],
|
||||
rendered_frames: [],
|
||||
interaction_events: [],
|
||||
exit: { code: 0, signal: null, reason: "exited" },
|
||||
settlement: {
|
||||
state: "quiesced",
|
||||
elapsed_ms: 50,
|
||||
cleanup_outcome: "not_required",
|
||||
},
|
||||
process_samples: [],
|
||||
filesystem_checkpoints: [
|
||||
{ name: "before", at_ms: 0, files: [], effects: [], truncated: false },
|
||||
{
|
||||
name: "after_settlement",
|
||||
at_ms: 50,
|
||||
files: [],
|
||||
effects: [],
|
||||
truncated: false,
|
||||
},
|
||||
],
|
||||
shim_events: [],
|
||||
protocol_events: [],
|
||||
replay_transitions: [],
|
||||
files_before: [],
|
||||
files_after: [],
|
||||
filesystem_effects: [],
|
||||
truncated: false,
|
||||
limitations: [],
|
||||
residual_unknowns: [],
|
||||
cleanup: {
|
||||
owned_process_group: "verified",
|
||||
temporary_root: "removed",
|
||||
},
|
||||
} satisfies JsonValue;
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../../../src/contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js";
|
||||
import {
|
||||
processCaptureSchema,
|
||||
type UnverifiedProcessCapture,
|
||||
} from "../../../src/domain/processCapture.js";
|
||||
} from "./processCapture.js";
|
||||
import {
|
||||
createProcessObservation,
|
||||
projectProcessObservation,
|
||||
} from "../../../src/domain/processObservation.js";
|
||||
} from "./processObservation.js";
|
||||
|
||||
const emptyCapture = processCaptureSchema.parse(EMPTY_PROCESS_CAPTURE_EXAMPLE);
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js";
|
||||
import {
|
||||
compareProcessCaptures,
|
||||
parseProcessCapture,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js";
|
||||
import { parseProcessCapture, type ProcessCapture } from "./processCapture.js";
|
||||
import {
|
||||
compareProcessTraces,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js";
|
||||
import { parseProcessCapture, type ProcessCapture } from "./processCapture.js";
|
||||
import {
|
||||
compareProcessTraces,
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ import {
|
|||
looksLikeCredential,
|
||||
protocolCaptureSchema,
|
||||
type ProtocolMessage,
|
||||
type ProtocolCapture,
|
||||
} from "../../../src/domain/protocolCapture.js";
|
||||
} from "./protocolCapture.js";
|
||||
|
||||
describe("protocol capture", () => {
|
||||
it("classifies gRPC from content type", () => {
|
||||
|
|
@ -179,19 +178,6 @@ describe("schema inference", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("uses ProtocolCapture type", () => {
|
||||
it("validates a well-formed capture typed value", () => {
|
||||
const capture: ProtocolCapture = {
|
||||
family: "grpc",
|
||||
messages: [],
|
||||
inferred_schema: [],
|
||||
has_truncated: false,
|
||||
credentials_detected: false,
|
||||
};
|
||||
expect(capture.family).toBe("grpc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("protocol capture schema validation", () => {
|
||||
it("validates a well-formed capture", () => {
|
||||
const capture = {
|
||||
|
|
@ -77,7 +77,6 @@ export const protocolCaptureSchema = z.strictObject({
|
|||
/** Whether credentials were detected and redacted. */
|
||||
credentials_detected: z.boolean().default(false),
|
||||
});
|
||||
export type ProtocolCapture = z.infer<typeof protocolCaptureSchema>;
|
||||
|
||||
/** Classification of a byte sequence as a known protocol family. */
|
||||
export function classifyProtocolFamily(
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
verifyReconstruction,
|
||||
} from "./reconstructionVerification.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../contracts/processCaptureExample.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "./processCapture.fixture.js";
|
||||
import { ARTIFACT_COMPARISON_EXAMPLE } from "../contracts/artifactComparisonExample.js";
|
||||
import { createResidualUnknown } from "./residualUnknown.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
replayMachineRunInputSchema,
|
||||
runReplayMachine,
|
||||
} from "../../../../src/domain/replayMachineRun.js";
|
||||
} from "./replayMachineRun.js";
|
||||
|
||||
const loginRun = () =>
|
||||
replayMachineRunInputSchema.parse({
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { replayMachineSchema } from "../../../../src/domain/replayMachine.js";
|
||||
import { ReplayMachineRuntime } from "../../../../src/domain/replayMachineRuntime.js";
|
||||
import { replayMachineSchema } from "./replayMachine.js";
|
||||
import { ReplayMachineRuntime } from "./replayMachineRuntime.js";
|
||||
|
||||
const machine = replayMachineSchema.parse({
|
||||
initial_state: "login",
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { replayMachineSchema } from "../../../../src/domain/replayMachine.js";
|
||||
import { ReplayMachineRuntime } from "../../../../src/domain/replayMachineRuntime.js";
|
||||
import { replayMachineSchema } from "./replayMachine.js";
|
||||
import { ReplayMachineRuntime } from "./replayMachineRuntime.js";
|
||||
|
||||
const machine = replayMachineSchema.parse({
|
||||
initial_state: "login",
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
buildNativePeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
describe("managed artifact failure classification", () => {
|
||||
it("distinguishes native, malformed, unsupported, and bounded metadata", () => {
|
||||
const nativeBytes = buildNativePeFixture();
|
||||
const native = inspectManagedArtifactBytes(
|
||||
nativeBytes,
|
||||
managedPeFixtureTarget(nativeBytes),
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
);
|
||||
expect(native).toMatchObject({
|
||||
classification: { status: "not-managed" },
|
||||
metadata: { status: "absent" },
|
||||
coverage: { state: "unavailable" },
|
||||
});
|
||||
|
||||
const malformedBytes = buildManagedPeFixture({
|
||||
corruptMetadataSignature: true,
|
||||
});
|
||||
const malformed = inspectManagedArtifactBytes(
|
||||
malformedBytes,
|
||||
managedPeFixtureTarget(malformedBytes),
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
);
|
||||
expect(malformed.classification.status).toBe("malformed");
|
||||
expect(malformed.coverage.issues).toEqual([
|
||||
expect.objectContaining({ code: "invalid-metadata-root" }),
|
||||
]);
|
||||
|
||||
const unsupportedTableBytes = buildManagedPeFixture({
|
||||
metadataValidMaskExtra: 1n << 50n,
|
||||
});
|
||||
const unsupportedTable = inspectManagedArtifactBytes(
|
||||
unsupportedTableBytes,
|
||||
managedPeFixtureTarget(unsupportedTableBytes),
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
);
|
||||
expect(unsupportedTable.coverage.issues).toEqual([
|
||||
expect.objectContaining({ code: "invalid-tables" }),
|
||||
]);
|
||||
|
||||
const limitedBytes = buildManagedPeFixture();
|
||||
const limited = inspectManagedArtifactBytes(
|
||||
limitedBytes,
|
||||
managedPeFixtureTarget(limitedBytes),
|
||||
{ ...MANAGED_ARTIFACT_FIXTURE_LIMITS, maxMetadataBytes: 256 },
|
||||
);
|
||||
expect(limited.metadata.status).toBe("partial");
|
||||
expect(limited.coverage.issues).toEqual([
|
||||
expect.objectContaining({ code: "limit-exceeded" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,60 +2,26 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../../src/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../../../../src/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { buildManagedPeFixture } from "../../../fixtures/managedPe.js";
|
||||
|
||||
const limits = {
|
||||
referenceOffset: 0,
|
||||
referenceLimit: 100,
|
||||
resourceOffset: 0,
|
||||
resourceLimit: 100,
|
||||
attributeOffset: 0,
|
||||
attributeLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
const nativeBoundaryLimits = {
|
||||
moduleRefOffset: 0,
|
||||
moduleRefLimit: 100,
|
||||
importOffset: 0,
|
||||
importLimit: 100,
|
||||
implementationOffset: 0,
|
||||
implementationLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
describe("managed artifact inventory", () => {
|
||||
it("inventories module, assembly, framework, references, and resources without loading CLR code", () => {
|
||||
const resource = Buffer.from("source-owned resource");
|
||||
const bytes = buildManagedPeFixture({ resourceData: resource });
|
||||
const result = inspectManagedArtifactBytes(bytes, target(bytes), limits);
|
||||
const result = inspectManagedArtifactBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.classification).toMatchObject({
|
||||
status: "managed",
|
||||
|
|
@ -100,10 +66,14 @@ describe("managed artifact inventory", () => {
|
|||
const bytes = buildManagedPeFixture({
|
||||
references: ["System.Runtime", "UnityEngine.CoreModule"],
|
||||
});
|
||||
const result = inspectManagedArtifactBytes(bytes, target(bytes), {
|
||||
...limits,
|
||||
referenceLimit: 1,
|
||||
});
|
||||
const result = inspectManagedArtifactBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
{
|
||||
...MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
referenceLimit: 1,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.classification.runtime_family).toBe("unity-mono");
|
||||
expect(result.references).toMatchObject({
|
||||
|
|
@ -119,16 +89,20 @@ describe("managed artifact inventory", () => {
|
|||
const bytes = buildManagedPeFixture({
|
||||
mvid: Buffer.from("3aebc60edc4a544b1f458b4ed40b33b1", "hex"),
|
||||
});
|
||||
const result = inspectManagedArtifactBytes(bytes, target(bytes), limits);
|
||||
const result = inspectManagedArtifactBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_ARTIFACT_FIXTURE_LIMITS,
|
||||
);
|
||||
const members = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
const boundaries = inspectManagedNativeBoundariesBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
nativeBoundaryLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.module?.mvid).toBe("0ec6eb3a-4adc-4b54-1f45-8b4ed40b33b1");
|
||||
|
|
@ -146,8 +120,8 @@ describe("managed artifact inventory", () => {
|
|||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.fields.items[0]?.signature).toMatchObject({
|
||||
|
|
@ -161,13 +135,3 @@ describe("managed artifact inventory", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
const target = (bytes: Buffer): BinaryTarget => ({
|
||||
path: "/fixture.exe",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
|
@ -2,28 +2,12 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.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,
|
||||
};
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
describe("managed CIL decoding", () => {
|
||||
it("reads fat method header size from the full flags-and-size word", () => {
|
||||
|
|
@ -41,8 +25,8 @@ describe("managed CIL decoding", () => {
|
|||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.methods.items[0]?.body).toMatchObject({
|
||||
|
|
@ -68,8 +52,8 @@ describe("managed CIL decoding", () => {
|
|||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.methods.items[0]?.body).toMatchObject({
|
||||
|
|
@ -96,13 +80,13 @@ describe("managed CIL decoding", () => {
|
|||
});
|
||||
const tiny = inspectManagedMembersBytes(
|
||||
tinyBytes,
|
||||
target(tinyBytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(tinyBytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
const fat = inspectManagedMembersBytes(
|
||||
fatBytes,
|
||||
target(fatBytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(fatBytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(tiny.methods.items[0]?.body).toMatchObject({
|
||||
|
|
@ -128,13 +112,3 @@ describe("managed CIL decoding", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
const target = (bytes: Buffer): BinaryTarget => ({
|
||||
path: "/fixture.exe",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
|
@ -2,36 +2,24 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.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,
|
||||
};
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
describe("managed CIL limits", () => {
|
||||
it("marks instruction-limited CIL as partial without assigning normalized identity", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const result = inspectManagedMembersBytes(bytes, target(bytes), {
|
||||
...memberLimits,
|
||||
maxMethodInstructions: 1,
|
||||
});
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
{
|
||||
...MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
maxMethodInstructions: 1,
|
||||
},
|
||||
);
|
||||
const il = bytes.subarray(0x0a01, 0x0a0d);
|
||||
|
||||
expect(result.metadata.status).toBe("complete");
|
||||
|
|
@ -63,13 +51,3 @@ describe("managed CIL limits", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
const target = (bytes: Buffer): BinaryTarget => ({
|
||||
path: "/fixture.exe",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
|
@ -1,42 +1,24 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.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,
|
||||
};
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
describe("managed member inspection", () => {
|
||||
it("inspects metadata members, signatures, CIL hashes, call edges, and field anchors", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const result = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.identity_scope).toEqual({
|
||||
token_identity: "build-local",
|
||||
requires_artifact_sha256: target(bytes).sha256,
|
||||
requires_artifact_sha256: managedPeFixtureTarget(bytes).sha256,
|
||||
requires_mvid: "00112233-4455-6677-8899-aabbccddeeff",
|
||||
});
|
||||
expect(result.types.items).toEqual([
|
||||
|
|
@ -130,13 +112,3 @@ describe("managed member inspection", () => {
|
|||
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
|
||||
});
|
||||
});
|
||||
const target = (bytes: Buffer): BinaryTarget => ({
|
||||
path: "/fixture.exe",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
|
@ -1,45 +1,14 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js";
|
||||
import { inspectManagedMembersBytes } from "../../../../src/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../../../../src/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
buildNativePeFixture,
|
||||
} 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,
|
||||
};
|
||||
|
||||
const nativeBoundaryLimits = {
|
||||
moduleRefOffset: 0,
|
||||
moduleRefLimit: 100,
|
||||
importOffset: 0,
|
||||
importLimit: 100,
|
||||
implementationOffset: 0,
|
||||
implementationLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
|
||||
} from "./ManagedPe.fixture.js";
|
||||
|
||||
describe("managed native boundaries", () => {
|
||||
it("inspects managed/native PInvoke declarations without verifying native exports", () => {
|
||||
|
|
@ -53,8 +22,8 @@ describe("managed native boundaries", () => {
|
|||
});
|
||||
const result = inspectManagedNativeBoundariesBytes(
|
||||
bytes,
|
||||
target(bytes),
|
||||
nativeBoundaryLimits,
|
||||
managedPeFixtureTarget(bytes),
|
||||
MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS,
|
||||
);
|
||||
|
||||
expect(result.cli_native).toMatchObject({
|
||||
|
|
@ -109,11 +78,15 @@ describe("managed native boundaries", () => {
|
|||
|
||||
it("keeps member evidence bounded and typed for pagination and unavailable metadata", () => {
|
||||
const bytes = buildManagedPeFixture();
|
||||
const paged = inspectManagedMembersBytes(bytes, target(bytes), {
|
||||
...memberLimits,
|
||||
methodLimit: 0 + 1,
|
||||
instructionAnchorLimit: 1,
|
||||
});
|
||||
const paged = inspectManagedMembersBytes(
|
||||
bytes,
|
||||
managedPeFixtureTarget(bytes),
|
||||
{
|
||||
...MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
methodLimit: 0 + 1,
|
||||
instructionAnchorLimit: 1,
|
||||
},
|
||||
);
|
||||
expect(paged.methods).toMatchObject({
|
||||
total: 1,
|
||||
returned: 1,
|
||||
|
|
@ -124,8 +97,8 @@ describe("managed native boundaries", () => {
|
|||
const nativeBytes = buildNativePeFixture();
|
||||
const native = inspectManagedMembersBytes(
|
||||
nativeBytes,
|
||||
target(nativeBytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(nativeBytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
expect(native.metadata.status).toBe("absent");
|
||||
expect(native.coverage.state).toBe("unavailable");
|
||||
|
|
@ -135,8 +108,8 @@ describe("managed native boundaries", () => {
|
|||
});
|
||||
const malformed = inspectManagedMembersBytes(
|
||||
malformedBytes,
|
||||
target(malformedBytes),
|
||||
memberLimits,
|
||||
managedPeFixtureTarget(malformedBytes),
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
);
|
||||
expect(malformed.metadata.status).toBe("malformed");
|
||||
expect(malformed.coverage.issues).toEqual([
|
||||
|
|
@ -144,13 +117,3 @@ describe("managed native boundaries", () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
const target = (bytes: Buffer): BinaryTarget => ({
|
||||
path: "/fixture.exe",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
|
@ -1,3 +1,7 @@
|
|||
import { createHash } from "node:crypto";
|
||||
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
|
||||
interface ManagedPeFixtureOptions {
|
||||
readonly cliFlags?: number;
|
||||
readonly corruptMetadataSignature?: boolean;
|
||||
|
|
@ -69,6 +73,73 @@ const DEFAULT_MVID = Buffer.from([
|
|||
0xdd, 0xee, 0xff,
|
||||
]);
|
||||
|
||||
/** Generous bounds for inventory assertions over the source-owned PE fixture. */
|
||||
export const MANAGED_ARTIFACT_FIXTURE_LIMITS = {
|
||||
referenceOffset: 0,
|
||||
referenceLimit: 100,
|
||||
resourceOffset: 0,
|
||||
resourceLimit: 100,
|
||||
attributeOffset: 0,
|
||||
attributeLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
/** Generous bounds for member assertions over the source-owned PE fixture. */
|
||||
export const MANAGED_MEMBER_FIXTURE_LIMITS = {
|
||||
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,
|
||||
};
|
||||
|
||||
/** Member bounds for fixture tests that inspect a real local path. */
|
||||
export const MANAGED_MEMBER_PATH_FIXTURE_LIMITS = {
|
||||
...MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
maxFileBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
/** Generous native-boundary bounds for the source-owned PE fixture. */
|
||||
export const MANAGED_NATIVE_BOUNDARY_FIXTURE_LIMITS = {
|
||||
moduleRefOffset: 0,
|
||||
moduleRefLimit: 100,
|
||||
importOffset: 0,
|
||||
importLimit: 100,
|
||||
implementationOffset: 0,
|
||||
implementationLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
/** Describe fixture bytes as the managed PE target consumed by static inspectors. */
|
||||
export const managedPeFixtureTarget = (
|
||||
bytes: Buffer,
|
||||
path = "/fixture.exe",
|
||||
): BinaryTarget => ({
|
||||
path,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
||||
const guidHeap = (mvid: Buffer = DEFAULT_MVID): Buffer => {
|
||||
if (mvid.length !== 16)
|
||||
throw new RangeError("Managed PE fixture MVID must be 16 bytes");
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
runDoctor,
|
||||
type DoctorHost,
|
||||
} from "../../../../src/application/Doctor.js";
|
||||
import { runDoctor, type DoctorHost } from "../application/Doctor.js";
|
||||
import { createDoctorHostFixture } from "../application/Doctor.fixture.js";
|
||||
import {
|
||||
inspectGhidraInstallation,
|
||||
type GhidraInstallationHost,
|
||||
type GhidraInstallationInspection,
|
||||
} from "../../../../src/ghidra/GhidraInstallation.js";
|
||||
import { projectGhidraDoctorInspection } from "../../../../src/ghidra/GhidraDoctor.js";
|
||||
import { CATALOG_IDENTITY } from "../../../../src/catalogIdentity.js";
|
||||
import { PRODUCT_IDENTITY } from "../../../../src/identity.js";
|
||||
} from "./GhidraInstallation.js";
|
||||
import { projectGhidraDoctorInspection } from "./GhidraDoctor.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||
const installationHost = (
|
||||
|
|
@ -51,39 +47,34 @@ const inspection = (
|
|||
const doctorHost = (
|
||||
ghidra: GhidraInstallationInspection,
|
||||
hopperAvailable = false,
|
||||
): DoctorHost => ({
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
nodeVersion: "24.18.0",
|
||||
macosVersion: () => Promise.resolve(undefined),
|
||||
linuxDistribution: () =>
|
||||
Promise.resolve({
|
||||
id: "ubuntu",
|
||||
versionId: "24.04",
|
||||
packageFamily: "deb",
|
||||
supported: true,
|
||||
}),
|
||||
validTarget: () => Promise.resolve(true),
|
||||
executable: (path) =>
|
||||
Promise.resolve(hopperAvailable && path.includes("Hopper")),
|
||||
supportedLinuxHopper: () => Promise.resolve(true),
|
||||
linuxDemoRuntimeCheck: () =>
|
||||
Promise.resolve({
|
||||
name: "hopper-demo-runtime",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
}),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
providerInspections: () =>
|
||||
Promise.resolve([projectGhidraDoctorInspection(ghidra)]),
|
||||
installedSkillIdentity: () =>
|
||||
Promise.resolve({
|
||||
version: PRODUCT_IDENTITY.skillVersion,
|
||||
toolCount: CATALOG_IDENTITY.counts.mcp_tools,
|
||||
catalogDigest: CATALOG_IDENTITY.digests.combined_sha256,
|
||||
}),
|
||||
});
|
||||
): DoctorHost =>
|
||||
createDoctorHostFixture({
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
nodeVersion: "24.18.0",
|
||||
macosVersion: () => Promise.resolve(undefined),
|
||||
linuxDistribution: () =>
|
||||
Promise.resolve({
|
||||
id: "ubuntu",
|
||||
versionId: "24.04",
|
||||
packageFamily: "deb",
|
||||
supported: true,
|
||||
}),
|
||||
validTarget: () => Promise.resolve(true),
|
||||
executable: (path) =>
|
||||
Promise.resolve(hopperAvailable && path.includes("Hopper")),
|
||||
supportedLinuxHopper: () => Promise.resolve(true),
|
||||
linuxDemoRuntimeCheck: () =>
|
||||
Promise.resolve({
|
||||
name: "hopper-demo-runtime",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
}),
|
||||
brewHopperPath: () => Promise.resolve(undefined),
|
||||
manualHopperPaths: () => Promise.resolve([]),
|
||||
providerInspections: () =>
|
||||
Promise.resolve([projectGhidraDoctorInspection(ghidra)]),
|
||||
});
|
||||
|
||||
describe("Ghidra doctor integration", () => {
|
||||
it("accepts a valid BYO Ghidra engine when Hopper is absent", async () => {
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { JsonValue } from "../../../../src/domain/jsonValue.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
GHIDRA_FUNCTION_OPERATIONS,
|
||||
isGhidraFunctionOperation,
|
||||
parseGhidraFunctionInput,
|
||||
parseGhidraFunctionResult,
|
||||
type GhidraFunctionOperation,
|
||||
} from "../../../../src/ghidra/GhidraFunctionValues.js";
|
||||
} from "./GhidraFunctionValues.js";
|
||||
import {
|
||||
ghidraBounded,
|
||||
ghidraFunctionClassification,
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
ghidraFunctionIdentity,
|
||||
ghidraNativeApiBoundary,
|
||||
ghidraReferenceEdge,
|
||||
} from "../../../fixtures/ghidraFunction.js";
|
||||
} from "../domain/hopperValues.fixture.js";
|
||||
|
||||
describe("Ghidra function-analysis boundary values", () => {
|
||||
it("defaults bounded inputs and rejects undeclared or implicit addresses", () => {
|
||||
|
|
@ -7,7 +7,7 @@ import {
|
|||
SUPPORTED_GHIDRA_VERSION,
|
||||
type GhidraInstallationHost,
|
||||
type GhidraJavaObservation,
|
||||
} from "../../../../src/ghidra/GhidraInstallation.js";
|
||||
} from "./GhidraInstallation.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra";
|
||||
const PROPERTIES = `${INSTALL}/Ghidra/application.properties`;
|
||||
|
|
@ -6,8 +6,8 @@ import {
|
|||
parseGhidraInventoryInput,
|
||||
parseGhidraInventoryResult,
|
||||
type GhidraInventoryOperation,
|
||||
} from "../../../../src/ghidra/GhidraInventoryValues.js";
|
||||
import type { JsonValue } from "../../../../src/domain/jsonValue.js";
|
||||
} from "./GhidraInventoryValues.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
describe("Ghidra inventory boundary values", () => {
|
||||
it("defaults bounded provider inputs without accepting undeclared fields", () => {
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseConfig } from "../../../../src/config.js";
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js";
|
||||
import { parseConfig } from "../config.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import {
|
||||
GHIDRA_PROVIDER_IDENTITY,
|
||||
GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
GhidraProvider,
|
||||
type GhidraProviderClientFactory,
|
||||
} from "../../../../src/ghidra/GhidraProvider.js";
|
||||
import type { GhidraInstallationHost } from "../../../../src/ghidra/GhidraInstallation.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../../../../src/ghidra/GhidraSessionValues.js";
|
||||
import { err, ok } from "../../../../src/domain/result.js";
|
||||
import { GhidraSessionError } from "../../../../src/ghidra/GhidraSessionError.js";
|
||||
import { silentLogger } from "../../../../src/logger.js";
|
||||
} from "./GhidraProvider.js";
|
||||
import type { GhidraInstallationHost } from "./GhidraInstallation.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "./GhidraSessionValues.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { GhidraSessionError } from "./GhidraSessionError.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||
const installationHost = (): GhidraInstallationHost => ({
|
||||
|
|
@ -61,7 +61,6 @@ describe("Ghidra provider", () => {
|
|||
GHIDRA_PROVIDER_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
expect(GHIDRA_PROVIDER_TOOL_CONTRACTS).toHaveLength(19);
|
||||
expect(Object.isFrozen(ghidra.capabilities())).toBe(true);
|
||||
expect(ghidra.capabilities()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseConfig } from "../../../src/config.js";
|
||||
import type { BinaryTarget } from "../../../src/domain/binaryTarget.js";
|
||||
import { HopperProvider } from "../../../src/hopper/HopperProvider.js";
|
||||
import { silentLogger } from "../../../src/logger.js";
|
||||
import { parseConfig } from "../config.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import { HopperProvider } from "./HopperProvider.js";
|
||||
|
||||
describe("Hopper provider discovery", () => {
|
||||
it("reports bounded launcher availability without starting Hopper", () => {
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
observeOwnedProcessLineage,
|
||||
verifyNoTokenOwnedProcesses,
|
||||
type ProcessOwnershipHost,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
} from "./ProcessOwnership.js";
|
||||
const ownership = {
|
||||
runId: "run-token",
|
||||
leaderPid: 100,
|
||||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
|||
import {
|
||||
observeOwnedProcessLineage,
|
||||
type ProcessOwnershipHost,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
} from "./ProcessOwnership.js";
|
||||
const ownership = {
|
||||
runId: "run-token",
|
||||
leaderPid: 100,
|
||||
|
|
@ -4,8 +4,8 @@ import {
|
|||
parseProcessEnvironment,
|
||||
selectCapturedProcessGroupIds,
|
||||
type ProcessOwnershipHost,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
import { matchesOwnedProcessCommand } from "../../../src/process/ProcessCommandIdentity.js";
|
||||
} from "./ProcessOwnership.js";
|
||||
import { matchesOwnedProcessCommand } from "./ProcessCommandIdentity.js";
|
||||
const ownership = {
|
||||
runId: "run-token",
|
||||
leaderPid: 100,
|
||||
|
|
@ -3,7 +3,7 @@ import {
|
|||
cleanupOwnedProcessGroup,
|
||||
observeOwnedProcessLineage,
|
||||
type ProcessOwnershipHost,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
} from "./ProcessOwnership.js";
|
||||
const ownership = {
|
||||
runId: "run-token",
|
||||
leaderPid: 100,
|
||||
|
|
@ -3,7 +3,7 @@ import {
|
|||
cleanupOwnedProcessGroup,
|
||||
observeOwnedProcessGroup,
|
||||
type ProcessOwnershipHost,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
} from "./ProcessOwnership.js";
|
||||
const ownership = {
|
||||
runId: "run-token",
|
||||
leaderPid: 100,
|
||||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
cleanupWindowsProcessTree,
|
||||
type WindowsProcessTreeHost,
|
||||
} from "../../../src/process/ProcessOwnership.js";
|
||||
} from "./ProcessOwnership.js";
|
||||
describe("Windows P0 process-tree cleanup", () => {
|
||||
it("reports whether taskkill signaled or found an exited tree", async () => {
|
||||
const terminated: WindowsProcessTreeHost = {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseReplayWorkerResponse } from "../../../../src/replay/ReplayWorkerProtocol.js";
|
||||
import { parseReplayWorkerResponse } from "./ReplayWorkerProtocol.js";
|
||||
|
||||
const digest = "1".repeat(64);
|
||||
const cases = [{ case_id: "case-1", sha256: digest }];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runDerivedOperation } from "../../../src/server/runDerivedOperation.js";
|
||||
import { runDerivedOperation } from "./runDerivedOperation.js";
|
||||
|
||||
const context = (
|
||||
controller: AbortController,
|
||||
|
|
@ -30,7 +30,7 @@ afterEach(async () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("agent lifecycle", () => {
|
||||
describe("client configuration filesystem lifecycle", () => {
|
||||
it("detects every supported client and skips absent clients", async () => {
|
||||
const home = await createTestTempDirectory("rea-detect-");
|
||||
roots.push(home);
|
||||
|
|
@ -170,7 +170,7 @@ describe("agent lifecycle", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("agent lifecycle removal", () => {
|
||||
describe("client configuration filesystem removal", () => {
|
||||
it("uninstalls only owned entries and refuses purge symlinks", async () => {
|
||||
const home = await createTestTempDirectory("rea-uninstall-");
|
||||
roots.push(home);
|
||||
|
|
@ -280,7 +280,7 @@ describe("agent lifecycle removal", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("agent lifecycle failure reporting", () => {
|
||||
describe("client configuration filesystem failure reporting", () => {
|
||||
it.each([
|
||||
[
|
||||
"read",
|
||||
|
|
@ -27,7 +27,7 @@ afterEach(async () => {
|
|||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe("direct analysis snapshot permissions", () => {
|
||||
describe("direct analysis snapshot filesystem permissions", () => {
|
||||
it("forwards the CLI provider selector ahead of the environment preference", async () => {
|
||||
const directory = await createTestTempDirectory("rea-direct-provider-");
|
||||
const targetPath = join(directory, "fixture.hop");
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
|
|
@ -15,29 +14,13 @@ import { MANAGED_STATIC_PROVIDER } from "../../../src/application/InvestigationP
|
|||
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,
|
||||
};
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
managedPeFixtureTarget,
|
||||
MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
} from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
describe("managed runtime correlation planning", () => {
|
||||
it("fails closed while the managed runtime authority is disabled", async () => {
|
||||
|
|
@ -157,7 +140,7 @@ describe("managed runtime correlation planning", () => {
|
|||
await writeFile(artifactPath, bytes);
|
||||
await writeFile(executablePath, "#!/bin/sh\n");
|
||||
const fixture = inspect(bytes, artifactPath, {
|
||||
...memberLimits,
|
||||
...MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
maxMethodInstructions: 1,
|
||||
});
|
||||
const authority = await authorityFor(directory, executablePath, true);
|
||||
|
|
@ -247,18 +230,9 @@ const inputFor = (
|
|||
const inspect = (
|
||||
bytes: Buffer,
|
||||
path: string,
|
||||
limits: typeof memberLimits = memberLimits,
|
||||
limits: typeof MANAGED_MEMBER_FIXTURE_LIMITS = MANAGED_MEMBER_FIXTURE_LIMITS,
|
||||
) => {
|
||||
const target: BinaryTarget = {
|
||||
path,
|
||||
sha256: hash(bytes),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
};
|
||||
const target = managedPeFixtureTarget(bytes, path);
|
||||
const result = inspectManagedMembersBytes(bytes, target, limits);
|
||||
const method = result.methods.items[0];
|
||||
if (method === undefined) throw new Error("fixture has no method");
|
||||
|
|
@ -274,6 +248,3 @@ const inspect = (
|
|||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const hash = (bytes: Buffer): string =>
|
||||
createHash("sha256").update(bytes).digest("hex");
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
type PermissionScope,
|
||||
} from "../../../src/domain/permissionPolicy.js";
|
||||
|
||||
describe("permission authority", () => {
|
||||
describe("permission authority filesystem boundaries", () => {
|
||||
it("distinguishes an elicitable grant from administrator reconfiguration", async () => {
|
||||
const sandbox = await createTestTempDirectory(
|
||||
"rea-permission-remediation-",
|
||||
|
|
@ -21,7 +21,7 @@ import {
|
|||
type FakeCdpBrowser,
|
||||
} from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js";
|
||||
import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js";
|
||||
import { createElectronActiveObservationFixtureResult } from "../../../src/domain/electronActiveObservation.fixture.js";
|
||||
|
||||
const browsers: FakeCdpBrowser[] = [];
|
||||
const resources: Array<{ close(): Promise<unknown> }> = [];
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
ghidraFunctionDossier,
|
||||
ghidraFunctionIdentity,
|
||||
ghidraReferenceEdge,
|
||||
} from "../../fixtures/ghidraFunction.js";
|
||||
} from "../../../src/domain/hopperValues.fixture.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { MANAGED_NATIVE_VERIFICATION_EXAMPLE } from "../../../src/contracts/mana
|
|||
import type { PermissionCeiling } from "../../../src/domain/permissionPolicy.js";
|
||||
import { ManagedStaticProvider } from "../../../src/dotnet/ManagedStaticProvider.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { buildManagedPeFixture } from "../../fixtures/managedPe.js";
|
||||
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
it("opens a managed PE and executes the managed static provider through MCP", async () => {
|
||||
const directory = await createTestTempDirectory("rea-managed-mcp-");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
parseProcessScenario,
|
||||
type ProcessCapture,
|
||||
} from "../../../src/domain/processCapture.js";
|
||||
import { processCaptureIssues } from "../../fixtures/processCapture.js";
|
||||
import { processCaptureIssues } from "../../../src/domain/processCapture.fixture.js";
|
||||
|
||||
type EventSource = "process" | "filesystem" | "http" | "websocket" | "shim";
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
parseProcessScenario,
|
||||
type ProcessCapture,
|
||||
} from "../../../src/domain/processCapture.js";
|
||||
import { processCaptureIssues } from "../../fixtures/processCapture.js";
|
||||
import { processCaptureIssues } from "../../../src/domain/processCapture.fixture.js";
|
||||
|
||||
function createInteractiveScenario(root: string, script: string) {
|
||||
return parseProcessScenario({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { getEventListeners } from "node:events";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { access, mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
|
@ -394,12 +393,6 @@ describe("GhidraClient cleanup and diagnostics", () => {
|
|||
expect(exited(process_)).toBe(true);
|
||||
if (process_ === undefined)
|
||||
throw new Error("Fixture process was not captured");
|
||||
expect(getEventListeners(process_, "exit")).toHaveLength(0);
|
||||
expect(getEventListeners(process_, "close")).toHaveLength(0);
|
||||
expect(getEventListeners(process_, "error")).toHaveLength(0);
|
||||
expect(process_.stdout).not.toBeNull();
|
||||
if (process_.stdout !== null)
|
||||
expect(getEventListeners(process_.stdout, "data")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("retains actionable runtime coordinates after successful cleanup", async () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { getEventListeners } from "node:events";
|
||||
import { access } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
|
|
@ -286,12 +285,6 @@ describe("HopperClient cleanup", () => {
|
|||
if (child === undefined)
|
||||
throw new Error("Fixture process was not captured");
|
||||
expect(child.exitCode !== null || child.signalCode !== null).toBe(true);
|
||||
expect(getEventListeners(child, "exit")).toHaveLength(0);
|
||||
expect(getEventListeners(child, "close")).toHaveLength(0);
|
||||
expect(getEventListeners(child, "error")).toHaveLength(0);
|
||||
expect(child.stderr).not.toBeNull();
|
||||
if (child.stderr === null) throw new Error("Fixture stderr is unavailable");
|
||||
expect(getEventListeners(child.stderr, "data")).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
const stopUnrelatedFixture = async (child: ChildProcess): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js
|
|||
|
||||
import { runProviderAnalysis } from "../../../../src/application/DirectAnalysis.js";
|
||||
import { parseEvidence } from "../../../../src/domain/evidence.js";
|
||||
import { buildManagedPeFixture } from "../../../fixtures/managedPe.js";
|
||||
import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
describe("managed CLI evidence", () => {
|
||||
it("has the same evidence shape through direct CLI analysis plumbing", async () => {
|
||||
describe("managed direct-analysis path boundary", () => {
|
||||
it("returns provider evidence through the shared analysis service", async () => {
|
||||
const directory = await createTestTempDirectory("rea-managed-cli-");
|
||||
const path = join(directory, "fixture.exe");
|
||||
await writeFile(path, buildManagedPeFixture());
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
|
|
@ -7,292 +6,11 @@ import { describe, expect, it } from "vitest";
|
|||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { compareManagedMemberPaths } from "../../../../src/application/ManagedMemberComparisonService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../../src/application/InvestigationProviders.js";
|
||||
import { managedMemberComparisonResultSchema } from "../../../../src/domain/managedMemberComparison.js";
|
||||
import {
|
||||
compareManagedMembers,
|
||||
managedMemberComparisonResultSchema,
|
||||
} from "../../../../src/domain/managedMemberComparison.js";
|
||||
import { createEvidence } from "../../../../src/domain/evidence.js";
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.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,
|
||||
};
|
||||
|
||||
const pathMemberLimits = {
|
||||
...memberLimits,
|
||||
maxFileBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
const comparisonLimits = {
|
||||
max_method_matches: 100,
|
||||
max_field_matches: 100,
|
||||
max_candidates: 10,
|
||||
};
|
||||
|
||||
describe("managed member comparison", () => {
|
||||
it("remaps renamed methods by exact CIL/signature without using names", () => {
|
||||
const leftBytes = buildManagedPeFixture();
|
||||
const rightBytes = buildManagedPeFixture({
|
||||
mvid: Buffer.from("00112233445566778899aabbccddeefe", "hex"),
|
||||
typeName: "A",
|
||||
typeNamespace: "X",
|
||||
methodName: "b",
|
||||
fieldName: "c",
|
||||
});
|
||||
const left = inspect(leftBytes, "/tmp/left.dll");
|
||||
const right = inspect(rightBytes, "/tmp/right.dll");
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidence.evidence_id, result: left.result },
|
||||
{ evidenceId: right.evidence.evidence_id, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.algorithm.name_matching).toBe("not-used");
|
||||
expect(result.left.mvid).toBe("00112233-4455-6677-8899-aabbccddeeff");
|
||||
expect(result.right.mvid).toBe("33221100-5544-7766-8899-aabbccddeefe");
|
||||
expect(result.matching.exact_il_signature).toBe(1);
|
||||
expect(result.methods).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "unchanged",
|
||||
left: expect.objectContaining({ token: "0x06000001", name: "Main" }),
|
||||
right: expect.objectContaining({ token: "0x06000001", name: "b" }),
|
||||
match: expect.objectContaining({
|
||||
status: "matched",
|
||||
basis: "exact-il-signature",
|
||||
confidence: "exact",
|
||||
}),
|
||||
dimensions: [],
|
||||
}),
|
||||
]);
|
||||
expect(result.limitations).toContain(
|
||||
"Names are reported as observations but are not used as a matching basis.",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses structural method shape when build-local token operands drift", () => {
|
||||
const leftBytes = buildManagedPeFixture();
|
||||
const rightBytes = buildManagedPeFixture({
|
||||
ilBody: Buffer.from([
|
||||
0x32, 0x02, 0x7b, 0x02, 0x00, 0x00, 0x04, 0x28, 0x02, 0x00, 0x00, 0x0a,
|
||||
0x2a,
|
||||
]),
|
||||
});
|
||||
const left = inspect(leftBytes, "/tmp/left.dll");
|
||||
const right = inspect(rightBytes, "/tmp/right.dll");
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidence.evidence_id, result: left.result },
|
||||
{ evidenceId: right.evidence.evidence_id, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.matching.exact_il_signature).toBe(0);
|
||||
expect(result.matching.structural_method_shape).toBe(1);
|
||||
expect(result.methods[0]).toMatchObject({
|
||||
status: "changed",
|
||||
match: { status: "matched", basis: "structural-method-shape" },
|
||||
dimensions: ["cil"],
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects member states that disagree with their observed sides or match", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/left.dll");
|
||||
const right = inspect(buildManagedPeFixture(), "/tmp/right.dll");
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidence.evidence_id, result: left.result },
|
||||
{ evidenceId: right.evidence.evidence_id, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
const method = result.methods[0];
|
||||
expect(method).toBeDefined();
|
||||
if (method === undefined) return;
|
||||
|
||||
expect(
|
||||
managedMemberComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
methods: [{ ...method, status: "unchanged", right: null }],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
managedMemberComparisonResultSchema.safeParse({
|
||||
...result,
|
||||
methods: [
|
||||
{
|
||||
...method,
|
||||
match: {
|
||||
...method.match,
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
},
|
||||
},
|
||||
],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not match identical instruction-limited method prefixes", () => {
|
||||
const limited = { ...memberLimits, maxMethodInstructions: 1 };
|
||||
const left = inspect(
|
||||
buildManagedPeFixture(),
|
||||
"/tmp/left-partial.dll",
|
||||
limited,
|
||||
);
|
||||
const right = inspect(
|
||||
buildManagedPeFixture(),
|
||||
"/tmp/right-partial.dll",
|
||||
limited,
|
||||
);
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidence.evidence_id, result: left.result },
|
||||
{ evidenceId: right.evidence.evidence_id, result: right.result },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.matching.exact_il_signature).toBe(0);
|
||||
expect(result.matching.structural_method_shape).toBe(0);
|
||||
expect(result.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
left_status: "partial",
|
||||
right_status: "partial",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("managed member comparison uncertainty", () => {
|
||||
it("keeps unmatched members unknown when the opposite page is incomplete", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/left-paged.dll");
|
||||
const right = inspect(buildManagedPeFixture(), "/tmp/right-paged.dll");
|
||||
const leftPartial = {
|
||||
...left.result,
|
||||
fields: {
|
||||
...left.result.fields,
|
||||
items: [],
|
||||
returned: 0,
|
||||
dropped: 1,
|
||||
complete: false,
|
||||
},
|
||||
};
|
||||
const rightPartial = {
|
||||
...right.result,
|
||||
methods: {
|
||||
...right.result.methods,
|
||||
items: [],
|
||||
returned: 0,
|
||||
dropped: 1,
|
||||
complete: false,
|
||||
},
|
||||
};
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidence.evidence_id, result: leftPartial },
|
||||
{ evidenceId: right.evidence.evidence_id, result: rightPartial },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.summary).toMatchObject({
|
||||
added: 0,
|
||||
removed: 0,
|
||||
unknown: 2,
|
||||
});
|
||||
expect(result.methods[0]).toMatchObject({
|
||||
status: "unknown",
|
||||
left: { token: "0x06000001" },
|
||||
right: null,
|
||||
limitations: [expect.stringContaining("unknown-within-unobserved-page")],
|
||||
});
|
||||
expect(result.fields[0]).toMatchObject({
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: { token: "0x04000001" },
|
||||
limitations: [expect.stringContaining("unknown-within-unobserved-page")],
|
||||
});
|
||||
expect(result.coverage).toEqual({
|
||||
status: "truncated",
|
||||
left_status: "partial",
|
||||
right_status: "partial",
|
||||
omitted_methods: 1,
|
||||
omitted_fields: 1,
|
||||
omitted_candidates: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not guess ambiguous field signature matches", () => {
|
||||
const left = inspect(buildManagedPeFixture(), "/tmp/left.dll");
|
||||
const leftField = left.result.fields.items[0];
|
||||
expect(leftField).toBeDefined();
|
||||
if (leftField === undefined) return;
|
||||
const duplicatedLeft = {
|
||||
...left.result,
|
||||
fields: {
|
||||
...left.result.fields,
|
||||
items: [
|
||||
leftField,
|
||||
{
|
||||
...leftField,
|
||||
token: "0x04000002",
|
||||
name: "other",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
returned: 2,
|
||||
},
|
||||
};
|
||||
const right = inspect(buildManagedPeFixture(), "/tmp/right.dll");
|
||||
const rightField = right.result.fields.items[0];
|
||||
expect(rightField).toBeDefined();
|
||||
if (rightField === undefined) return;
|
||||
const duplicatedRight = {
|
||||
...right.result,
|
||||
fields: {
|
||||
...right.result.fields,
|
||||
items: [
|
||||
rightField,
|
||||
{
|
||||
...rightField,
|
||||
token: "0x04000002",
|
||||
name: "renamed",
|
||||
},
|
||||
],
|
||||
total: 2,
|
||||
returned: 2,
|
||||
},
|
||||
};
|
||||
const result = compareManagedMembers(
|
||||
{ evidenceId: left.evidence.evidence_id, result: duplicatedLeft },
|
||||
{ evidenceId: right.evidence.evidence_id, result: duplicatedRight },
|
||||
comparisonLimits,
|
||||
);
|
||||
|
||||
expect(result.matching.ambiguous).toBe(1);
|
||||
expect(result.fields).toEqual([
|
||||
expect.objectContaining({
|
||||
status: "unknown",
|
||||
match: expect.objectContaining({
|
||||
status: "ambiguous",
|
||||
basis: "field-signature",
|
||||
candidate_left_tokens: ["0x04000001", "0x04000002"],
|
||||
candidate_right_tokens: ["0x04000001", "0x04000002"],
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
buildManagedPeFixture,
|
||||
MANAGED_MEMBER_PATH_FIXTURE_LIMITS,
|
||||
} from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
describe("managed member comparison path workflow", () => {
|
||||
it("compares two local paths and returns derived Evidence", async () => {
|
||||
|
|
@ -308,49 +26,24 @@ describe("managed member comparison path workflow", () => {
|
|||
const result = await compareManagedMemberPaths({
|
||||
leftPath,
|
||||
rightPath,
|
||||
memberLimits: pathMemberLimits,
|
||||
comparisonLimits,
|
||||
memberLimits: MANAGED_MEMBER_PATH_FIXTURE_LIMITS,
|
||||
comparisonLimits: {
|
||||
max_method_matches: 100,
|
||||
max_field_matches: 100,
|
||||
max_candidates: 10,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
expect(result.value.operation).toBe("compare_managed_members");
|
||||
expect(result.value.confidence).toBe("inferred");
|
||||
expect(result.value.authority).toBe("analyst-inference");
|
||||
expect(result.value).toMatchObject({
|
||||
operation: "compare_managed_members",
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
});
|
||||
expect(
|
||||
managedMemberComparisonResultSchema.parse(result.value.normalized_result)
|
||||
.matching.exact_il_signature,
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
const inspect = (
|
||||
bytes: Buffer,
|
||||
path: string,
|
||||
limits: typeof memberLimits = memberLimits,
|
||||
) => {
|
||||
const target: BinaryTarget = {
|
||||
path,
|
||||
sha256: hash(bytes),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
};
|
||||
const result = inspectManagedMembersBytes(bytes, target, limits);
|
||||
return {
|
||||
result,
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
|
|
@ -7,79 +6,16 @@ import { describe, expect, it } from "vitest";
|
|||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import type { AnalysisExecution } from "../../../../src/application/AnalysisProvider.js";
|
||||
import type { BinaryTarget } from "../../../../src/domain/binaryTarget.js";
|
||||
import { parseBinaryTarget } from "../../../../src/domain/binaryTarget.js";
|
||||
import {
|
||||
managedArtifactInspectionSchema,
|
||||
managedMemberInspectionSchema,
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
} from "../../../../src/domain/managedArtifact.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../../src/dotnet/ManagedArtifactInspector.js";
|
||||
import { ManagedStaticProvider } from "../../../../src/dotnet/ManagedStaticProvider.js";
|
||||
import {
|
||||
buildManagedPeFixture,
|
||||
buildNativePeFixture,
|
||||
} from "../../../fixtures/managedPe.js";
|
||||
|
||||
const limits = {
|
||||
referenceOffset: 0,
|
||||
referenceLimit: 100,
|
||||
resourceOffset: 0,
|
||||
resourceLimit: 100,
|
||||
attributeOffset: 0,
|
||||
attributeLimit: 100,
|
||||
maxMetadataBytes: 1024 * 1024,
|
||||
maxTableRows: 1_000,
|
||||
maxHeapItemBytes: 1024 * 1024,
|
||||
};
|
||||
|
||||
describe("managed artifact failures", () => {
|
||||
it("reports native PE, malformed metadata, and bounded metadata failures as typed results", () => {
|
||||
const native = inspectManagedArtifactBytes(
|
||||
buildNativePeFixture(),
|
||||
target(buildNativePeFixture()),
|
||||
limits,
|
||||
);
|
||||
expect(native.classification.status).toBe("not-managed");
|
||||
expect(native.metadata.status).toBe("absent");
|
||||
expect(native.coverage.state).toBe("unavailable");
|
||||
|
||||
const malformedBytes = buildManagedPeFixture({
|
||||
corruptMetadataSignature: true,
|
||||
});
|
||||
const malformed = inspectManagedArtifactBytes(
|
||||
malformedBytes,
|
||||
target(malformedBytes),
|
||||
limits,
|
||||
);
|
||||
expect(malformed.classification.status).toBe("malformed");
|
||||
expect(malformed.coverage.issues).toEqual([
|
||||
expect.objectContaining({ code: "invalid-metadata-root" }),
|
||||
]);
|
||||
|
||||
const unsupportedTableBytes = buildManagedPeFixture({
|
||||
metadataValidMaskExtra: 1n << 50n,
|
||||
});
|
||||
const unsupportedTable = inspectManagedArtifactBytes(
|
||||
unsupportedTableBytes,
|
||||
target(unsupportedTableBytes),
|
||||
limits,
|
||||
);
|
||||
expect(unsupportedTable.coverage.issues).toEqual([
|
||||
expect.objectContaining({ code: "invalid-tables" }),
|
||||
]);
|
||||
|
||||
const limited = inspectManagedArtifactBytes(
|
||||
buildManagedPeFixture(),
|
||||
target(buildManagedPeFixture()),
|
||||
{ ...limits, maxMetadataBytes: 256 },
|
||||
);
|
||||
expect(limited.metadata.status).toBe("partial");
|
||||
expect(limited.coverage.issues).toEqual([
|
||||
expect.objectContaining({ code: "limit-exceeded" }),
|
||||
]);
|
||||
});
|
||||
import { buildManagedPeFixture } from "../../../../src/dotnet/ManagedPe.fixture.js";
|
||||
|
||||
describe("managed static provider path boundary", () => {
|
||||
it("executes as a read-only auxiliary provider with digest, cancellation, and format boundaries", async () => {
|
||||
const directory = await createTestTempDirectory("rea-managed-provider-");
|
||||
const bytes = buildManagedPeFixture();
|
||||
|
|
@ -161,17 +97,6 @@ describe("managed artifact failures", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
const target = (bytes: Buffer): BinaryTarget => ({
|
||||
path: "/fixture.exe",
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
kind: "executable",
|
||||
format: "pe",
|
||||
architecture: "x86",
|
||||
availableArchitectures: ["x86"],
|
||||
executableRole: "application",
|
||||
managed: true,
|
||||
});
|
||||
|
||||
const asManagedResult = (execution: AnalysisExecution) =>
|
||||
managedArtifactInspectionSchema.parse(execution.result);
|
||||
|
||||
|
|
@ -1,96 +1,17 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import type {
|
||||
AnalysisProvider,
|
||||
CapabilityDescriptor,
|
||||
} from "../../../src/application/AnalysisProvider.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
|
||||
import {
|
||||
createBinarySessionTargets,
|
||||
createCacheProvider,
|
||||
createTestBinarySession,
|
||||
} from "../../fixtures/binarySession.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { createInvestigationWorkspace } from "../../../src/domain/investigationWorkspace.js";
|
||||
import { ok as resultOk } from "../../../src/domain/result.js";
|
||||
import { observed as ok } from "../../fixtures/analysisExecution.js";
|
||||
|
||||
const cacheProvider = (
|
||||
calls: string[],
|
||||
mayWriteFilesystem = false,
|
||||
): AnalysisProvider => {
|
||||
const identity = {
|
||||
id: "fixture",
|
||||
name: "Fixture analysis provider",
|
||||
version: "1",
|
||||
} as const;
|
||||
return {
|
||||
identity: () => identity,
|
||||
resolveAnalysisProfile: () =>
|
||||
Promise.resolve(
|
||||
resultOk({
|
||||
profile: createAnalysisProfile(identity, 1, { fixture: true }),
|
||||
compatibility: {},
|
||||
}),
|
||||
),
|
||||
capabilities: () => [
|
||||
cacheCapability(identity, "address_name", false, mayWriteFilesystem),
|
||||
cacheCapability(identity, "set_address_name", true),
|
||||
],
|
||||
createClient: () => ({
|
||||
execute: (operation) => {
|
||||
calls.push(operation);
|
||||
return Promise.resolve(ok(operation));
|
||||
},
|
||||
close: () => Promise.resolve(),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const cacheCapability = (
|
||||
provider: CapabilityDescriptor["provider"],
|
||||
operation: "address_name" | "set_address_name",
|
||||
mutatesArtifact: boolean,
|
||||
mayWriteFilesystem = false,
|
||||
): CapabilityDescriptor => ({
|
||||
provider,
|
||||
operation,
|
||||
inputContractVersion: 1,
|
||||
outputContractVersion: 1,
|
||||
available: true,
|
||||
reason: null,
|
||||
pagination: "none",
|
||||
exhaustive: true,
|
||||
effects: {
|
||||
mutatesArtifact,
|
||||
launchesProcess: false,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem,
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
},
|
||||
limits: {
|
||||
maxResults: null,
|
||||
maxPayloadBytes: null,
|
||||
timeoutMs: null,
|
||||
},
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
const targets = async (): Promise<readonly [string, string]> => {
|
||||
const directory = await createTestTempDirectory("bb-session-");
|
||||
const first = join(directory, "first.hop");
|
||||
const second = join(directory, "second.hop");
|
||||
await writeFile(first, "one");
|
||||
await writeFile(second, "two");
|
||||
return [first, second];
|
||||
};
|
||||
|
||||
describe("binary session", () => {
|
||||
it("returns detached provider, target, and workspace metadata", async () => {
|
||||
const [first] = await targets();
|
||||
const session = createTestBinarySession(cacheProvider([]));
|
||||
const [first] = await createBinarySessionTargets();
|
||||
const session = createTestBinarySession(createCacheProvider([]));
|
||||
expect((await session.open(first)).ok).toBe(true);
|
||||
expect(session.status()).toMatchObject({
|
||||
analysis_run: {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue