diff --git a/scripts/verify/javascript/application-workflows.mjs b/scripts/verify/javascript/application-workflows.mjs index 5a88b7e11e..725e48a9ab 100644 --- a/scripts/verify/javascript/application-workflows.mjs +++ b/scripts/verify/javascript/application-workflows.mjs @@ -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; diff --git a/scripts/verify/javascript/runtime-observation.mjs b/scripts/verify/javascript/runtime-observation.mjs index b9af11516c..5dcaf2ca71 100644 --- a/scripts/verify/javascript/runtime-observation.mjs +++ b/scripts/verify/javascript/runtime-observation.mjs @@ -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", diff --git a/src/application/AnalysisInput.test.ts b/src/application/AnalysisInput.test.ts new file mode 100644 index 0000000000..207d39f1dd --- /dev/null +++ b/src/application/AnalysisInput.test.ts @@ -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(); +}); diff --git a/src/application/AnalysisInput.ts b/src/application/AnalysisInput.ts new file mode 100644 index 0000000000..556dc99564 --- /dev/null +++ b/src/application/AnalysisInput.ts @@ -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 = ( + operation: string, + rawInput: unknown, + schema: z.ZodType, + signal?: AbortSignal, +): Result => { + 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); +}; diff --git a/src/application/FunctionAnalysisEvidence.ts b/src/application/FunctionAnalysisEvidence.ts index 2502adde81..b97fa398ca 100644 --- a/src/application/FunctionAnalysisEvidence.ts +++ b/src/application/FunctionAnalysisEvidence.ts @@ -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_, diff --git a/src/application/ProviderOperationExecution.test.ts b/src/application/ProviderOperationExecution.test.ts index 8d32d48fc7..f3a36e1b5a 100644 --- a/src/application/ProviderOperationExecution.test.ts +++ b/src/application/ProviderOperationExecution.test.ts @@ -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(); -}); diff --git a/src/application/ProviderOperationExecution.ts b/src/application/ProviderOperationExecution.ts index be9478302b..ce441b57a9 100644 --- a/src/application/ProviderOperationExecution.ts +++ b/src/application/ProviderOperationExecution.ts @@ -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 = ( - operation: string, - rawInput: unknown, - schema: z.ZodType, - signal?: AbortSignal, -): Result => { - 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, diff --git a/src/application/ReconstructionCoverageService.ts b/src/application/ReconstructionCoverageService.ts index 906474b5f5..c19cef03a1 100644 --- a/src/application/ReconstructionCoverageService.ts +++ b/src/application/ReconstructionCoverageService.ts @@ -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 => { - 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, ); }; diff --git a/src/application/ReconstructionObligationLedgerRequest.ts b/src/application/ReconstructionObligationLedgerRequest.ts index 6a568f70a4..98e0f7e043 100644 --- a/src/application/ReconstructionObligationLedgerRequest.ts +++ b/src/application/ReconstructionObligationLedgerRequest.ts @@ -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 => { - 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, }); }; diff --git a/src/application/WebModuleTraceService.ts b/src/application/WebModuleTraceService.ts index 2014b59c07..84d27286ae 100644 --- a/src/application/WebModuleTraceService.ts +++ b/src/application/WebModuleTraceService.ts @@ -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> { - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( OPERATION, rawInput, webModuleTraceInputSchema, diff --git a/src/application/WebRuntimeService.ts b/src/application/WebRuntimeService.ts index 53af27d648..b30a6097a0 100644 --- a/src/application/WebRuntimeService.ts +++ b/src/application/WebRuntimeService.ts @@ -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> { const operation = "observe_web_execution"; - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( operation, rawInput, observeWebExecutionInputSchema, @@ -64,7 +64,7 @@ export class WebRuntimeService { options?: ExecutionOptions, ): Promise> { const operation = "inspect_web_event_listeners"; - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( operation, rawInput, inspectWebEventListenersInputSchema, diff --git a/src/application/WebSourceLocationService.ts b/src/application/WebSourceLocationService.ts index ea64a191fd..58d20c556d 100644 --- a/src/application/WebSourceLocationService.ts +++ b/src/application/WebSourceLocationService.ts @@ -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> { - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( OPERATION, rawInput, webSourceLocationInputSchema, diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index 531e1be5af..3c69a9d632 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -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> => { - 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); }; /** diff --git a/src/application/android/AndroidPackageInspection.ts b/src/application/android/AndroidPackageInspection.ts index a879b79727..07c9312a96 100644 --- a/src/application/android/AndroidPackageInspection.ts +++ b/src/application/android/AndroidPackageInspection.ts @@ -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> => { const operation = "inspect_android_package"; - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( operation, rawInput, androidPackageInspectionInputSchema, diff --git a/src/application/apkSignature/ApkSignatureService.ts b/src/application/apkSignature/ApkSignatureService.ts index bb98c31227..74ec0731a5 100644 --- a/src/application/apkSignature/ApkSignatureService.ts +++ b/src/application/apkSignature/ApkSignatureService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectApkSignatureInputSchema, diff --git a/src/application/artifacts/DesktopApplicationService.ts b/src/application/artifacts/DesktopApplicationService.ts index 7fd4773043..865bdbaf5b 100644 --- a/src/application/artifacts/DesktopApplicationService.ts +++ b/src/application/artifacts/DesktopApplicationService.ts @@ -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> { const operation = "inspect_desktop_application"; - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( operation, rawInput, inspectDesktopApplicationInputSchema, diff --git a/src/application/binaryDiagnostics/AppleIpsService.ts b/src/application/binaryDiagnostics/AppleIpsService.ts index 3581712eeb..29a15cac4a 100644 --- a/src/application/binaryDiagnostics/AppleIpsService.ts +++ b/src/application/binaryDiagnostics/AppleIpsService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectAppleIpsInputSchema, diff --git a/src/application/binaryDiagnostics/BinaryLayoutService.ts b/src/application/binaryDiagnostics/BinaryLayoutService.ts index 2b3e04d509..87dd9723b6 100644 --- a/src/application/binaryDiagnostics/BinaryLayoutService.ts +++ b/src/application/binaryDiagnostics/BinaryLayoutService.ts @@ -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[0], ) => ReturnType, ): Promise> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectBinaryLayoutInputSchema, diff --git a/src/application/binaryDiagnostics/ByteEntropyService.ts b/src/application/binaryDiagnostics/ByteEntropyService.ts index c3cd75dc1a..2e5776d587 100644 --- a/src/application/binaryDiagnostics/ByteEntropyService.ts +++ b/src/application/binaryDiagnostics/ByteEntropyService.ts @@ -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> { - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( OPERATION, rawInput, inspectByteEntropyInputSchema, diff --git a/src/application/binaryDiagnostics/DevicetreeService.ts b/src/application/binaryDiagnostics/DevicetreeService.ts index af2f76aff0..44e58443c4 100644 --- a/src/application/binaryDiagnostics/DevicetreeService.ts +++ b/src/application/binaryDiagnostics/DevicetreeService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectDevicetreeInputSchema, diff --git a/src/application/binaryDiagnostics/ElfNotesService.ts b/src/application/binaryDiagnostics/ElfNotesService.ts index b33ac1cf4d..2640172e23 100644 --- a/src/application/binaryDiagnostics/ElfNotesService.ts +++ b/src/application/binaryDiagnostics/ElfNotesService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectElfNotesInputSchema, diff --git a/src/application/binaryDiagnostics/KernelModuleService.ts b/src/application/binaryDiagnostics/KernelModuleService.ts index 0122071e0c..8f8fbe1dd7 100644 --- a/src/application/binaryDiagnostics/KernelModuleService.ts +++ b/src/application/binaryDiagnostics/KernelModuleService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectKernelModuleInputSchema, diff --git a/src/application/binaryDiagnostics/NativeExportsService.ts b/src/application/binaryDiagnostics/NativeExportsService.ts index 4ac52072aa..590ccbc3af 100644 --- a/src/application/binaryDiagnostics/NativeExportsService.ts +++ b/src/application/binaryDiagnostics/NativeExportsService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectNativeExportsInputSchema, diff --git a/src/application/binaryDiagnostics/PeCertificatesService.ts b/src/application/binaryDiagnostics/PeCertificatesService.ts index 9faf5f53bd..0868dd6fac 100644 --- a/src/application/binaryDiagnostics/PeCertificatesService.ts +++ b/src/application/binaryDiagnostics/PeCertificatesService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectPeCertificatesInputSchema, diff --git a/src/application/binaryDiagnostics/PeImportsService.ts b/src/application/binaryDiagnostics/PeImportsService.ts index 0cb2f55eb1..299bfa4fad 100644 --- a/src/application/binaryDiagnostics/PeImportsService.ts +++ b/src/application/binaryDiagnostics/PeImportsService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectPeImportsInputSchema, diff --git a/src/application/binaryDiagnostics/PeLoadConfigService.ts b/src/application/binaryDiagnostics/PeLoadConfigService.ts index bb4e322b0d..4db23b6f69 100644 --- a/src/application/binaryDiagnostics/PeLoadConfigService.ts +++ b/src/application/binaryDiagnostics/PeLoadConfigService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectPeLoadConfigInputSchema, diff --git a/src/application/binaryDiagnostics/PeResourcesService.ts b/src/application/binaryDiagnostics/PeResourcesService.ts index c0b0f6a2f9..78b58216ca 100644 --- a/src/application/binaryDiagnostics/PeResourcesService.ts +++ b/src/application/binaryDiagnostics/PeResourcesService.ts @@ -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> { const operation = "inspect_pe_resources"; - const input = parseProviderOperationInput( + const input = parseAnalysisInput( operation, rawInput, inspectPeResourcesInputSchema, diff --git a/src/application/binaryDiagnostics/RecordedCrashService.ts b/src/application/binaryDiagnostics/RecordedCrashService.ts index e1dcd2d166..7713a336b4 100644 --- a/src/application/binaryDiagnostics/RecordedCrashService.ts +++ b/src/application/binaryDiagnostics/RecordedCrashService.ts @@ -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[0], ) => ReturnType, ): Promise> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectRecordedCrashInputSchema, diff --git a/src/application/captures/PacketCaptureService.ts b/src/application/captures/PacketCaptureService.ts index 3c224c0587..832fe95e92 100644 --- a/src/application/captures/PacketCaptureService.ts +++ b/src/application/captures/PacketCaptureService.ts @@ -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> { const operation = "inspect_packet_capture"; - const input = parseProviderOperationInput( + const input = parseAnalysisInput( operation, raw, inspectPacketCaptureInputSchema, @@ -85,7 +83,7 @@ export class PacketCaptureService { options?: ExecutionOptions, ): Promise> { const operation = "inspect_packet_fields"; - const input = parseProviderOperationInput( + const input = parseAnalysisInput( operation, raw, inspectPacketFieldsInputSchema, diff --git a/src/application/electron/ElectronFuseService.ts b/src/application/electron/ElectronFuseService.ts index 4290ebdf4b..dab580db09 100644 --- a/src/application/electron/ElectronFuseService.ts +++ b/src/application/electron/ElectronFuseService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectElectronFusesInputSchema, diff --git a/src/application/erlang/BeamService.ts b/src/application/erlang/BeamService.ts index 46fa29a988..bcf5e904b0 100644 --- a/src/application/erlang/BeamService.ts +++ b/src/application/erlang/BeamService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectErlangBeamInputSchema, diff --git a/src/application/evm/EvmInterfaceService.ts b/src/application/evm/EvmInterfaceService.ts index 4706106c47..99fb060598 100644 --- a/src/application/evm/EvmInterfaceService.ts +++ b/src/application/evm/EvmInterfaceService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectEvmInterfaceInputSchema, diff --git a/src/application/flutter/DartAotInspection.ts b/src/application/flutter/DartAotInspection.ts index ab3deb6388..1d4b48f236 100644 --- a/src/application/flutter/DartAotInspection.ts +++ b/src/application/flutter/DartAotInspection.ts @@ -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> => { const operation = "inspect_dart_aot"; - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( operation, rawInput, dartAotInspectionInputSchema, diff --git a/src/application/frida/FridaInstrumentationService.ts b/src/application/frida/FridaInstrumentationService.ts index 08084743ec..b615fda1a7 100644 --- a/src/application/frida/FridaInstrumentationService.ts +++ b/src/application/frida/FridaInstrumentationService.ts @@ -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 { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( "frida_session_status", rawInput, fridaSessionStatusInputSchema, diff --git a/src/application/go/GoBinaryService.ts b/src/application/go/GoBinaryService.ts index 78f5cfe562..536c7c59a5 100644 --- a/src/application/go/GoBinaryService.ts +++ b/src/application/go/GoBinaryService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectGoBinaryInputSchema, diff --git a/src/application/il2cpp/Il2cppMetadataService.ts b/src/application/il2cpp/Il2cppMetadataService.ts index 2e8a04ab75..be18ef7a2f 100644 --- a/src/application/il2cpp/Il2cppMetadataService.ts +++ b/src/application/il2cpp/Il2cppMetadataService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectIl2cppMetadataInputSchema, diff --git a/src/application/javascript/JavaScriptApplicationWorkflowService.ts b/src/application/javascript/JavaScriptApplicationWorkflowService.ts index 703dbbb4b9..11c6c6d78c 100644 --- a/src/application/javascript/JavaScriptApplicationWorkflowService.ts +++ b/src/application/javascript/JavaScriptApplicationWorkflowService.ts @@ -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, diff --git a/src/application/javascript/JavaScriptRecoveryService.ts b/src/application/javascript/JavaScriptRecoveryService.ts index c5291f148d..a0bfed4778 100644 --- a/src/application/javascript/JavaScriptRecoveryService.ts +++ b/src/application/javascript/JavaScriptRecoveryService.ts @@ -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> { - 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, diff --git a/src/application/managed/ManagedMemberComparisonService.ts b/src/application/managed/ManagedMemberComparisonService.ts index daa5cc799c..dfe9a0da44 100644 --- a/src/application/managed/ManagedMemberComparisonService.ts +++ b/src/application/managed/ManagedMemberComparisonService.ts @@ -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 => { 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( diff --git a/src/application/managed/ManagedReconstructionService.ts b/src/application/managed/ManagedReconstructionService.ts index 70e6323838..27c2e6092a 100644 --- a/src/application/managed/ManagedReconstructionService.ts +++ b/src/application/managed/ManagedReconstructionService.ts @@ -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 => { - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( OPERATION, rawInput, managedReconstructionImportRequestSchema, diff --git a/src/application/python/PythonPycService.ts b/src/application/python/PythonPycService.ts index 4cc8e2e57f..3d77369b06 100644 --- a/src/application/python/PythonPycService.ts +++ b/src/application/python/PythonPycService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectPythonPycInputSchema, diff --git a/src/application/reverse/ReverseEngineeringService.ts b/src/application/reverse/ReverseEngineeringService.ts index 726abb4475..e4117b31a8 100644 --- a/src/application/reverse/ReverseEngineeringService.ts +++ b/src/application/reverse/ReverseEngineeringService.ts @@ -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> { - const parsed = parseProviderOperationInput( + const parsed = parseAnalysisInput( "inspect_with_objdump", rawInput, objdumpInputSchema, diff --git a/src/application/rust/RustBinaryService.ts b/src/application/rust/RustBinaryService.ts index 79a02099e7..3784f4ada7 100644 --- a/src/application/rust/RustBinaryService.ts +++ b/src/application/rust/RustBinaryService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectRustBinaryRequestSchema, diff --git a/src/application/signals/SignalRecordingService.ts b/src/application/signals/SignalRecordingService.ts index 67570ea595..9173f6e859 100644 --- a/src/application/signals/SignalRecordingService.ts +++ b/src/application/signals/SignalRecordingService.ts @@ -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> { const operation = "decode_signal_recording"; - const input = parseProviderOperationInput( + const input = parseAnalysisInput( operation, raw, decodeSignalRecordingInputSchema, @@ -85,7 +83,7 @@ export class SignalRecordingService { options?: ExecutionOptions, ): Promise> { const operation = "inspect_signal_decoders"; - const input = parseProviderOperationInput( + const input = parseAnalysisInput( operation, raw, inspectSignalDecodersInputSchema, diff --git a/src/application/sqlite/SqliteDatabaseService.ts b/src/application/sqlite/SqliteDatabaseService.ts index ba206261d7..6f79fbdd63 100644 --- a/src/application/sqlite/SqliteDatabaseService.ts +++ b/src/application/sqlite/SqliteDatabaseService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectSqliteDatabaseInputSchema, diff --git a/src/application/wasm/WasmArtifactService.ts b/src/application/wasm/WasmArtifactService.ts index a95ab38014..2545311805 100644 --- a/src/application/wasm/WasmArtifactService.ts +++ b/src/application/wasm/WasmArtifactService.ts @@ -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> { - const input = parseProviderOperationInput( + const input = parseAnalysisInput( OPERATION, rawInput, inspectWasmArtifactInputSchema, diff --git a/tests/boundary/process/javascriptVerifierSelectedPaths.test.ts b/tests/boundary/process/javascriptVerifierSelectedPaths.test.ts new file mode 100644 index 0000000000..19b4e51755 --- /dev/null +++ b/tests/boundary/process/javascriptVerifierSelectedPaths.test.ts @@ -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);