mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
291059adb4
* feat(analysis-view): project an environment manifest from web network captures Add the environment-manifest view kind to inspect_analysis_view for retained HAR Evidence: the primary origin's recorded request lines become a deterministic tool surface (names, path templates, parameters with their rule and basis, observed query names, value-free body shapes, record pointers), with excluded groups, three fixed unknowns and the shared projection budget. The four format rules always apply; after-uid-literal, opaque-token and sibling-variance are opt-in and labelled, and literal fold-in never touches a format-rule parameter. The api-surface accumulator moves onto shared route, media-type and budget helpers without behaviour change. Refs morluto/rea#4933 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web-network): check the manifest refusal text like the api-surface refusal The real-capture verifier stringified the whole MCP result for the environment-manifest refusal, so a declared "[" literal matched the result envelope itself; compare the parsed refusal as the api-surface branch does. Refs morluto/rea#4933 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(specs): record the hosted web-network lane run for the manifest view Refs morluto/rea#4933 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(specs): reconcile the manifest view verification scope note with the hosted run Refs morluto/rea#4933 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): bound body, route and name work in the environment manifest Addresses the automated review findings on the PR: read request bodies at the top level only instead of walking nested values; admit every literal route part before materializing it; compare at most 32 segments in sibling-variance and say so; keep stop words literal after a uid literal; keep the next collision suffix per tool-name base; preserve a templated exclusion when two groups print the same row. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): keep manifest folding, naming and media-type reads linear Addresses the second automated review round: fold literal siblings through set membership, key collision suffixes by the truncated name they share, and charge a request media type before it is scanned. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): charge the manifest group key before building it Addresses the third automated review round: the composite key copies the method, origin, template and rules, so that copy is reserved before the key is stringified. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): accept a +json suffix only on application media types Addresses the fourth automated review round: `image/example+json` or `text/example+json` no longer counts as JSON for origin selection, exclusion or body shapes, matching the documented `application/json` and `application/*+json` contract. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): require the capture parent's exact subject for a manifest Addresses the fifth automated review round: the manifest cites the report path and digest, so a parent whose subject is absent or names another path or SHA-256 is refused at admission, as the capture view already does; the api-surface view keeps its existing admission. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): share truncated suffix allocation work Track suffix progress by the actual truncated prefix and suffix width. Distinct full names can collide only after suffix truncation; sharing that progress prevents quadratic rescanning while preserving shorter free names and existing allocation order. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(analysis-view): merge only identifier-like siblings under sibling-variance Addresses the sixth automated review finding: a bucket merged whenever one literal carried a digit, folding plain words such as `mine` into the parameter. The rule now partitions the bucket, merges the identifier-like siblings and leaves the other literals as their own tools. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(analysis-view): combine qualifying sibling cohorts Keep coded identifiers and a qualifying plural hyphenated cohort in one eligibility Set before checking the minimum merge size. Read each literal once and reuse the same membership at the consumer, preserving non-identifier routes and the three-hyphen minimum. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * docs(analysis-view): clarify opt-in sibling folding State the additional sibling-variance selection in the guide, acceptance specification and MCP parameter description. Preserve the existing single-rule behavior and caller-selected inference rules. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
273 lines
8.0 KiB
TypeScript
273 lines
8.0 KiB
TypeScript
import {
|
|
AnalysisInputError,
|
|
type AnalysisInputIssue,
|
|
} from "../analysisErrorCore.js";
|
|
import type { AnalysisError } from "../analysisErrorBase.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 {
|
|
boundedViewMessages,
|
|
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: boundedViewMessages(
|
|
[...parent.limitations, ...layout.limitations],
|
|
parent.evidenceId,
|
|
"limitations",
|
|
),
|
|
unknowns: boundedViewMessages(
|
|
layoutUnknowns(layout),
|
|
parent.evidenceId,
|
|
"unknowns",
|
|
),
|
|
});
|
|
|
|
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" | "environment-manifest" }
|
|
>,
|
|
): Result<UnsignedAnalysisView, AnalysisError> => {
|
|
const shared = parentFields(parent, layout);
|
|
if (view.kind === "native")
|
|
return err(
|
|
layoutIncompatible("Native views require analyze_function Evidence."),
|
|
);
|
|
if (view.kind === "inventory")
|
|
return err(
|
|
layoutIncompatible("Inventory views require artifact Evidence."),
|
|
);
|
|
if (view.kind === "wasm")
|
|
return err(
|
|
layoutIncompatible("WASM views require inspect_wasm_artifact Evidence."),
|
|
);
|
|
if (view.kind === "evm")
|
|
return err(
|
|
layoutIncompatible("EVM views require inspect_evm_interface 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.kind !== "page")
|
|
return err(
|
|
layoutIncompatible(
|
|
"Layout views require summary, facet, item or page selections.",
|
|
),
|
|
);
|
|
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.",
|
|
),
|
|
);
|
|
};
|