fix(inputs): centralize admission and preserve caller selections

Move shared request parsing into its application owner, migrate all consumers, and remove six duplicated admission blocks while preserving cancellation and validation policy.

Keep operator-selected JavaScript paths in verifier parent Evidence while retaining canonical comparison guards. Validated with 204 passing tests, 3 Linux-x64-only skips, typecheck, lint, and test discovery.
This commit is contained in:
morluto committed 2026-10-26 17:35:18 +08:00
1 parent 7d36d172ab
commit ccdeda7a27
47 files changed
+381 -333

No files matched your search

@@ -42,8 +42,8 @@ const [leftPath, rightPath] = await Promise.all([
if (leftPath === rightPath)
throw new Error("--left and --right must resolve to distinct paths");
const [left, right] = await Promise.all([
analyze(leftPath),
analyze(rightPath),
analyze(options.left),
analyze(options.right),
]);
const comparison = compareApplicationVersionsEvidence({ left, right });
if (!comparison.ok) throw comparison.error;
@@ -36,14 +36,8 @@ const requestedLayers = [
...options.cache.map((path) => ({ role: "cache", path })),
...options.assets.map((path) => ({ role: "assets", path })),
];
const canonicalLayers = await Promise.all(
requestedLayers.map(async (layer) => ({
...layer,
path: await realpath(layer.path),
})),
);
const staticLayers = [];
for (const layer of canonicalLayers) {
for (const layer of requestedLayers) {
const analyzed = await analyzeJavaScriptApplicationWithOwnedScope({
input_path: layer.path,
format: "auto",
+68
View File
@@ -0,0 +1,68 @@
import { expect, it, vi } from "vitest";
import { z } from "zod";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import { parseAnalysisInput } from "./AnalysisInput.js";
it("admits direct inputs with schema defaults while preserving caller values", () => {
const raw = {
path: "/tmp/selected artifact",
nested: { input: "caller value" },
};
const result = parseAnalysisInput(
"inspect",
raw,
z.strictObject({
path: z.string(),
nested: z.strictObject({ input: z.string() }),
detail: z.literal("full").default("full"),
}),
);
if (!result.ok) throw result.error;
expect(result.value).toEqual({ ...raw, detail: "full" });
expect(raw).not.toHaveProperty("detail");
});
it("preserves direct-input issue paths and the original validation cause", () => {
const result = parseAnalysisInput(
"inspect",
{ input: "", extra: "caller-only-marker" },
z.strictObject({ input: z.string().min(1) }),
);
if (result.ok || !(result.error instanceof AnalysisInputError))
throw new Error("Expected input error");
expect(result.error.operation).toBe("inspect");
expect(result.error.issues).toEqual([
{ path: ["input"], reason: "out_of_range", minimum: 1 },
{ path: ["extra"], reason: "unknown_argument" },
]);
expect(result.error.cause).toBeInstanceOf(z.ZodError);
expect(JSON.stringify(result.error.issues)).not.toContain(
"caller-only-marker",
);
});
it("honors direct-input admission cancellation before reading the caller input", () => {
const controller = new AbortController();
const reason = { request: "cancel before validation" };
controller.abort(reason);
const read = vi.fn(() => "caller input");
const raw = {
get path() {
return read();
},
};
const result = parseAnalysisInput(
"inspect",
raw,
z.strictObject({ path: z.string() }),
controller.signal,
);
if (result.ok || !(result.error instanceof AnalysisCancelledError))
throw new Error("Expected cancellation");
expect(result.error.operation).toBe("inspect");
expect(result.error.cause).toBe(reason);
expect(read).not.toHaveBeenCalled();
});
+24
View File
@@ -0,0 +1,24 @@
import type { z } from "zod";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
/** Validate an application request, honoring cancellation before admission when supplied. */
export const parseAnalysisInput = <T>(
operation: string,
rawInput: unknown,
schema: z.ZodType<T>,
signal?: AbortSignal,
): Result<T, AnalysisError> => {
if (signal?.aborted)
return err(new AnalysisCancelledError(operation, { cause: signal.reason }));
const input = schema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(operation, input.error.issues, rawInput, {
cause: input.error,
}),
);
return ok(input.data);
};
+9 -20
View File
@@ -1,3 +1,4 @@
import { parseAnalysisInput } from "./AnalysisInput.js";
import type {
AnalysisOperationPort,
ExecutionOptions,
@@ -7,9 +8,6 @@ import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { createEvidence } from "../domain/evidence.js";
import { parseFunctionDossier } from "../domain/hopperValues.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
/**
@@ -23,23 +21,14 @@ export const executeFunctionAnalysisEvidence = async (
target: BinaryTarget | undefined,
options?: ExecutionOptions,
) => {
if (options?.signal?.aborted)
return err(
new AnalysisCancelledError("analyze_function", {
cause: options.signal.reason,
}),
);
const input = nativeProcedureInputSchema.safeParse(parameters);
if (!input.success)
return err(
analysisInputErrorFromIssues(
"analyze_function",
input.error.issues,
parameters,
{ cause: input.error },
),
);
const arguments_ = input.data;
const input = parseAnalysisInput(
"analyze_function",
parameters,
nativeProcedureInputSchema,
options?.signal,
);
if (!input.ok) return input;
const arguments_ = input.value;
const execution = await analysis.execute(
"analyze_function",
arguments_,
@@ -12,7 +12,6 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import {
executeProviderOperation,
keepIncompleteObservation,
parseProviderOperationInput,
} from "./ProviderOperationExecution.js";
it("projects request-schema issues without exposing the request envelope", async () => {
@@ -179,64 +178,3 @@ it("freezes a retained partial so a later mutation cannot break projection", ()
details: { partial_observation: { steps: [] } },
});
});
it("admits direct inputs with schema defaults while preserving caller values", () => {
const raw = {
path: "/tmp/selected artifact",
nested: { input: "caller value" },
};
const result = parseProviderOperationInput(
"inspect",
raw,
z.strictObject({
path: z.string(),
nested: z.strictObject({ input: z.string() }),
detail: z.literal("full").default("full"),
}),
);
if (!result.ok) throw result.error;
expect(result.value).toEqual({ ...raw, detail: "full" });
expect(raw).not.toHaveProperty("detail");
});
it("preserves direct-input issue paths and the original validation cause", () => {
const result = parseProviderOperationInput(
"inspect",
{ input: "", extra: "caller-only-marker" },
z.strictObject({ input: z.string().min(1) }),
);
if (result.ok || !(result.error instanceof AnalysisInputError))
throw new Error("Expected input error");
expect(result.error.operation).toBe("inspect");
expect(result.error.issues).toEqual([
{ path: ["input"], reason: "out_of_range", minimum: 1 },
{ path: ["extra"], reason: "unknown_argument" },
]);
expect(result.error.cause).toBeInstanceOf(z.ZodError);
expect(JSON.stringify(result.error.issues)).not.toContain(
"caller-only-marker",
);
});
it("honors direct-input admission cancellation before reading the caller input", () => {
const controller = new AbortController();
const reason = { request: "cancel before validation" };
controller.abort(reason);
const read = vi.fn(() => "caller input");
const raw = {
get path() {
return read();
},
};
const result = parseProviderOperationInput(
"inspect",
raw,
z.strictObject({ path: z.string() }),
controller.signal,
);
if (result.ok || !(result.error instanceof AnalysisCancelledError))
throw new Error("Expected cancellation");
expect(result.error.operation).toBe("inspect");
expect(result.error.cause).toBe(reason);
expect(read).not.toHaveBeenCalled();
});
+1 -23
View File
@@ -14,10 +14,7 @@ import {
isImmutableJsonSnapshot,
} from "../domain/immutableJson.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import {
analysisInputErrorFromIssues,
analysisOperationInputErrorFromIssues,
} from "../domain/inputIssueProjection.js";
import { analysisOperationInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
import type {
AnalysisExecution,
@@ -81,25 +78,6 @@ export const executeProviderOperation = async <
return completeProviderEvidence(evidence, options?.signal);
};
/** Admit a direct provider input before acquiring any provider resources. */
export const parseProviderOperationInput = <T>(
operation: string,
rawInput: unknown,
schema: z.ZodType<T>,
signal?: AbortSignal,
): Result<T, AnalysisError> => {
if (signal?.aborted)
return err(new AnalysisCancelledError(operation, { cause: signal.reason }));
const input = schema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(operation, input.error.issues, rawInput, {
cause: input.error,
}),
);
return ok(input.data);
};
/** Classify completion after Evidence assembly, preserving facts on late abort. */
export const completeProviderEvidence = (
evidence: Evidence,
@@ -1,3 +1,4 @@
import { parseAnalysisInput } from "./AnalysisInput.js";
import { z } from "zod";
import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstructionCoverageInput.js";
@@ -7,8 +8,7 @@ import {
readReconstructionCoverageData,
type ReconstructionClosureResult,
} from "../domain/reconstructionCoverage.js";
import { err, type Result } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { Result } from "../domain/result.js";
const OPERATION = "evaluate_reconstruction_coverage";
const requestEnvelopeSchema =
@@ -21,20 +21,15 @@ export const evaluateReconstructionCoverage = (
rawInput: unknown,
nowEpochMs = Date.now(),
): Result<ReconstructionClosureResult, AnalysisError> => {
const parsed = requestEnvelopeSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput, {
cause: parsed.error,
}),
);
const coverage = readReconstructionCoverageData(parsed.data.coverage, [
const parsed = parseAnalysisInput(OPERATION, rawInput, requestEnvelopeSchema);
if (!parsed.ok) return parsed;
const coverage = readReconstructionCoverageData(parsed.value.coverage, [
"coverage",
]);
if (!coverage.ok) return coverage;
return evaluateParsedReconstructionClosure(
coverage.value,
parsed.data.boundary_id,
parsed.value.boundary_id,
nowEpochMs,
);
};
@@ -1,3 +1,4 @@
import { parseAnalysisInput } from "./AnalysisInput.js";
import { z } from "zod";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
@@ -13,7 +14,6 @@ import {
type ReconstructionObligationManifest,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import { err, ok, type Result } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
const OPERATION = "build_reconstruction_obligation_ledger";
const requestSchema = reconstructionObligationLedgerInputSchema.extend({
@@ -36,19 +36,17 @@ export interface ResolvedReconstructionObligationRequest {
export const resolveReconstructionObligationLedgerRequest = (
input: unknown,
): Result<ResolvedReconstructionObligationRequest, AnalysisError> => {
const frame = requestSchema.safeParse(input);
if (!frame.success)
return err(
analysisInputErrorFromIssues(OPERATION, frame.error.issues, input, {
cause: frame.error,
}),
);
const bundle = readEvidenceBundle(frame.data.evidence_bundle);
const frame = parseAnalysisInput(OPERATION, input, requestSchema);
if (!frame.ok) return frame;
const bundle = readEvidenceBundle(frame.value.evidence_bundle);
if (!bundle.ok) return bundle;
const records = new Map(
bundle.value.records.map((record) => [record.evidence_id, record]),
);
for (const [index, obligation] of frame.data.reviewed_obligations.entries()) {
for (const [
index,
obligation,
] of frame.value.reviewed_obligations.entries()) {
for (const [
referenceIndex,
id,
@@ -73,7 +71,7 @@ export const resolveReconstructionObligationLedgerRequest = (
}
return ok({
sources: { bundle: bundle.value, records },
reviewedObligations: frame.data.reviewed_obligations,
manifest: frame.data.manifest,
reviewedObligations: frame.value.reviewed_obligations,
manifest: frame.value.manifest,
});
};
+3 -5
View File
@@ -36,10 +36,8 @@ import type {
WebModuleResolutionPort,
} from "./WebModulePorts.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "./ProviderOperationExecution.js";
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
import { parseAnalysisInput } from "./AnalysisInput.js";
const OPERATION = "trace_web_module_imports";
const artifactsSchema = selectedWebScriptArtifactsSchema.extend({
@@ -83,7 +81,7 @@ export class WebModuleTraceService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
OPERATION,
rawInput,
webModuleTraceInputSchema,
+3 -3
View File
@@ -18,8 +18,8 @@ import {
import {
completeProviderEvidence,
keepIncompleteObservation,
parseProviderOperationInput,
} from "./ProviderOperationExecution.js";
import { parseAnalysisInput } from "./AnalysisInput.js";
/** Shared public workflows keep instrumentation and listener inspection independently selectable. */
export class WebRuntimeService {
@@ -31,7 +31,7 @@ export class WebRuntimeService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "observe_web_execution";
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
operation,
rawInput,
observeWebExecutionInputSchema,
@@ -64,7 +64,7 @@ export class WebRuntimeService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_web_event_listeners";
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
operation,
rawInput,
inspectWebEventListenersInputSchema,
+3 -5
View File
@@ -32,10 +32,8 @@ import type {
WebSourceLocationArtifactPort,
WebSourceMapPort,
} from "./WebSourceLocationPorts.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "./ProviderOperationExecution.js";
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
import { parseAnalysisInput } from "./AnalysisInput.js";
const OPERATION = "trace_web_source_location";
const artifactsSchema = selectedWebScriptArtifactsSchema.extend({
@@ -90,7 +88,7 @@ export class WebSourceLocationService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
OPERATION,
rawInput,
webSourceLocationInputSchema,
@@ -1,3 +1,4 @@
import { parseAnalysisInput } from "../AnalysisInput.js";
import { isNativeObservationOperation } from "../../domain/native/nativeObservation.js";
import { setImmediate } from "node:timers/promises";
import { dispatchQueuedInterrupts } from "../../process/queuedInterrupts.js";
@@ -60,22 +61,18 @@ export const inspectAnalysisView = async (
lookup?: EvidenceLookup,
options: AnalysisViewReadOptions = {},
): Promise<Result<Evidence, AnalysisError>> => {
const parsed = inspectAnalysisViewInputSchema.safeParse(rawInput);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(
INSPECT_ANALYSIS_VIEW_OPERATION,
parsed.error.issues,
rawInput,
{ cause: parsed.error },
),
);
const parsed = parseAnalysisInput(
INSPECT_ANALYSIS_VIEW_OPERATION,
rawInput,
inspectAnalysisViewInputSchema,
);
if (!parsed.ok) return parsed;
if (options.evidenceBundlePath !== undefined)
return inspectBundleView(parsed.data, options.evidenceBundlePath, options);
const source = parsed.data.source;
return inspectBundleView(parsed.value, options.evidenceBundlePath, options);
const source = parsed.value.source;
if (source.kind === "evidence-file")
return inspectFileView({ ...parsed.data, source }, lookup, options);
return inspectAnalysisViewValidated({ ...parsed.data, source }, lookup);
return inspectFileView({ ...parsed.value, source }, lookup, options);
return inspectAnalysisViewValidated({ ...parsed.value, source }, lookup);
};
/**
@@ -9,7 +9,7 @@ import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { androidPackageInspectionInputSchema } from "../../domain/android/androidAnalysis.js";
import type { Evidence } from "../../domain/evidence.js";
import type { Result } from "../../domain/result.js";
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
/** Preserve the complete provider request; select retained delivery explicitly. */
export const inspectAndroidPackage = async (
@@ -19,7 +19,7 @@ export const inspectAndroidPackage = async (
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> => {
const operation = "inspect_android_package";
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
operation,
rawInput,
androidPackageInspectionInputSchema,
@@ -19,10 +19,8 @@ import {
} from "../../domain/apkSignature/apkSignatureReport.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_apk_signature";
@@ -43,7 +41,7 @@ export class ApkSignatureService {
recordEvidence: undefined,
},
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectApkSignatureInputSchema,
@@ -1,10 +1,8 @@
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { DesktopApplicationPort } from "./DesktopApplicationPort.js";
import { ARTIFACT_GRAPH_PROVIDER } from "../../domain/investigationProviders.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import type { Evidence } from "../../domain/evidence.js";
import { createDesktopApplicationEvidence } from "../../domain/desktopApplicationEvidence.js";
@@ -27,7 +25,7 @@ export class DesktopApplicationService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_desktop_application";
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
operation,
rawInput,
inspectDesktopApplicationInputSchema,
@@ -14,10 +14,8 @@ import {
type AppleIps,
} from "../../domain/native/appleIps.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_apple_ips";
@@ -33,7 +31,7 @@ export class AppleIpsService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectAppleIpsInputSchema,
@@ -8,10 +8,8 @@ import {
binaryLayoutSchema,
} from "../../domain/native/binaryLayout.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_binary_layout";
const EVIDENCE_ARCHITECTURES = {
@@ -56,7 +54,7 @@ export class BinaryLayoutService {
input: Parameters<BinaryLayoutPort["inspect"]>[0],
) => ReturnType<BinaryLayoutPort["inspect"]>,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectBinaryLayoutInputSchema,
@@ -36,10 +36,8 @@ import {
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { sameRegularFileState } from "../../filesystem/RegularFile.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
/** Bound retained result rows before constructing the Evidence representation. */
export const MAX_BYTE_ENTROPY_BLOCKS = 32_768;
@@ -99,7 +97,7 @@ export class ByteEntropyService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
OPERATION,
rawInput,
inspectByteEntropyInputSchema,
@@ -13,10 +13,8 @@ import {
type Devicetree,
} from "../../domain/firmware/devicetree.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_devicetree";
@@ -32,7 +30,7 @@ export class DevicetreeService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectDevicetreeInputSchema,
@@ -15,10 +15,8 @@ import {
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
import { elfArchitecture } from "../../domain/elfFixedHeader.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_elf_notes";
@@ -34,7 +32,7 @@ export class ElfNotesService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectElfNotesInputSchema,
@@ -21,10 +21,8 @@ import {
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
import { elfArchitecture } from "../../domain/elfFixedHeader.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_kernel_module";
@@ -42,7 +40,7 @@ export class KernelModuleService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectKernelModuleInputSchema,
@@ -8,10 +8,8 @@ import {
nativeExportsSchema,
} from "../../domain/native/nativeExports.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_native_exports";
@@ -28,7 +26,7 @@ export class NativeExportsService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectNativeExportsInputSchema,
@@ -14,10 +14,8 @@ import {
} from "../../domain/native/peCertificates.js";
import { peSubjectArchitecture } from "../../domain/peMachine.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_pe_certificates";
@@ -33,7 +31,7 @@ export class PeCertificatesService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectPeCertificatesInputSchema,
@@ -14,10 +14,8 @@ import {
} from "../../domain/native/peImports.js";
import { peSubjectArchitecture } from "../../domain/peMachine.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_pe_imports";
@@ -33,7 +31,7 @@ export class PeImportsService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectPeImportsInputSchema,
@@ -14,10 +14,8 @@ import {
} from "../../domain/native/peLoadConfig.js";
import { peSubjectArchitecture } from "../../domain/peMachine.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_pe_load_config";
@@ -33,7 +31,7 @@ export class PeLoadConfigService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectPeLoadConfigInputSchema,
@@ -11,10 +11,8 @@ import {
} from "../../domain/native/peResources.js";
import { peSubjectArchitecture } from "../../domain/peMachine.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
export class PeResourcesService {
@@ -29,7 +27,7 @@ export class PeResourcesService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_pe_resources";
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
operation,
rawInput,
inspectPeResourcesInputSchema,
@@ -9,10 +9,8 @@ import {
recordedCrashSchema,
} from "../../domain/native/recordedCrash.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_recorded_crash";
@@ -51,7 +49,7 @@ export class RecordedCrashService {
input: Parameters<RecordedCrashPort["inspect"]>[0],
) => ReturnType<RecordedCrashPort["inspect"]>,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectRecordedCrashInputSchema,
@@ -17,10 +17,8 @@ import {
type InspectPacketCaptureInput,
} from "../../domain/captures/packetCapture.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
export interface PacketCapturePort {
/** Release per-call resources before returning, or retain failed cleanup for close/retry. */
@@ -46,7 +44,7 @@ export class PacketCaptureService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_packet_capture";
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
operation,
raw,
inspectPacketCaptureInputSchema,
@@ -85,7 +83,7 @@ export class PacketCaptureService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_packet_fields";
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
operation,
raw,
inspectPacketFieldsInputSchema,
@@ -9,10 +9,8 @@ import {
import type { Evidence } from "../../domain/evidence.js";
import { createElectronFuseEvidence } from "../../domain/electron/electronFuseEvidence.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_electron_fuses";
@@ -30,7 +28,7 @@ export class ElectronFuseService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectElectronFusesInputSchema,
+3 -5
View File
@@ -9,10 +9,8 @@ import {
type ErlangBeam,
} from "../../domain/erlang/beam.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_erlang_beam";
@@ -28,7 +26,7 @@ export class BeamService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectErlangBeamInputSchema,
+3 -5
View File
@@ -10,10 +10,8 @@ import {
evmInterfaceSchema,
} from "../../domain/evm/evmInterface.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_evm_interface";
const diagnosticsSchema = z.object({
@@ -33,7 +31,7 @@ export class EvmInterfaceService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectEvmInterfaceInputSchema,
+2 -2
View File
@@ -9,7 +9,7 @@ import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { dartAotInspectionInputSchema } from "../../domain/flutter/flutterBuildAnalysis.js";
import type { Evidence } from "../../domain/evidence.js";
import type { Result } from "../../domain/result.js";
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
/** Keep the caller's ABI choice and complete analysis; select only its delivery. */
export const inspectDartAot = async (
@@ -19,7 +19,7 @@ export const inspectDartAot = async (
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> => {
const operation = "inspect_dart_aot";
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
operation,
rawInput,
dartAotInspectionInputSchema,
@@ -16,7 +16,7 @@ import {
} from "../../domain/analysisErrorCore.js";
import { err, ok, type Result } from "../../domain/result.js";
import { fridaSessionStatusInputSchema } from "../../domain/fridaSessionStatus.js";
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
@@ -252,7 +252,7 @@ export class FridaInstrumentationService {
evidenceById: undefined,
},
): Result<Evidence, AnalysisError> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
"frida_session_status",
rawInput,
fridaSessionStatusInputSchema,
+3 -5
View File
@@ -10,10 +10,8 @@ import {
inspectGoBinaryInputSchema,
} from "../../domain/go/goBinary.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import {
requireAnalysisViewRetention,
summarizeRetainedAnalysis,
@@ -40,7 +38,7 @@ export class GoBinaryService {
options?: ExecutionOptions,
retention: AnalysisViewRetention = NO_RETENTION,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectGoBinaryInputSchema,
@@ -1,4 +1,4 @@
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import type {
@@ -99,7 +99,7 @@ export class Il2cppMetadataService {
retention: AnalysisViewRetention,
output?: Il2cppSeedOutput,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectIl2cppMetadataInputSchema,
@@ -3,7 +3,7 @@ import {
type EvidenceLookup,
} from "../EvidenceInputResolver.js";
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import type { ResolvedSourceToBundleReference } from "./SourceToBundleReference.js";
import { z } from "zod";
@@ -122,7 +122,7 @@ export const traceApplicationFeatureEvidence = (
AnalysisError
> => {
const operation = "trace_application_feature";
const request = parseProviderOperationInput(
const request = parseAnalysisInput(
operation,
rawInput,
featureTraceFrameSchema,
@@ -187,7 +187,7 @@ export const compareApplicationVersionsEvidence = (
AnalysisError
> => {
const operation = "compare_application_versions";
const request = parseProviderOperationInput(
const request = parseAnalysisInput(
operation,
rawInput,
versionComparisonFrameSchema,
@@ -305,7 +305,7 @@ export const compareJavaScriptExportShapesEvidence = (
AnalysisError
> => {
const operation = "compare_javascript_export_shapes";
const request = parseProviderOperationInput(
const request = parseAnalysisInput(
operation,
rawInput,
exportShapeFrameSchema,
@@ -1,7 +1,7 @@
import { parseAnalysisInput } from "../AnalysisInput.js";
import { isAbsolute, resolve } from "node:path";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
@@ -11,7 +11,6 @@ import {
javascriptRecoveryInputSchema,
javascriptRecoveryResultSchema,
} from "../../domain/javascript/javascriptRecovery.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, type Result } from "../../domain/result.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
@@ -32,21 +31,17 @@ export class JavaScriptRecoveryService {
input: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted === true)
return err(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const parsed = javascriptRecoveryInputSchema.safeParse(input);
if (!parsed.success)
return err(
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, input, {
cause: parsed.error,
}),
);
const invalidPaths = (["path", "output_directory"] as const).filter(
(field) => !isAbsolute(parsed.data[field]),
const parsed = parseAnalysisInput(
OPERATION,
input,
javascriptRecoveryInputSchema,
options?.signal,
);
const invalidAdditional = (parsed.data.additional_paths ?? []).filter(
if (!parsed.ok) return parsed;
const invalidPaths = (["path", "output_directory"] as const).filter(
(field) => !isAbsolute(parsed.value[field]),
);
const invalidAdditional = (parsed.value.additional_paths ?? []).filter(
(additional) => !isAbsolute(additional),
);
if (invalidPaths.length > 0 || invalidAdditional.length > 0)
@@ -65,11 +60,14 @@ export class JavaScriptRecoveryService {
]),
);
const selectedInputs = new Set(
[parsed.data.path, ...(parsed.data.additional_paths ?? [])].map((path) =>
resolve(path),
[parsed.value.path, ...(parsed.value.additional_paths ?? [])].map(
(path) => resolve(path),
),
);
if (selectedInputs.size !== 1 + (parsed.data.additional_paths ?? []).length)
if (
selectedInputs.size !==
1 + (parsed.value.additional_paths ?? []).length
)
return err(
new AnalysisInputError(OPERATION, undefined, [
{
@@ -79,7 +77,7 @@ export class JavaScriptRecoveryService {
},
]),
);
const execution = await this.provider.recover(parsed.data, options);
const execution = await this.provider.recover(parsed.value, options);
if (!execution.ok) return execution;
const result = javascriptRecoveryResultSchema.safeParse(
execution.value.result,
@@ -92,10 +90,10 @@ export class JavaScriptRecoveryService {
),
);
if (
result.data.source.path !== parsed.data.path ||
result.data.source.path !== parsed.value.path ||
(result.data.additional_sources ?? []).length !==
(parsed.data.additional_paths ?? []).length ||
(parsed.data.additional_paths ?? []).some(
(parsed.value.additional_paths ?? []).length ||
(parsed.value.additional_paths ?? []).some(
(path, index) => result.data.additional_sources?.[index]?.path !== path,
) ||
execution.value.subject.path !== result.data.source.path ||
@@ -114,7 +112,7 @@ export class JavaScriptRecoveryService {
execution.value.provider,
{
operation: OPERATION,
parameters: parsed.data,
parameters: parsed.value,
result: result.data,
rawResult: execution.value.rawResult,
locations: execution.value.locations,
@@ -1,3 +1,4 @@
import { parseAnalysisInput } from "../AnalysisInput.js";
import { z } from "zod";
import {
@@ -28,7 +29,6 @@ import {
MANAGED_STATIC_PROVIDER,
MANAGED_WORKFLOW_PROVIDER,
} from "../../domain/investigationProviders.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
const managedMemberComparisonRequestSchema = z.strictObject({
left: z.unknown(),
@@ -46,16 +46,15 @@ export const executeManagedMemberComparison = (
input: unknown,
): Result<ManagedMemberComparisonExecution, AnalysisError> => {
const operation = "compare_managed_members";
const request = managedMemberComparisonRequestSchema.safeParse(input);
if (!request.success)
return err(
analysisInputErrorFromIssues(operation, request.error.issues, input, {
cause: request.error,
}),
);
const left = readManagedMemberEvidence(request.data.left, "left");
const request = parseAnalysisInput(
operation,
input,
managedMemberComparisonRequestSchema,
);
if (!request.ok) return request;
const left = readManagedMemberEvidence(request.value.left, "left");
if (!left.ok) return left;
const right = readManagedMemberEvidence(request.data.right, "right");
const right = readManagedMemberEvidence(request.value.right, "right");
if (!right.ok) return right;
if (left.value.evidence.evidence_id === right.value.evidence.evidence_id)
return err(
@@ -10,7 +10,7 @@ import {
} from "../../domain/managed/managedReconstruction.js";
import { ok, type Result } from "../../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "../../domain/investigationProviders.js";
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "import_managed_reconstruction" as const;
@@ -18,7 +18,7 @@ const OPERATION = "import_managed_reconstruction" as const;
export const resolveManagedReconstructionEvidenceInput = (
rawInput: unknown,
): Result<ResolvedManagedReconstructionImport, AnalysisError> => {
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
OPERATION,
rawInput,
managedReconstructionImportRequestSchema,
+3 -5
View File
@@ -13,10 +13,8 @@ import {
pythonPycSchema,
} from "../../domain/python/pythonPyc.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_python_pyc";
@@ -32,7 +30,7 @@ export class PythonPycService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectPythonPycInputSchema,
@@ -47,7 +47,7 @@ import {
} from "../../process/OwnedCommand.js";
import type { ProviderProcessSupervisor } from "../../process/ProviderProcess.js";
import { absoluteCallerPath } from "../../filesystem/callerPath.js";
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
export type ReverseEngineeringOperation =
| "inspect_with_objdump"
@@ -215,7 +215,7 @@ export class ReverseEngineeringService {
recordEvidence: undefined,
},
): Promise<Result<Evidence, AnalysisError>> {
const parsed = parseProviderOperationInput(
const parsed = parseAnalysisInput(
"inspect_with_objdump",
rawInput,
objdumpInputSchema,
+3 -5
View File
@@ -11,10 +11,8 @@ import {
} from "../../domain/rust/rustBinary.js";
import { inspectRustBinaryRequestSchema } from "../../domain/rust/rustBinaryQuery.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import {
requireAnalysisViewRetention,
projectRetainedAnalysis,
@@ -40,7 +38,7 @@ export class RustBinaryService {
evidenceById: undefined,
},
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectRustBinaryRequestSchema,
@@ -17,10 +17,8 @@ import {
type DecodeSignalRecordingInput,
} from "../../domain/signals/signalRecording.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
export interface SignalRecordingPort {
/** Release per-call resources before returning, or retain failed cleanup for close/retry. */
@@ -46,7 +44,7 @@ export class SignalRecordingService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "decode_signal_recording";
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
operation,
raw,
decodeSignalRecordingInputSchema,
@@ -85,7 +83,7 @@ export class SignalRecordingService {
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_signal_decoders";
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
operation,
raw,
inspectSignalDecodersInputSchema,
@@ -1,4 +1,4 @@
import { parseProviderOperationInput } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { SqliteDatabasePort } from "./SqliteDatabasePort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
@@ -79,7 +79,7 @@ export class SqliteDatabaseService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectSqliteDatabaseInputSchema,
+3 -5
View File
@@ -12,10 +12,8 @@ import {
DEFAULT_WASM_COMMAND_TIMEOUT_MS,
} from "../../domain/wasm/wasmArtifact.js";
import { err, type Result } from "../../domain/result.js";
import {
completeProviderEvidence,
parseProviderOperationInput,
} from "../ProviderOperationExecution.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
import { parseAnalysisInput } from "../AnalysisInput.js";
const OPERATION = "inspect_wasm_artifact";
@@ -36,7 +34,7 @@ export class WasmArtifactService {
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const input = parseProviderOperationInput(
const input = parseAnalysisInput(
OPERATION,
rawInput,
inspectWasmArtifactInputSchema,
@@ -0,0 +1,120 @@
import { execFile } from "node:child_process";
import { rm, symlink, writeFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { promisify } from "node:util";
import { afterEach, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
SOURCE,
applicationFixture,
electronRuntimeEvidence,
} from "../../fixtures/javascriptRuntimeReconciliation.js";
const execute = promisify(execFile);
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map((path) => rm(path, { recursive: true, force: true })),
);
});
const selectedFixture = async () => {
const physical = await applicationFixture();
const root = await createTestTempDirectory("rea-verifier-selection-");
temporary.push(physical, root);
const alias = join(root, "selected-app");
await symlink(
physical,
alias,
process.platform === "win32" ? "junction" : "dir",
);
return { physical, root, selected: relative(process.cwd(), alias) };
};
// Run the real script helper against the same built owner used by the verifiers.
const analysisIdentity = async (path: string) => {
const helper = new URL(
"../../../scripts/lib/javascript-application-analysis.mjs",
import.meta.url,
).href;
const { stdout } = await execute(
process.execPath,
[
"--input-type=module",
"-e",
`import { analyzeJavaScriptApplicationWithOwnedScope } from ${JSON.stringify(helper)};
const evidence = await analyzeJavaScriptApplicationWithOwnedScope({ input_path: process.argv[1] });
console.log(JSON.stringify({ evidence_id: evidence.evidence_id, selected: evidence.parameters.input_path }));`,
path,
],
{ maxBuffer: 16 * 1_024 * 1_024 },
);
return JSON.parse(stdout) as { evidence_id: string; selected: string };
};
it("retains selected layer paths in runtime verifier parent Evidence", async () => {
const { physical, root, selected } = await selectedFixture();
const evidencePath = join(root, "runtime.json");
await writeFile(
evidencePath,
JSON.stringify(electronRuntimeEvidence(physical, SOURCE)),
);
const expected = await analysisIdentity(selected);
const canonical = await analysisIdentity(physical);
expect(expected.selected).toBe(selected);
expect(expected.evidence_id).not.toBe(canonical.evidence_id);
const { stdout } = await execute(
process.execPath,
[
"scripts/verify/javascript/runtime-observation.mjs",
"--application",
selected,
"--runtime-evidence",
evidencePath,
],
{ maxBuffer: 16 * 1_024 * 1_024 },
);
expect(JSON.parse(stdout)).toMatchObject({
verified: true,
layers: [{ evidence_id: expected.evidence_id }],
});
}, 20_000);
it("retains workflow selections while rejecting aliases of the same target", async () => {
const left = await selectedFixture();
const right = await selectedFixture();
const expectedLeft = await analysisIdentity(left.selected);
const expectedRight = await analysisIdentity(right.selected);
const { stdout } = await execute(
process.execPath,
[
"scripts/verify/javascript/application-workflows.mjs",
"--left",
left.selected,
"--right",
right.selected,
],
{ maxBuffer: 16 * 1_024 * 1_024 },
);
expect(JSON.parse(stdout)).toMatchObject({
verified: true,
left: { evidence_id: expectedLeft.evidence_id },
right: { evidence_id: expectedRight.evidence_id },
});
await expect(
execute(process.execPath, [
"scripts/verify/javascript/application-workflows.mjs",
"--left",
left.selected,
"--right",
left.physical,
]),
).rejects.toMatchObject({
stderr: expect.stringContaining("must resolve to distinct paths"),
});
}, 20_000);