rea/src/domain/analysisView/binaryLayoutView.ts

244 lines
7.3 KiB
TypeScript

import {
AnalysisInputError,
type AnalysisInputIssue,
} from "../analysisErrorCore.js";
import type { AnalysisError } from "../analysisErrorBase.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import type { BinaryLayout } from "../native/binaryLayout.js";
import { err, ok, type Result } from "../result.js";
import type {
AnalysisViewParent,
AnalysisViewRequest,
UnsignedAnalysisView,
} from "./analysisView.js";
import {
completeWithinViewCoverage as completeCoverage,
pageViewCoverage as pageCoverage,
} from "./analysisViewCoverage.js";
const OPERATION = "inspect_analysis_view";
type LayoutName = BinaryLayout["sections"][number]["name"];
const inputError = (
issues: readonly AnalysisInputIssue[],
): AnalysisInputError => new AnalysisInputError(OPERATION, undefined, issues);
const layoutUnknowns = (layout: BinaryLayout): readonly string[] => {
const unknowns = [
"runtime_load_base is unknown",
"entry_point.execution_status is unknown",
"relocation_inventory_completeness is unknown",
"linkage.convenience_maps_completeness is unknown",
"linkage.runtime_library_paths are unknown",
];
if (layout.mitigations.nx_indicator === null)
unknowns.push("mitigations.nx_indicator is unknown");
if (layout.mitigations.relro === null)
unknowns.push("mitigations.relro is unknown");
return unknowns;
};
const displayName = (name: LayoutName): string => name.display;
const namedMatches = <Row extends { readonly name: LayoutName }>(
rows: readonly Row[],
name: string,
): readonly { readonly index: number; readonly row: Row }[] =>
rows.flatMap((row, index) =>
row.name.unknown_reason === null && displayName(row.name) === name
? [{ index, row }]
: [],
);
const selectNamed = <Row extends { readonly name: LayoutName }>(
collection: "sections" | "symbols",
rows: readonly Row[],
name: string,
): Result<Row, AnalysisError> => {
const matches = namedMatches(rows, name);
if (matches.length === 1) {
const selected = matches[0];
if (selected !== undefined) return ok(selected.row);
}
if (matches.length === 0)
return err(
inputError([
{
path: ["view", "selector", "name"],
reason: "invalid_value",
message: `No ${collection} named ${JSON.stringify(name)}. Collection has ${String(rows.length)} entries; page it to inspect exact indexes and names.`,
},
]),
);
return err(
inputError([
{
path: ["view", "selector", "name"],
reason: "invalid_value",
message: `Multiple ${collection} named ${JSON.stringify(name)}; select one by index.`,
expected: matches.map(({ index }) => ({ index })),
},
]),
);
};
const selectIndexed = <Row>(
collection: "sections" | "symbols",
rows: readonly Row[],
index: number,
): Result<Row, AnalysisError> => {
const row = rows[index];
if (row !== undefined) return ok(row);
return err(
inputError([
{
path: ["view", "selector", "index"],
reason: "out_of_range",
message: `${collection} index ${String(index)} is outside 0..${String(Math.max(0, rows.length - 1))} (${String(rows.length)} entries).`,
minimum: 0,
maximum: Math.max(0, rows.length - 1),
},
]),
);
};
const pageRows = <Row>(
rows: readonly Row[],
offset: number,
limit: number,
): readonly Row[] => rows.slice(offset, offset + limit);
const layoutIncompatible = (detail: string): AnalysisError =>
inputError([
{
path: ["view"],
reason: "invalid_value",
message: detail,
},
]);
const parentFields = (parent: AnalysisViewParent, layout: BinaryLayout) => ({
parent_evidence_id: parent.evidenceId,
parent_operation: "inspect_binary_layout" as const,
parent_digest: parent.evidenceId.slice(3),
artifact: {
path: layout.artifact.path,
sha256: layout.artifact.sha256,
},
limitations: uniqueSorted([...parent.limitations, ...layout.limitations]),
unknowns: uniqueSorted([...layoutUnknowns(layout)]),
});
const layoutSummary = (layout: BinaryLayout) => ({
bytes: layout.artifact.bytes,
format: layout.format,
architecture: layout.architecture,
image_type: layout.image_type,
entry: layout.entry_point,
counts: {
sections: layout.sections.length,
segments: layout.segments.length,
symbols: layout.symbols.length,
relocations: layout.relocations.length,
},
limitation_count: layout.limitations.length,
});
/** Project one layout view without re-running the decoder. */
export const projectBinaryLayoutView = (
parent: AnalysisViewParent,
layout: BinaryLayout,
view: Exclude<AnalysisViewRequest, { kind: "api-surface" }>,
): Result<UnsignedAnalysisView, AnalysisError> => {
const shared = parentFields(parent, layout);
if (view.kind === "native")
return err(
layoutIncompatible("Native views require analyze_function Evidence."),
);
if (view.kind === "summary")
return ok({
kind: "summary",
view,
summary: layoutSummary(layout),
coverage: completeCoverage(
layout.sections.length +
layout.segments.length +
layout.symbols.length +
layout.relocations.length,
layout.sections.length +
layout.segments.length +
layout.symbols.length +
layout.relocations.length,
),
...shared,
});
if (view.kind === "facet")
return ok({
kind: "facet",
view,
facet: view.facet === "mitigations" ? layout.mitigations : layout.linkage,
coverage: completeCoverage(1, 1),
...shared,
});
if (view.kind === "item") {
if (view.collection === "modules")
return err(
layoutIncompatible(
"modules collection applies to analyze_javascript_application Evidence.",
),
);
const selected =
view.collection === "sections"
? selectLayoutItem(view.collection, layout.sections, view.selector)
: selectLayoutItem(view.collection, layout.symbols, view.selector);
if (!selected.ok) return selected;
return ok({
kind: "item",
view,
item: selected.value,
coverage: completeCoverage(
1,
view.collection === "sections"
? layout.sections.length
: layout.symbols.length,
),
...shared,
});
}
if (view.collection === "modules")
return err(
layoutIncompatible(
"modules collection applies to analyze_javascript_application Evidence.",
),
);
const rows =
view.collection === "sections" ? layout.sections : layout.symbols;
const items =
view.collection === "sections"
? pageRows(layout.sections, view.offset, view.limit)
: pageRows(layout.symbols, view.offset, view.limit);
return ok({
kind: "page",
view,
items: [...items],
coverage: pageCoverage(view.offset, items.length, rows.length),
...shared,
});
};
const selectLayoutItem = <Row extends { readonly name: LayoutName }>(
collection: "sections" | "symbols",
rows: readonly Row[],
selector: Extract<AnalysisViewRequest, { readonly kind: "item" }>["selector"],
): Result<Row, AnalysisError> => {
if ("index" in selector)
return selectIndexed(collection, rows, selector.index);
if ("name" in selector) return selectNamed(collection, rows, selector.name);
return err(
layoutIncompatible(
"Section and symbol items are selected by index or exact name.",
),
);
};