Files
rea/src/domain/analysisView/analysisView.ts
T
Omisakin Joshua 8823c55b0c feat(ghidra): decode Swift String literals in function dossiers (#2049)
* feat(ghidra): decode Swift String literals in function dossiers

Swift passes a String as two words. Small literals keep their UTF-8 bytes
inside those words, and immortal large literals keep a count plus a pointer
to their bytes biased by 32. Ghidra creates no data reference for either, so
analyze_function reported no strings for Swift code.

On AArch64 programs that contain Swift metadata, the Ghidra bridge now runs
constant propagation over the function and decodes register pairs where
Swift consumes a String: consecutive argument registers at calls and tail
calls, x0 and x1 at returns, and both registers of one STP. Large literals
must resolve to terminated bytes inside one initialized block that is not
executable. A pair whose registers both come from conditional selects is
skipped, because Ghidra forks each select without correlating the
condition and could otherwise pair words from different cases.

Decoded values are a derivation from the ABI layout rather than observed
references, so they appear in a new optional swift_string_literals field
with their source instruction, registers and raw words, and in a matching
inspect_analysis_view facet. referenced_strings is unchanged.

Refs #2030

* fix(ghidra): bound Swift literal reads and compare decoded strings

Large immortal literals are measured against the remaining heap before
allocation, and a failed fit stays visible as a limitation. The canonical
empty string is kept. Small and large literals now require matching
address nullability, and function comparison treats decoded literals as
string evidence without equating a missing record to an empty one.

* fix(ghidra): budget Swift literal decoding and tighten use sites

Large literals now draw on one heap budget per dossier, measured once
before decoding, instead of a separate check per literal. A literal that
does not fit is listed in a new swift_string_literal_omissions field
with its address, byte length and reason, so consumers read omissions
from structured data rather than from limitation text.

strings_names comparison checks referenced strings and names first and
reports a change there even when Swift literal coverage differs; it
returns unknown only when those facts match and literal coverage is
absent on one side or partial.

A computed jump counts as a tail call only when it has no resolved
target inside the function, so jump tables no longer qualify, and a
return requires a RETURN p-code operation, so traps no longer qualify.

Decoded literals keep control characters, which Swift strings may
contain and JSON escapes. The Swift verifier builds the literal fixture
for arm64 on any Mac host and now also covers the empty string and a
literal containing a tab.

* fix(ghidra): charge escaped JSON and follow copied selects

The Swift literal budget now charges the worst case per UTF-8 byte: UTF-16
text, six escaped JSON characters held in Gson's growing buffer and in the
returned String, and the UTF-8 bytes written to the socket. Literals made
of control characters can no longer exceed the reserved heap.

A jump counts as a tail call only when every resolved target lies outside
the function. The conditional select check follows register copies back
to their definition, so selects copied into argument registers are
recognized like selects written there directly.

strings_names reports a change when both dossiers decoded different
literals at the same use site, even when either side has omissions; it
returns unknown only when no such use site disagrees.

* fix(ghidra): bound literal records and trace selects across branches

Every Swift literal record now pays a fixed cost against the dossier
budget in addition to its text, so many empty or small literals are
bounded as well. The first record that does not fit is kept as an
omission and decoding stops there, which keeps the omission record
itself bounded; omissions now describe small or large literals.

The conditional select check searches every predecessor path for the
reaching definitions, following register copies, so selects made
before a branch into the use block are recognized.

A dossier records Swift literals and their omissions together. Coverage
reads omissions first, and strings_names compares every use site that
neither side omitted: a decoded literal is known where it was recorded,
and an absence is known only in a dossier whose decoding examined every
use site.

* fix(ghidra): stop every flow at the literal budget and route omissions

Ending one flow from the evaluator leaves other queued flows running, so
the budget now records a shared stop that every flow checks before it
examines another instruction. A record that exceeds the budget is routed
by that capacity result rather than by whether text was decoded, so a
small literal over budget becomes an omission instead of a literal.

When coverage is partial, the per site comparison keeps each literal's
representation as well as its value, matching the full strings_names
projection.

* fix(ghidra): keep unresolved Swift tail-call sites unknown

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-13 20:41:10 +08:00

796 lines
26 KiB
TypeScript

import { z } from "zod";
import { javaScriptSourceSelectionSchema } from "../javascript/javascriptSourceSelection.js";
import {
AnalysisInputError,
type AnalysisInputIssue,
AnalysisUnsupportedTargetError,
} from "../analysisErrorCore.js";
import { digestCanonicalValue } from "../canonicalDigest.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { evidenceSchema, type Evidence } from "../evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js";
import { err, ok, type Result } from "../result.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import { binaryLayoutSchema } from "../native/binaryLayout.js";
import { functionDossierSchema } from "../hopperValues.js";
import {
artifactGraphManifestSchema,
artifactNodeSchema,
artifactOccurrenceSchema,
artifactEdgeSchema,
integrityContradictionSchema,
} from "../artifactGraph.js";
import { artifactInspectionResultSchema } from "../artifactInspection.js";
import {
parseOwnedJavaScriptApplicationAnalysisSteps,
type JavaScriptApplicationAnalysisResult,
} from "../javascript/javascriptApplicationAnalysis.js";
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { projectBinaryLayoutView } from "./binaryLayoutView.js";
import { projectJavaScriptApplicationView } from "./javascriptApplicationView.js";
import {
projectWebNetworkApiSurfaceView,
apiSurfaceFields,
apiSurfaceViewSchema,
} from "./webNetworkApiSurfaceView.js";
import { projectNativeFunctionView } from "./nativeFunctionView.js";
import {
projectEvmInterfaceView,
evmViewRequestSchema,
evmViewItemSchema,
evmSummarySchema,
} from "./evmInterfaceView.js";
import { projectArtifactInventoryView } from "./artifactInventoryView.js";
import {
projectManagedArtifactView,
managedViewRequestSchema,
managedViewItemSchema,
managedSummarySchema,
managedViewContextSchema,
} from "./managedArtifactView.js";
import {
projectWasmArtifactView,
wasmViewRequestSchema,
wasmViewItemSchema,
wasmSummarySchema,
} from "./wasmArtifactView.js";
import {
machoSummarySchema,
machoViewContextSchema,
machoViewItemSchema,
machoViewRequestSchema,
projectMachoView,
} from "./machoView.js";
import {
projectWebNetworkCaptureView,
captureViewRequestSchema,
captureViewItemsSchema,
captureSummarySchema,
} from "./webNetworkCaptureView.js";
import type { WebNetworkApiSurfaceAccumulator } from "./WebNetworkApiSurfaceAccumulator.js";
import {
pcapViewRequestSchema,
pcapViewPageSchema,
pcapSummarySchema,
projectPcapCaptureView,
} from "./pcapCaptureView.js";
import {
qlogViewRequestSchema,
qlogViewPageSchema,
qlogViewContextSchema,
} from "./qlogCaptureView.js";
import {
projectAndroidPackageView,
androidPackageSummarySchema,
androidViewRequestSchema,
androidManifestViewItemSchema,
} from "./androidPackageView.js";
import {
projectDartAotView,
dartAotSummarySchema,
dartAotViewRequestSchema,
dartAotViewContextSchema,
} from "./dartAotView.js";
/** Public name of the selected-view workflow. */
export const INSPECT_ANALYSIS_VIEW_OPERATION = "inspect_analysis_view" as const;
const summaryViewSchema = z.strictObject({ kind: z.literal("summary") });
const facetViewSchema = z.strictObject({
kind: z.literal("facet"),
facet: z.enum(["mitigations", "linkage"]),
});
const indexSelectorSchema = z.strictObject({
index: z.number().int().nonnegative(),
});
const nameSelectorSchema = z.strictObject({ name: z.string() });
const pathSelectorSchema = z.strictObject({
path: z
.string()
.describe(
"Exact observed module path or original source-map reference from a module page; an empty reference is valid.",
),
});
const nodeIdSelectorSchema = z.strictObject({
node_id: prefixedDigestSchema("jag_node"),
});
const moduleItemViewSchema = z.strictObject({
kind: z.literal("item"),
collection: z.literal("modules"),
selector: z.union([pathSelectorSchema, nodeIdSelectorSchema]),
});
const sectionOrSymbolItemViewSchema = z.strictObject({
kind: z.literal("item"),
collection: z.enum(["sections", "symbols"]),
selector: z.union([indexSelectorSchema, nameSelectorSchema]),
});
/**
* The collection-specific selector alternatives are expressed in the owning
* contract so the advertised JSON Schema preserves them: modules accept
* path/node_id, sections and symbols accept index/name. Refinement-only
* constraints do not survive JSON Schema conversion, so the advertised
* schema previously accepted combinations that runtime input validation
* rejects.
*/
const itemViewSchema = z.discriminatedUnion("collection", [
moduleItemViewSchema,
sectionOrSymbolItemViewSchema,
]);
const pageViewSchema = z.strictObject({
kind: z.literal("page"),
collection: z.enum(["sections", "symbols", "modules"]),
offset: z.number().int().nonnegative(),
limit: z.number().int().positive(),
});
/** Bounded native function facets of already authenticated analyze_function Evidence. */
const nativeViewSchema = z.strictObject({
kind: z.literal("native"),
facet: z.enum([
"procedure",
"pseudocode",
"assembly",
"basic_blocks",
"comments",
"callers",
"callees",
"incoming_references",
"outgoing_references",
"unresolved_calls",
"referenced_strings",
"referenced_names",
"swift_string_literals",
"swift_string_literal_omissions",
"native_api",
"value_flow_summary",
"value_flow_operations",
"value_flow_def_use",
"value_flow_effects",
"value_flow_parameters",
"value_flow_parameter_uses",
]),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
});
/** Original inventory records in stable order, optionally matching exact paths. */
const inventoryViewSchema = z.discriminatedUnion("collection", [
z.strictObject({
kind: z.literal("inventory"),
collection: z.literal("occurrences"),
logical_paths: z
.array(z.string())
.optional()
.describe(
"Exact recorded logical paths. Omitted selects all available occurrences; an empty array selects none. This does not resolve filesystem paths.",
),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
}),
z.strictObject({
kind: z.literal("inventory"),
collection: z.enum(["nodes", "edges", "integrity_contradictions"]),
offset: z.number().int().nonnegative().default(0),
limit: z.number().int().positive().default(64),
}),
]);
/** Caller-selected projection of already completed analysis Evidence. */
export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
summaryViewSchema,
facetViewSchema,
itemViewSchema,
pageViewSchema,
nativeViewSchema,
inventoryViewSchema,
wasmViewRequestSchema,
machoViewRequestSchema,
evmViewRequestSchema,
managedViewRequestSchema,
androidViewRequestSchema,
apiSurfaceViewSchema,
captureViewRequestSchema,
qlogViewRequestSchema,
pcapViewRequestSchema,
dartAotViewRequestSchema,
]);
// The application owns admission and authenticated Evidence parsing. SDK and
// CLI envelope parsing must borrow this unknown value rather than clone it
// before admission. Advertise the owning Evidence contract in both dialects.
const inlineEvidenceInputSchema = z.unknown().nonoptional();
inlineEvidenceInputSchema._zod.processJSONSchema =
evidenceSchema._zod.processJSONSchema;
/** Unauthenticated inline input or an exact same-session retained reference. */
export const analysisViewSourceSchema = z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("retained-evidence"),
evidence_id: prefixedDigestSchema("ev"),
}),
z.strictObject({
kind: z.literal("inline"),
evidence: inlineEvidenceInputSchema,
}),
]);
/** Public inspect_analysis_view input. */
export const inspectAnalysisViewInputSchema = z.strictObject({
source: analysisViewSourceSchema,
view: analysisViewRequestSchema,
});
const coverageSchema = z.strictObject({
status: z.enum(["complete-within-view", "page", "empty"]),
examined: z.number().int().nonnegative(),
total: z.number().int().nonnegative(),
next_offset: z.number().int().nonnegative().nullable(),
exhausted: z.boolean(),
});
const artifactSchema = z.strictObject({
path: z
.string()
.describe("Exact recorded artifact path, including an empty string."),
sha256: digestSchema.nullable(),
});
const layoutSummarySchema = z.strictObject({
bytes: z.number().int().nonnegative(),
format: z.literal("elf"),
architecture: z.strictObject({
machine: z.literal("EM_X86_64"),
bits: z.literal(64),
byte_order: z.literal("little"),
}),
image_type: z.enum(["ET_EXEC", "ET_DYN", "ET_REL"]),
entry: z.strictObject({
reported_value: z.string().min(1),
meaning: z.enum(["linked-virtual-address", "not-applicable", "absent"]),
execution_status: z.literal("unknown"),
}),
counts: z.strictObject({
sections: z.number().int().nonnegative(),
segments: z.number().int().nonnegative(),
symbols: z.number().int().nonnegative(),
relocations: z.number().int().nonnegative(),
}),
limitation_count: z.number().int().nonnegative(),
});
const javascriptSummarySchema = z.strictObject({
input_path: z.string().min(1),
format: z.enum(["asar", "directory"]),
root_artifact_sha256: digestSchema,
source_selection: javaScriptSourceSelectionSchema.exactOptional(),
statistics: jsonObjectSchema,
electron: jsonObjectSchema,
coverage: jsonObjectSchema,
limitation_count: z.number().int().nonnegative(),
});
const inventorySummarySchema = z.strictObject({
manifest: artifactGraphManifestSchema,
available_counts: z.strictObject({
nodes: z.number().int().nonnegative(),
occurrences: z.number().int().nonnegative(),
edges: z.number().int().nonnegative(),
integrity_contradictions: z.number().int().nonnegative(),
}),
observed_entry_kind_counts: z.strictObject({
file: z.number().int().nonnegative(),
directory: z.number().int().nonnegative(),
symlink: z.number().int().nonnegative(),
slice: z.number().int().nonnegative(),
}),
source_complete: z.boolean(),
inventory_evidence_ids: z.array(prefixedDigestSchema("ev")).min(1),
inspection: z
.strictObject({
coverage: artifactInspectionResultSchema.shape.coverage,
unexplored_branches:
artifactInspectionResultSchema.shape.unexplored_branches,
})
.nullable(),
limitation_count: z.number().int().nonnegative(),
});
const viewResultBase = {
parent_evidence_id: prefixedDigestSchema("ev"),
parent_operation: z.enum([
"inspect_binary_layout",
"analyze_javascript_application",
"analyze_function",
"inspect_artifact",
"inventory_artifact",
"inspect_wasm_artifact",
"inspect_macho",
"inspect_evm_interface",
"inspect_managed_artifact",
"inspect_web_network_capture",
"inspect_android_package",
"inspect_pcap_capture",
"inspect_dart_aot",
]),
parent_digest: digestSchema,
view_digest: digestSchema,
artifact: artifactSchema,
coverage: coverageSchema,
limitations: z.array(z.string()),
unknowns: z.array(z.string()),
} as const;
const summaryViewResultBase = {
kind: z.literal("summary"),
view: summaryViewSchema,
...viewResultBase,
} as const;
/** Summary `inspect_binary_layout` detail returns; no other view kind. */
export const binaryLayoutSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
summary: layoutSummarySchema,
parent_operation: z.literal("inspect_binary_layout"),
});
/** Summary `analyze_javascript_application` detail returns; no other view kind. */
export const javascriptApplicationSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
summary: javascriptSummarySchema,
parent_operation: z.literal("analyze_javascript_application"),
});
/** Exact summary carrier for artifact inspection's optional delivery contract. */
export const artifactInventorySummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.enum(["inspect_artifact", "inventory_artifact"]),
summary: inventorySummarySchema,
});
/** Exact summary carrier for WASM inspection's optional delivery contract. */
export const wasmArtifactSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_wasm_artifact"),
summary: wasmSummarySchema,
});
/** Exact slice identity and collection counts for Mach-O's selected delivery. */
export const machoSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_macho"),
summary: machoSummarySchema,
});
/** Exact inferred-interface summary for EVM's caller-selected delivery. */
export const evmInterfaceSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_evm_interface"),
summary: evmSummarySchema,
});
/** Exact metadata overview for the managed artifact's selected delivery. */
export const managedArtifactSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_managed_artifact"),
summary: managedSummarySchema,
});
/** Exact historical capture summary, with no body or member-inventory payload. */
export const webNetworkCaptureSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_web_network_capture"),
summary: captureSummarySchema,
});
/** Exact package declarations with retained decoded manifest text. */
export const androidPackageSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_android_package"),
summary: androidPackageSummarySchema,
});
export const pcapCaptureSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_pcap_capture"),
summary: pcapSummarySchema,
});
/** Exact snapshot and pool-coverage carrier for selected AOT delivery. */
export const dartAotSummaryViewResultSchema = z.strictObject({
...summaryViewResultBase,
parent_operation: z.literal("inspect_dart_aot"),
summary: dartAotSummarySchema,
});
/** Projected facts for one selected view of retained analysis Evidence. */
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
z.strictObject({
...viewResultBase,
...apiSurfaceFields,
kind: z.literal("api-surface"),
parent_operation: z.literal("inspect_web_network_capture"),
view: apiSurfaceViewSchema,
artifact: z.strictObject({
path: z.string(),
sha256: digestSchema.nullable(),
}),
}),
z.strictObject({
...summaryViewResultBase,
summary: z.union([
layoutSummarySchema,
javascriptSummarySchema,
inventorySummarySchema,
wasmSummarySchema,
machoSummarySchema,
evmSummarySchema,
managedSummarySchema,
captureSummarySchema,
androidPackageSummarySchema,
pcapSummarySchema,
dartAotSummarySchema,
]),
}),
z.strictObject({
kind: z.literal("capture"),
view: captureViewRequestSchema,
items: captureViewItemsSchema,
...viewResultBase,
parent_operation: z.literal("inspect_web_network_capture"),
}),
z.strictObject({
kind: z.literal("qlog"),
view: qlogViewRequestSchema,
page: qlogViewPageSchema,
context: qlogViewContextSchema,
...viewResultBase,
parent_operation: z.literal("inspect_web_network_capture"),
}),
z.strictObject({
kind: z.literal("pcap"),
view: pcapViewRequestSchema,
page: pcapViewPageSchema,
source_coverage: z.enum(["complete", "partial"]),
...viewResultBase,
parent_operation: z.literal("inspect_pcap_capture"),
}),
z.strictObject({
kind: z.literal("facet"),
view: facetViewSchema,
facet: jsonValueSchema,
...viewResultBase,
}),
z.strictObject({
kind: z.literal("item"),
view: itemViewSchema,
item: jsonValueSchema,
...viewResultBase,
}),
z.strictObject({
kind: z.literal("page"),
view: pageViewSchema,
items: z.array(jsonValueSchema),
...viewResultBase,
}),
z.strictObject({
kind: z.literal("native"),
view: nativeViewSchema,
procedure_address: z.string().nullable(),
item: jsonValueSchema,
...viewResultBase,
}),
z.strictObject({
kind: z.literal("wasm"),
view: wasmViewRequestSchema,
item: wasmViewItemSchema,
...viewResultBase,
}),
z.strictObject({
kind: z.literal("macho"),
view: machoViewRequestSchema,
item: machoViewItemSchema,
...viewResultBase,
...machoViewContextSchema.shape,
parent_operation: z.literal("inspect_macho"),
}),
z.strictObject({
kind: z.literal("evm"),
view: evmViewRequestSchema,
item: evmViewItemSchema,
...viewResultBase,
parent_operation: z.literal("inspect_evm_interface"),
}),
z.strictObject({
kind: z.literal("managed"),
view: managedViewRequestSchema,
item: managedViewItemSchema,
...viewResultBase,
...managedViewContextSchema.shape,
parent_operation: z.literal("inspect_managed_artifact"),
}),
z.strictObject({
kind: z.literal("android"),
view: androidViewRequestSchema,
item: androidManifestViewItemSchema,
...viewResultBase,
parent_operation: z.literal("inspect_android_package"),
}),
z.strictObject({
kind: z.literal("dart-aot"),
view: dartAotViewRequestSchema,
items: z.array(z.string()),
...viewResultBase,
...dartAotViewContextSchema.shape,
parent_operation: z.literal("inspect_dart_aot"),
}),
z.strictObject({
kind: z.literal("inventory"),
view: inventoryViewSchema,
manifest: artifactGraphManifestSchema,
inventory_evidence_ids: z.array(prefixedDigestSchema("ev")).min(1),
source_complete: z.boolean(),
items: z.array(
z.union([
artifactNodeSchema,
artifactOccurrenceSchema,
artifactEdgeSchema,
integrityContradictionSchema,
]),
),
...viewResultBase,
}),
]);
export type AnalysisViewRequest = z.output<typeof analysisViewRequestSchema>;
export type InspectAnalysisViewInput = z.output<
typeof inspectAnalysisViewInputSchema
>;
export type AnalysisViewResult = z.output<typeof analysisViewResultSchema>;
export type AnalysisViewCoverage = z.output<typeof coverageSchema>;
/** Parent record already resolved from inline Evidence or a session reference. */
export interface AnalysisViewParent {
readonly evidenceId: string;
readonly operation: string;
readonly normalizedResult: unknown;
/** Preserve the authenticated source identity, including explicit absence. */
readonly subject?: Evidence["subject"];
readonly limitations: readonly string[];
readonly artifact?: { readonly path: string; readonly sha256: string } | null;
/** Parent Evidence parameters, when the parent operation records them. */
readonly parameters?: unknown;
}
/** Construct a typed input failure for one selected-view constraint. */
export const analysisViewInputError = (
issues: readonly AnalysisInputIssue[],
): AnalysisInputError =>
new AnalysisInputError(INSPECT_ANALYSIS_VIEW_OPERATION, undefined, issues);
/** View payload before the content digest is attached. */
export type UnsignedAnalysisView =
| Omit<
Extract<AnalysisViewResult, { readonly kind: "api-surface" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "summary" }>,
"view_digest"
>
| Omit<Extract<AnalysisViewResult, { readonly kind: "facet" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "item" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "page" }>, "view_digest">
| Omit<
Extract<AnalysisViewResult, { readonly kind: "native" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "inventory" }>,
"view_digest"
>
| Omit<Extract<AnalysisViewResult, { readonly kind: "wasm" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "macho" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "evm" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "qlog" }>, "view_digest">
| Omit<Extract<AnalysisViewResult, { readonly kind: "pcap" }>, "view_digest">
| Omit<
Extract<AnalysisViewResult, { readonly kind: "capture" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "managed" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "android" }>,
"view_digest"
>
| Omit<
Extract<AnalysisViewResult, { readonly kind: "dart-aot" }>,
"view_digest"
>;
/** Attach the content digest that must change when projected bytes change. */
export const sealAnalysisView = (
projection: UnsignedAnalysisView,
): AnalysisViewResult =>
analysisViewResultSchema.parse({
...projection,
view_digest: digestCanonicalValue(projection, "Analysis view"),
});
const parentDigest = (evidenceId: string): string => evidenceId.slice(3);
const parseJavaScriptParent = (
input: unknown,
): Result<JavaScriptApplicationAnalysisResult, AnalysisError> => {
try {
// Only the existing realm-private graph proofs permit reuse. Imported or
// merely frozen graphs still receive full canonical validation.
const steps = parseOwnedJavaScriptApplicationAnalysisSteps(input);
let step = steps.next();
while (!step.done) step = steps.next();
return ok(step.value);
} catch (cause: unknown) {
if (!(cause instanceof z.ZodError)) throw cause;
return err(
analysisInputErrorFromIssues(
INSPECT_ANALYSIS_VIEW_OPERATION,
cause.issues.map((issue) => ({
...issue,
path: ["source", "normalized_result", ...issue.path],
})),
{ source: { normalized_result: input } },
{ cause },
),
);
}
};
const unsupportedParent = (
parent: AnalysisViewParent,
): AnalysisUnsupportedTargetError =>
new AnalysisUnsupportedTargetError(
INSPECT_ANALYSIS_VIEW_OPERATION,
parentArtifactPath(parent),
`Parent Evidence operation ${JSON.stringify(parent.operation)} does not support analysis views. Use completed binary layout, JavaScript application, native function, artifact inventory/inspection, WASM, Mach-O, EVM, managed artifact, Android package, Dart AOT, or web network capture Evidence.`,
);
const parentArtifactPath = (parent: AnalysisViewParent): string => {
if (
typeof parent.normalizedResult === "object" &&
parent.normalizedResult !== null &&
"artifact" in parent.normalizedResult &&
typeof parent.normalizedResult.artifact === "object" &&
parent.normalizedResult.artifact !== null &&
"path" in parent.normalizedResult.artifact &&
typeof parent.normalizedResult.artifact.path === "string"
)
return parent.normalizedResult.artifact.path;
if (
typeof parent.normalizedResult === "object" &&
parent.normalizedResult !== null &&
"input_path" in parent.normalizedResult &&
typeof parent.normalizedResult.input_path === "string"
)
return parent.normalizedResult.input_path;
return parent.evidenceId;
};
/** Named source owners for views that need no extra decoder or accumulator. */
const retainedViewProjectors = new Map<string, typeof projectWasmArtifactView>([
["inspect_wasm_artifact", projectWasmArtifactView],
["inspect_macho", projectMachoView],
["inspect_evm_interface", projectEvmInterfaceView],
["inspect_managed_artifact", projectManagedArtifactView],
["inspect_android_package", projectAndroidPackageView],
["inspect_dart_aot", projectDartAotView],
["inspect_artifact", projectArtifactInventoryView],
["inventory_artifact", projectArtifactInventoryView],
]);
/** Project one selected view from authenticated parent analysis Evidence. */
export const projectAnalysisView = (
parent: AnalysisViewParent,
view: AnalysisViewRequest,
apiSurfaceAccumulator?: WebNetworkApiSurfaceAccumulator,
): Result<AnalysisViewResult, AnalysisError> => {
const digest = parentDigest(parent.evidenceId);
if (!digestSchema.safeParse(digest).success)
return err(
analysisViewInputError([
{
path: ["source"],
reason: "invalid_format",
message: "Parent Evidence ID is not a digest-backed identifier.",
},
]),
);
if (view.kind === "api-surface") {
const projected = projectWebNetworkApiSurfaceView(
parent,
view,
apiSurfaceAccumulator,
);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "inspect_web_network_capture") {
const projected = projectWebNetworkCaptureView(parent, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "inspect_pcap_capture") {
const projected = projectPcapCaptureView(parent, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "inspect_binary_layout") {
const layout = binaryLayoutSchema.safeParse(parent.normalizedResult);
if (!layout.success)
return err(
analysisViewInputError([
{
path: ["source"],
reason: "invalid_value",
message:
"Parent Evidence result does not match inspect_binary_layout.",
},
]),
);
const projected = projectBinaryLayoutView(parent, layout.data, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "analyze_javascript_application") {
const analysis = parseJavaScriptParent(parent.normalizedResult);
if (!analysis.ok) return analysis;
const projected = projectJavaScriptApplicationView(
parent,
analysis.value,
view,
);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
if (parent.operation === "analyze_function") {
const native = functionDossierSchema.safeParse(parent.normalizedResult);
if (!native.success)
return err(
analysisInputErrorFromIssues(
INSPECT_ANALYSIS_VIEW_OPERATION,
native.error.issues.map((issue) => ({
...issue,
path: ["source", "normalized_result", ...issue.path],
})),
{ source: { normalized_result: parent.normalizedResult } },
{ cause: native.error },
),
);
const projected = projectNativeFunctionView(parent, native.data, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
const project = retainedViewProjectors.get(parent.operation);
if (project !== undefined) {
const projected = project(parent, view);
return projected.ok ? ok(sealAnalysisView(projected.value)) : projected;
}
return err(unsupportedParent(parent));
};