mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
feat(mcp): select views of retained analysis results (#1180)
Add explicit summary, facet, item and paged views of retained analysis Evidence without repeating analysis. Preserve captured observations, graph validation, artifact identity, coverage and complete default results. Fixes #1050 Verified with focused source, CLI, pinned-SDK schema and MCP tests, serial static checks, and one complete native Windows Notion ASAR workflow. Real Hopper/Ghidra verification was not performed for this provider-independent projection change. Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
This commit is contained in:
1 parent
911152a04d
commit
db675f1d52
39 files changed
+2609
-30
No files matched your search
@@ -84,6 +84,13 @@ uses isolated mode and an owned cache. Exact upstream profiles are recorded in
|
||||
stack can accompany an unknown (`null`) NX indicator. ET_DYN can be a shared
|
||||
library, and an absent canary symbol does not prove every function unprotected.
|
||||
|
||||
When complete layout Evidence is already retained, `inspect_analysis_view` /
|
||||
`inspect-analysis-view` projects a summary, the mitigations or linkage facet,
|
||||
one section or symbol, or a stable page without re-running the decoder. CLI
|
||||
one-shot invocations pass portable inline Evidence JSON; an MCP session uses
|
||||
the retained `evidence_id`. Page `limit` is required and bounded by the
|
||||
measured MCP stdio budget documented on the tool contract.
|
||||
|
||||
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
|
||||
and 30-second owned command deadline. The reply budget applies to the decoder
|
||||
record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
REA derives a complete reachable feature trace from one authenticated JavaScript
|
||||
Application Graph and compares two authenticated graph versions. The MCP tools
|
||||
are `trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes`; their CLI equivalents use the same names
|
||||
with hyphens.
|
||||
are `inspect_analysis_view`, `trace_application_feature`,
|
||||
`trace_javascript_semantics`, `compare_application_versions`,
|
||||
`compare_source_to_bundle`, and `compare_javascript_export_shapes`; their CLI
|
||||
equivalents use the same names with hyphens. `inspect_analysis_view` projects a
|
||||
summary, module page, or one module from already completed application
|
||||
Evidence; it does not walk relationships.
|
||||
|
||||
These workflows consume Evidence produced by
|
||||
`inspect_analysis_view` also projects retained `inspect_binary_layout`
|
||||
Evidence. The remaining workflows consume Evidence produced by
|
||||
`analyze_javascript_application` or `reconcile_javascript_runtime`. They do not
|
||||
read an artifact, execute application code, attach to a process, or open a
|
||||
native-analysis provider. Static artifact observations, passive runtime
|
||||
|
||||
@@ -108,14 +108,15 @@ memory needed to construct the analysis graph itself.
|
||||
MCP prepares the complete repeated response incrementally against the pinned
|
||||
SDK's 10 MiB stdio receive-buffer budget. Oversized results return an actionable
|
||||
transport constraint and the exact same-session Evidence reference. Use
|
||||
`trace_application_feature` to inspect a selected module's relationships, or
|
||||
`export_evidence_bundle` to write the complete canonical bundle without a
|
||||
document-sized allocation. Same-session analysis reads reuse authenticated
|
||||
immutable snapshots; foreign inline Evidence is still parsed and authenticated.
|
||||
`inspect_analysis_view` with the retained Evidence ID for a summary, one
|
||||
module, or a stable page of module identities. Use `trace_application_feature`
|
||||
once a module, route, or string seed is known, or `export_evidence_bundle` to
|
||||
write the complete canonical bundle without a document-sized allocation.
|
||||
Same-session analysis reads reuse authenticated immutable snapshots; foreign
|
||||
inline Evidence is still parsed and authenticated.
|
||||
See [MCP tool results](mcp-contracts.md#tool-results) for larger client buffers
|
||||
and `REA_MCP_MAX_RESPONSE_BYTES`. Follow-up results remain complete and can also
|
||||
exceed the transport budget. Compact result views are tracked separately in
|
||||
[#1050](https://github.com/morluto/rea/issues/1050).
|
||||
exceed the transport budget.
|
||||
|
||||
## What is reconstructed
|
||||
|
||||
|
||||
+15
-2
@@ -194,8 +194,9 @@ room for the JSON-RPC envelope. If a result cannot fit, REA returns
|
||||
`resource_constraint` with `details.resource: "transport"` before constructing
|
||||
a document-sized string. Analysis Evidence remains complete in the current
|
||||
session. Its exact reference is reported in
|
||||
`details.reported_limits.evidence_reference`; use it with a focused application
|
||||
workflow, or call `export_evidence_bundle` with a destination path. Complete
|
||||
`details.reported_limits.evidence_reference`; call `inspect_analysis_view` with
|
||||
that reference for a summary, one section/module, or a stable page, or call
|
||||
`export_evidence_bundle` with a destination path. Complete
|
||||
bundle exports stream canonical JSON into an atomically published file. A broad
|
||||
follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
|
||||
preserves the complete session without sending it through a single MCP frame.
|
||||
@@ -212,6 +213,18 @@ Ordinary responses keep their existing complete result contract.
|
||||
|
||||
## Retained application Evidence inputs
|
||||
|
||||
`inspect_analysis_view` projects a caller-selected view of already completed
|
||||
`inspect_binary_layout` or `analyze_javascript_application` Evidence. Source is
|
||||
an exact same-session retained reference or portable inline Evidence. Views are
|
||||
a summary, a layout mitigations or linkage facet, one section/symbol/module, or
|
||||
a stable page with a caller-selected positive `limit`. Module pages include
|
||||
JavaScript assets, bundled modules, and source modules. Select an exact
|
||||
`node_id` when a path is ambiguous or unavailable. Module items retain their
|
||||
recorded property values and source locations; summaries include parent
|
||||
application and semantic coverage. The result carries a digest of the selected
|
||||
facts. Actual serialized size determines MCP transport admission: reduce the
|
||||
page size or export retained Evidence if the view is too large.
|
||||
|
||||
`trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes` accept complete inline application Evidence
|
||||
|
||||
+3
-1
@@ -27,7 +27,9 @@ prompt argument or tool explicitly accepts one.
|
||||
|
||||
For a JavaScript/Electron application directory, `investigate_feature` points
|
||||
directly to `analyze_javascript_application` with `input_path`; it does not
|
||||
require `open_binary`. The CLI equivalent is `rea analyze <directory>`.
|
||||
require `open_binary`. When that Evidence, or `inspect_binary_layout` Evidence,
|
||||
is already retained, it suggests `inspect_analysis_view` before tracing a
|
||||
feature seed. The CLI equivalent is `rea analyze <directory>`.
|
||||
`open_binary` and the doctor target check admit files and macOS app bundles.
|
||||
A directory outside that opening route is reported with an available analysis
|
||||
action; this does not establish support for every directory format.
|
||||
|
||||
@@ -75,6 +75,7 @@ export const toolFamilyCatalog = (sources) => {
|
||||
surface: "application-workflow",
|
||||
contracts: [
|
||||
...sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
|
||||
...sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
...sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
|
||||
...sources.javascriptRecoveryContracts
|
||||
.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
|
||||
@@ -246,6 +247,10 @@ export const providerCatalog = (sources) => {
|
||||
name !== "project_apple_application_graph",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.ANALYSIS_VIEW_PROVIDER,
|
||||
contracts: sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
|
||||
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
|
||||
|
||||
@@ -64,6 +64,7 @@ const SOURCE_PATHS = {
|
||||
javascriptRuntimeObservationContracts:
|
||||
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
|
||||
applicationContracts: "dist/contracts/applicationToolContracts.js",
|
||||
analysisViewContracts: "dist/contracts/analysisViewToolContracts.js",
|
||||
webScriptContracts: "dist/contracts/webScriptToolContracts.js",
|
||||
javascriptRecoveryContracts:
|
||||
"dist/contracts/javascript/javascriptRecoveryToolContracts.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "33"
|
||||
version: "34"
|
||||
---
|
||||
|
||||
# REA
|
||||
@@ -146,12 +146,13 @@ Start with the default result and use its inline Evidence and graph context.
|
||||
Do not repeat an identical tool call. Make a focused follow-up only when the
|
||||
returned result leaves a specific question unanswered.
|
||||
|
||||
If MCP reports `resource_constraint` with `details.resource: "transport"`, use
|
||||
its reported same-session Evidence reference with a focused application tool,
|
||||
or call `export_evidence_bundle` to write the complete session to a selected
|
||||
path. The analysis remains complete in the ledger; avoid repeating it merely
|
||||
to request the same oversized response. Complete CLI JSON output also streams
|
||||
without a single MCP frame.
|
||||
If MCP reports `resource_constraint` with `details.resource: "transport"`, call
|
||||
`inspect_analysis_view` with its reported same-session Evidence reference for a
|
||||
summary, one section/module, or a stable page. Use `trace_application_feature`
|
||||
once a seed is known, or call `export_evidence_bundle` to write the complete
|
||||
session to a selected path. The analysis remains complete in the ledger; avoid
|
||||
repeating it merely to request the same oversized response. Complete CLI JSON
|
||||
output also streams without a single MCP frame.
|
||||
|
||||
Every conclusion must distinguish observations, inferences, and unknowns. Cite
|
||||
Evidence IDs, preserve limitations and incomplete coverage, and never imply
|
||||
|
||||
@@ -5,7 +5,10 @@ Evidence ID when another tool or your explanation needs a stable reference, and
|
||||
preserve authority, limitations, coverage, and residual unknowns. If a result
|
||||
is incomplete or paginated, continue only when the remaining data matters to
|
||||
the task; do not fetch a bundle or resource merely to read a result already
|
||||
returned inline.
|
||||
returned inline. When complete `inspect_binary_layout` or
|
||||
`analyze_javascript_application` Evidence is already retained, call
|
||||
`inspect_analysis_view` for a summary, one section/symbol/module, or a stable
|
||||
page instead of repeating the producer.
|
||||
|
||||
Use `record_unknown` to track unresolved questions in the user's investigation
|
||||
and name the authority or environment still required. Supply supporting and
|
||||
|
||||
@@ -8,10 +8,12 @@ only when a specific question remains unanswered.
|
||||
|
||||
An oversized response reports `resource_constraint` and
|
||||
`details.resource: "transport"`. Reuse
|
||||
`details.reported_limits.evidence_reference` with `trace_application_feature`
|
||||
for the module or feature under investigation. `export_evidence_bundle` writes
|
||||
the complete canonical session to a caller-selected file. Both workflows keep
|
||||
the original Evidence and coverage; a broad follow-up can also exceed framing.
|
||||
`details.reported_limits.evidence_reference` with `inspect_analysis_view` for a
|
||||
summary, one module, or a stable page of module identities. Use
|
||||
`trace_application_feature` once a module, route, or string seed is known.
|
||||
`export_evidence_bundle` writes the complete canonical session to a
|
||||
caller-selected file. These workflows keep the original Evidence and coverage;
|
||||
a broad follow-up can also exceed framing.
|
||||
|
||||
BrowserWindow preferences, preload and contextBridge surfaces, IPC
|
||||
registrations, utility processes, and native binding requests are static syntax
|
||||
@@ -50,7 +52,9 @@ through the available browser, Electron, or process workflows.
|
||||
When advertised by the connected server, application trace and compare tools
|
||||
accept complete inline Evidence or
|
||||
`{"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`
|
||||
for their application input (`application`, or `left`/`right`). This notation is
|
||||
for their application input (`application`, or `left`/`right`). `inspect_analysis_view`
|
||||
uses the same retained-reference form in `source`, or portable inline Evidence,
|
||||
to project a summary, module page, or one module with its recorded observations. This notation is
|
||||
a template: replace it with the actual returned ID. Versions before 4.1.0 accept only full
|
||||
inline Evidence. Use the exact ID
|
||||
returned by the producer on the same MCP connection. Resolution does not run
|
||||
|
||||
@@ -16,6 +16,15 @@ Requested unavailable context fails with setup guidance. See
|
||||
[recorded crashes](https://github.com/morluto/rea/blob/main/docs/recorded-crashes.md)
|
||||
for exact upstream profiles, bounds and verification coverage.
|
||||
|
||||
## Offline ELF layout
|
||||
|
||||
Use `inspect_binary_layout` with an explicit local ELF path for file-backed
|
||||
sections, symbols, relocations, linkage names, and static mitigation inferences
|
||||
without opening a disassembler target. When that complete record is already
|
||||
retained, call `inspect_analysis_view` for a summary, mitigations or linkage
|
||||
facet, one section or symbol, or a stable page. Do not re-run the decoder to
|
||||
read a single object.
|
||||
|
||||
## Native targets
|
||||
|
||||
After `open_binary`, use focused search, procedure, or function tools directly.
|
||||
|
||||
@@ -529,5 +529,9 @@ describe("optional JavaScript recovery availability", () => {
|
||||
expect(entry("analyze_javascript_application", status())).toMatchObject({
|
||||
available: true,
|
||||
});
|
||||
expect(entry("inspect_analysis_view", status())).toMatchObject({
|
||||
available: true,
|
||||
surface: "application",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -77,6 +77,13 @@ export const JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER = {
|
||||
version: "1",
|
||||
} as const;
|
||||
|
||||
/** Provider identity for selected views of retained analysis Evidence. */
|
||||
export const ANALYSIS_VIEW_PROVIDER = {
|
||||
id: "rea-analysis-view",
|
||||
name: "REA analysis view projection",
|
||||
version: "1",
|
||||
} as const;
|
||||
|
||||
/** Provider identity for graph-native feature tracing and version comparison. */
|
||||
export const JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER = {
|
||||
id: "rea-javascript-application-workflows",
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
analysisViewJavaScriptAnalysisWithSource,
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewLayoutEvidence,
|
||||
} from "../../../tests/fixtures/analysisView.js";
|
||||
import { inspectAnalysisView } from "./AnalysisViewService.js";
|
||||
|
||||
it("projects inline layout Evidence and retains a derived view record", () => {
|
||||
const parent = analysisViewLayoutEvidence();
|
||||
const result = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { index: 2 },
|
||||
},
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const evidence = parseEvidence(result.value);
|
||||
expect(evidence.operation).toBe("inspect_analysis_view");
|
||||
expect(evidence.predicate_type).toBe("rea.analysis-view");
|
||||
expect(evidence.confidence).toBe("derived");
|
||||
expect(evidence.evidence_links).toEqual([parent.evidence_id]);
|
||||
expect(evidence.normalized_result).toMatchObject({
|
||||
kind: "item",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
item: { name: { display: ".data" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves same-session retained JavaScript Evidence without re-running analysis", () => {
|
||||
const parent = analysisViewJavaScriptEvidence(
|
||||
analysisViewJavaScriptAnalysisWithSource(),
|
||||
);
|
||||
const lookup = (evidenceId: string) =>
|
||||
evidenceId === parent.evidence_id ? parent : undefined;
|
||||
const summary = inspectAnalysisView(
|
||||
{
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
lookup,
|
||||
);
|
||||
if (!summary.ok) throw summary.error;
|
||||
expect(summary.value.normalized_result).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
parent_operation: "analyze_javascript_application",
|
||||
});
|
||||
expect(summary.value.normalized_result).not.toHaveProperty(
|
||||
"summary.semantic_graph",
|
||||
);
|
||||
const item = inspectAnalysisView(
|
||||
{
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
},
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "modules",
|
||||
selector: { path: "renderer.js" },
|
||||
},
|
||||
},
|
||||
lookup,
|
||||
);
|
||||
if (!item.ok) throw item.error;
|
||||
expect(JSON.stringify(item.value.normalized_result)).toContain(
|
||||
"export const secret = 1;",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects stale retained IDs and unsupported parent operations", () => {
|
||||
const missing = inspectAnalysisView(
|
||||
{
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.evidence_id,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
() => undefined,
|
||||
);
|
||||
expect(missing).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "EvidenceIntegrityError",
|
||||
evidenceId: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.evidence_id,
|
||||
},
|
||||
});
|
||||
const unsupported = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{
|
||||
operation: "inspect_recorded_crash",
|
||||
parameters: {},
|
||||
result: { path: "/artifacts/crash.core" },
|
||||
},
|
||||
);
|
||||
const rejected = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: unsupported },
|
||||
view: { kind: "summary" },
|
||||
});
|
||||
expect(rejected).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisUnsupportedTargetError" },
|
||||
});
|
||||
expect(
|
||||
inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: analysisViewLayoutEvidence() },
|
||||
view: { kind: "summary", approval: true },
|
||||
}),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an absent parent subject instead of guessing its format", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysisWithSource();
|
||||
const original = analysisViewJavaScriptEvidence(analysis);
|
||||
const parent = createEvidence(undefined, original.provider, {
|
||||
operation: original.operation,
|
||||
parameters: {},
|
||||
result: original.normalized_result,
|
||||
limitations: original.limitations,
|
||||
});
|
||||
const result = inspectAnalysisView(
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
() => parent,
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.subject).toBeNull();
|
||||
expect(result.value.locations).toEqual([
|
||||
{ kind: "artifact-path", path: analysis.input_path },
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects contradictory subject and analysis artifact digests", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysisWithSource();
|
||||
const original = analysisViewJavaScriptEvidence(analysis);
|
||||
const parent = createEvidence(
|
||||
{
|
||||
path: analysis.input_path,
|
||||
format: analysis.format,
|
||||
sha256: "d".repeat(64),
|
||||
},
|
||||
original.provider,
|
||||
{
|
||||
operation: original.operation,
|
||||
parameters: {},
|
||||
result: original.normalized_result,
|
||||
},
|
||||
);
|
||||
const result = inspectAnalysisView(
|
||||
{
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
() => parent,
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "EvidenceIntegrityError" },
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js";
|
||||
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
|
||||
import { resolveEvidenceInput } from "../EvidenceInputResolver.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import {
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
inspectAnalysisViewInputSchema,
|
||||
projectAnalysisView,
|
||||
type AnalysisViewResult,
|
||||
type InspectAnalysisViewInput,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
|
||||
/** Authenticate a selected-view request and project already completed Evidence. */
|
||||
export const inspectAnalysisView = (
|
||||
rawInput: unknown,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
const parsed = inspectAnalysisViewInputSchema.safeParse(rawInput);
|
||||
if (!parsed.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
parsed.error.issues,
|
||||
rawInput,
|
||||
{ cause: parsed.error },
|
||||
),
|
||||
);
|
||||
return inspectAnalysisViewValidated(parsed.data, lookup);
|
||||
};
|
||||
|
||||
/** Project a view from input already parsed by a trusted adapter. */
|
||||
export const inspectAnalysisViewValidated = (
|
||||
input: InspectAnalysisViewInput,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
const resolved = resolveAnalysisViewSource(input.source, lookup);
|
||||
if (!resolved.ok) return resolved;
|
||||
const parent = resolved.value;
|
||||
const projected = projectAnalysisView(
|
||||
{
|
||||
evidenceId: parent.evidence_id,
|
||||
operation: parent.operation,
|
||||
normalizedResult: parent.normalized_result,
|
||||
limitations: parent.limitations,
|
||||
},
|
||||
input.view,
|
||||
);
|
||||
if (!projected.ok) return projected;
|
||||
if (
|
||||
parent.subject !== null &&
|
||||
parent.subject.digest.sha256 !== projected.value.artifact.sha256
|
||||
)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
`Parent subject SHA-256 ${parent.subject.digest.sha256} differs from analysis artifact SHA-256 ${projected.value.artifact.sha256}.`,
|
||||
),
|
||||
);
|
||||
return ok(createAnalysisViewEvidence(input, parent, projected.value));
|
||||
};
|
||||
|
||||
const resolveAnalysisViewSource = (
|
||||
source: InspectAnalysisViewInput["source"],
|
||||
lookup: EvidenceLookup | undefined,
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
if (source.kind === "retained-evidence")
|
||||
return resolveEvidenceInput(source, lookup);
|
||||
try {
|
||||
return ok(parseEvidence(source.evidence));
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof z.ZodError)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(
|
||||
INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
cause.issues,
|
||||
source.evidence,
|
||||
{ cause },
|
||||
),
|
||||
);
|
||||
if (cause instanceof TypeError)
|
||||
return err(
|
||||
new EvidenceIntegrityError(
|
||||
cause.message,
|
||||
cause.message.length > 0 ? { userMessage: cause.message } : undefined,
|
||||
),
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
|
||||
const createAnalysisViewEvidence = (
|
||||
input: InspectAnalysisViewInput,
|
||||
parent: Evidence,
|
||||
result: AnalysisViewResult,
|
||||
): Evidence => {
|
||||
const target =
|
||||
parent.subject === null
|
||||
? undefined
|
||||
: {
|
||||
path: parent.subject.local_path,
|
||||
sha256: parent.subject.digest.sha256,
|
||||
format: parent.subject.format,
|
||||
...(parent.subject.architecture === null
|
||||
? {}
|
||||
: { architecture: parent.subject.architecture }),
|
||||
};
|
||||
return createEvidence(target, ANALYSIS_VIEW_PROVIDER, {
|
||||
predicateType: "rea.analysis-view",
|
||||
operation: INSPECT_ANALYSIS_VIEW_OPERATION,
|
||||
parameters: {
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
view: jsonValueSchema.parse(input.view),
|
||||
},
|
||||
result: jsonValueSchema.parse(result),
|
||||
rawResult: null,
|
||||
confidence: "derived",
|
||||
authority: parent.authority,
|
||||
limitations: result.limitations,
|
||||
locations: [{ kind: "artifact-path", path: result.artifact.path }],
|
||||
evidenceLinks: [parent.evidence_id],
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { registerEvmCommands } from "./cli/evmCommands.js";
|
||||
import { registerAnalysisViewCommands } from "./cli/analysisViewCommands.js";
|
||||
import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js";
|
||||
import { Cli } from "incur";
|
||||
|
||||
@@ -65,6 +66,7 @@ export const createCli = (
|
||||
registerAndroidCommands(cli, logger, environment);
|
||||
registerFirmwareCommands(cli, logger, environment);
|
||||
registerBinaryDiagnosticsCommands(cli, logger, environment);
|
||||
registerAnalysisViewCommands(cli, logger);
|
||||
registerEvidenceCommands(cli, logger);
|
||||
registerProcessCommands(cli, logger, environment);
|
||||
registerBrowserCommands(cli, logger);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { z } from "incur";
|
||||
|
||||
import { inspectAnalysisViewValidated } from "../application/analysisView/AnalysisViewService.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisView.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import type { CliInstance } from "./types.js";
|
||||
|
||||
/** Project selected views of completed analysis Evidence through the CLI. */
|
||||
export const registerAnalysisViewCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectAnalysisView, {
|
||||
description:
|
||||
"Project a selected view of completed layout or JavaScript application Evidence JSON",
|
||||
args: z.object({
|
||||
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectAnalysisView, async () => {
|
||||
const input = await parseCliJsonInput(
|
||||
args.inputJson,
|
||||
CLI_COMMANDS.inspectAnalysisView,
|
||||
);
|
||||
if (!input.ok) return input.error;
|
||||
const parsed = inspectAnalysisViewInputSchema.safeParse(input.value);
|
||||
if (!parsed.success)
|
||||
return {
|
||||
error: "Application workflow failed",
|
||||
...projectAnalysisError(
|
||||
analysisInputErrorFromIssues(
|
||||
CLI_COMMANDS.inspectAnalysisView,
|
||||
parsed.error.issues,
|
||||
input.value,
|
||||
),
|
||||
),
|
||||
};
|
||||
const result = inspectAnalysisViewValidated(parsed.data);
|
||||
return result.ok
|
||||
? jsonValueSchema.parse(result.value)
|
||||
: {
|
||||
error: "Application workflow failed",
|
||||
...projectAnalysisError(result.error),
|
||||
};
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -41,6 +41,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectManagedArtifact: "inspect-managed-artifact",
|
||||
inspectRecordedCrash: "inspect-recorded-crash",
|
||||
inspectBinaryLayout: "inspect-binary-layout",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
inspectFirmwareRegions: "inspect-firmware-regions",
|
||||
extractFirmware: "extract-firmware",
|
||||
inspectAndroidPackage: "inspect-android-package",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
inspectAnalysisViewInputSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import type { ToolContract } from "./toolContractTypes.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { evidenceResultOf } from "./toolOutputSchemas.js";
|
||||
|
||||
const RETAINED_EXAMPLE_ID = `ev_${"a".repeat(64)}`;
|
||||
|
||||
/** Selected views of already completed layout and JavaScript analysis Evidence. */
|
||||
export const ANALYSIS_VIEW_TOOL_CONTRACTS = [
|
||||
{
|
||||
name: "inspect_analysis_view",
|
||||
...toolContractMetadata("inspect_analysis_view"),
|
||||
kind: "application",
|
||||
description:
|
||||
"Inspect a selected view of completed binary-layout or JavaScript application Evidence without repeating analysis. Use an exact same-session retained evidence_id or portable inline Evidence. Select a summary, layout mitigations/linkage facet, one section/symbol/module, or a stable page with a positive limit. Module pages contain JavaScript assets, bundled modules and source modules; use node_id when paths are ambiguous or unavailable. Returns selected facts inline with artifact identity, parent Evidence ID, a distinct view digest, coverage, limitations and unknowns. Actual serialized size determines MCP transport admission; choose a smaller page or export the retained Evidence when needed.",
|
||||
inputSchema: inspectAnalysisViewInputSchema,
|
||||
outputSchema: evidenceResultOf(analysisViewResultSchema),
|
||||
examples: [
|
||||
{
|
||||
title: "Summarize retained JavaScript application Evidence",
|
||||
input: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: RETAINED_EXAMPLE_ID,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Read one ELF section from retained layout Evidence",
|
||||
input: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: RETAINED_EXAMPLE_ID,
|
||||
},
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: ".text" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Page module identities from retained application Evidence",
|
||||
input: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: RETAINED_EXAMPLE_ID,
|
||||
},
|
||||
view: {
|
||||
kind: "page",
|
||||
collection: "modules",
|
||||
offset: 0,
|
||||
limit: 32,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly ToolContract[];
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
|
||||
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
|
||||
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
@@ -72,6 +73,7 @@ describe("tool contract surface", () => {
|
||||
...ENHANCED_TOOL_CONTRACTS,
|
||||
...NATIVE_TOOL_CONTRACTS,
|
||||
...BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
|
||||
...ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
...ARTIFACT_TOOL_CONTRACTS,
|
||||
...EVM_TOOL_CONTRACTS,
|
||||
...ANDROID_TOOL_CONTRACTS,
|
||||
|
||||
@@ -131,9 +131,9 @@ export const PROMPT_CONTRACTS = [
|
||||
"Search relevant strings or procedures and analyze the matching procedures; pass names or addresses directly when already known.",
|
||||
},
|
||||
{
|
||||
tools: ["trace_application_feature"],
|
||||
tools: ["inspect_analysis_view", "trace_application_feature"],
|
||||
instruction:
|
||||
"When application-graph Evidence is relevant, trace a route, API, channel, module, string, or native export across layers.",
|
||||
"When inspect_binary_layout or analyze_javascript_application Evidence is already retained, call inspect_analysis_view with that evidence_id for a summary, one section/symbol/module, or a stable page. Do not re-run the producer. Use trace_application_feature once a route, API, channel, module, string, or native export seed is known.",
|
||||
},
|
||||
{
|
||||
tools: [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
|
||||
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
|
||||
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
@@ -26,6 +27,7 @@ const GROUPS = {
|
||||
enhanced: ENHANCED_TOOL_CONTRACTS,
|
||||
native: NATIVE_TOOL_CONTRACTS,
|
||||
binary_diagnostics: BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
|
||||
analysis_view: ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
artifact: ARTIFACT_TOOL_CONTRACTS,
|
||||
evm: EVM_TOOL_CONTRACTS,
|
||||
managed: MANAGED_TOOL_CONTRACTS,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
|
||||
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
|
||||
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
|
||||
import { NATIVE_TOOL_CONTRACTS } from "./native/nativeToolContracts.js";
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
|
||||
@@ -26,6 +27,7 @@ export const TOOL_CONTRACTS = [
|
||||
...ENHANCED_TOOL_CONTRACTS,
|
||||
...NATIVE_TOOL_CONTRACTS,
|
||||
...BINARY_DIAGNOSTICS_TOOL_CONTRACTS,
|
||||
...ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
...ARTIFACT_TOOL_CONTRACTS,
|
||||
...EVM_TOOL_CONTRACTS,
|
||||
...MANAGED_TOOL_CONTRACTS,
|
||||
|
||||
@@ -80,6 +80,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
writesFilesystem: true,
|
||||
launchesProcess: true,
|
||||
}),
|
||||
inspect_analysis_view: evidence,
|
||||
inspect_firmware_regions: effects({
|
||||
mutatesSession: true,
|
||||
launchesProcess: true,
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
import {
|
||||
analysisViewBindJavaScriptGraphs,
|
||||
analysisViewJavaScriptAnalysis,
|
||||
analysisViewJavaScriptAnalysisWithSource,
|
||||
analysisViewLayoutEvidence,
|
||||
analysisViewLayoutFixture,
|
||||
} from "../../../tests/fixtures/analysisView.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
createImmutableJavaScriptApplicationGraphSteps,
|
||||
} from "../javascript/javascriptApplicationGraph.js";
|
||||
import {
|
||||
createJavaScriptSemanticGraph,
|
||||
createImmutableJavaScriptSemanticGraphSteps,
|
||||
} from "../javascript/javascriptSemanticGraph.js";
|
||||
import {
|
||||
completeWithinViewCoverage,
|
||||
inspectAnalysisViewInputSchema,
|
||||
pageViewCoverage,
|
||||
projectAnalysisView,
|
||||
type AnalysisViewParent,
|
||||
} from "./analysisView.js";
|
||||
import { javascriptModulePath } from "./javascriptApplicationView.js";
|
||||
|
||||
const layoutParent = (): AnalysisViewParent => {
|
||||
const evidence = analysisViewLayoutEvidence();
|
||||
return {
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: evidence.normalized_result,
|
||||
limitations: evidence.limitations,
|
||||
};
|
||||
};
|
||||
|
||||
const javascriptParent = (
|
||||
analysis = analysisViewJavaScriptAnalysis(),
|
||||
): AnalysisViewParent => ({
|
||||
evidenceId: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.evidence_id,
|
||||
operation: "analyze_javascript_application",
|
||||
normalizedResult: analysis,
|
||||
limitations: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.limitations,
|
||||
});
|
||||
|
||||
it("accepts caller-selected page sizes and rejects malformed bounds", () => {
|
||||
const input = {
|
||||
source: { kind: "retained-evidence", evidence_id: `ev_${"a".repeat(64)}` },
|
||||
view: { kind: "page", collection: "sections", offset: 0, limit: 500 },
|
||||
};
|
||||
expect(inspectAnalysisViewInputSchema.safeParse(input).success).toBe(true);
|
||||
for (const limit of [0, -1, 0.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
|
||||
expect(
|
||||
inspectAnalysisViewInputSchema.safeParse({
|
||||
...input,
|
||||
view: { ...input.view, limit },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("projects layout summary, facet, item, and stable pages", () => {
|
||||
const parent = layoutParent();
|
||||
const layout = analysisViewLayoutFixture();
|
||||
const summary = projectAnalysisView(parent, { kind: "summary" });
|
||||
if (!summary.ok) throw summary.error;
|
||||
expect(summary.value).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_operation: "inspect_binary_layout",
|
||||
summary: {
|
||||
format: "elf",
|
||||
counts: { sections: 3, symbols: 3, segments: 0, relocations: 0 },
|
||||
},
|
||||
coverage: completeWithinViewCoverage(6, 6),
|
||||
});
|
||||
const mitigations = projectAnalysisView(parent, {
|
||||
kind: "facet",
|
||||
facet: "mitigations",
|
||||
});
|
||||
if (!mitigations.ok) throw mitigations.error;
|
||||
if (mitigations.value.kind !== "facet")
|
||||
throw new Error("expected facet view");
|
||||
expect(mitigations.value.facet).toEqual(layout.mitigations);
|
||||
const data = projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: ".data" },
|
||||
});
|
||||
if (!data.ok) throw data.error;
|
||||
if (data.value.kind !== "item") throw new Error("expected item view");
|
||||
expect(data.value.item).toMatchObject({
|
||||
index: 2,
|
||||
name: { display: ".data" },
|
||||
});
|
||||
const indexed = projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { index: 0 },
|
||||
});
|
||||
if (!indexed.ok) throw indexed.error;
|
||||
expect(indexed.value.view_digest).not.toBe(data.value.view_digest);
|
||||
expect(indexed.value.view_digest).not.toBe(parent.evidenceId.slice(3));
|
||||
const page = projectAnalysisView(parent, {
|
||||
kind: "page",
|
||||
collection: "sections",
|
||||
offset: 0,
|
||||
limit: 2,
|
||||
});
|
||||
if (!page.ok) throw page.error;
|
||||
expect(page.value).toMatchObject({
|
||||
kind: "page",
|
||||
coverage: {
|
||||
status: "page",
|
||||
examined: 2,
|
||||
total: 3,
|
||||
next_offset: 2,
|
||||
exhausted: false,
|
||||
},
|
||||
});
|
||||
if (page.value.kind !== "page") throw new Error("expected page view");
|
||||
expect(page.value.items).toHaveLength(2);
|
||||
const exhausted = projectAnalysisView(parent, {
|
||||
kind: "page",
|
||||
collection: "symbols",
|
||||
offset: 3,
|
||||
limit: 8,
|
||||
});
|
||||
if (!exhausted.ok) throw exhausted.error;
|
||||
expect(exhausted.value).toMatchObject({
|
||||
items: [],
|
||||
coverage: pageViewCoverage(3, 0, 3),
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects ambiguous layout names with candidate indexes", () => {
|
||||
const parent = layoutParent();
|
||||
const result = projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: ".text" },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [
|
||||
{
|
||||
path: ["view", "selector", "name"],
|
||||
expected: [{ index: 0 }, { index: 1 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("projects JavaScript summary and preserves selected module observations", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysisWithSource();
|
||||
const parent = javascriptParent(analysis);
|
||||
const summary = projectAnalysisView(parent, { kind: "summary" });
|
||||
if (!summary.ok) throw summary.error;
|
||||
if (summary.value.kind !== "summary") throw new Error("expected summary");
|
||||
expect(summary.value.summary).toMatchObject({
|
||||
format: "directory",
|
||||
limitation_count: 0,
|
||||
});
|
||||
expect(JSON.stringify(summary.value)).not.toMatch(/"graph"/);
|
||||
expect(summary.value.summary).not.toHaveProperty("semantic_graph");
|
||||
const item = projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "modules",
|
||||
selector: { path: "renderer.js" },
|
||||
});
|
||||
if (!item.ok) throw item.error;
|
||||
const moduleNode = analysis.graph.nodes[0];
|
||||
if (moduleNode === undefined) throw new Error("missing module");
|
||||
expect(javascriptModulePath(moduleNode)).toBe("renderer.js");
|
||||
if (item.value.kind !== "item") throw new Error("expected item view");
|
||||
expect(item.value.item).toMatchObject({
|
||||
path: "renderer.js",
|
||||
kind: "javascript-asset",
|
||||
});
|
||||
expect(JSON.stringify(item.value.item)).toContain("export const secret = 1;");
|
||||
expect(JSON.stringify(item.value.item)).toContain("should not leak");
|
||||
const page = projectAnalysisView(parent, {
|
||||
kind: "page",
|
||||
collection: "modules",
|
||||
offset: 0,
|
||||
limit: 8,
|
||||
});
|
||||
if (!page.ok) throw page.error;
|
||||
if (page.value.kind !== "page") throw new Error("expected page view");
|
||||
expect(page.value.items).toEqual([
|
||||
{
|
||||
node_id: analysis.graph.nodes[0]?.node_id,
|
||||
kind: "javascript-asset",
|
||||
path: "renderer.js",
|
||||
},
|
||||
]);
|
||||
expect(page.value.coverage).toMatchObject({
|
||||
status: "page",
|
||||
examined: 1,
|
||||
total: 1,
|
||||
next_offset: null,
|
||||
exhausted: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects ambiguous JavaScript module paths with candidate node ids", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const first = analysis.graph.nodes[0];
|
||||
if (first === undefined) throw new Error("missing module");
|
||||
const duplicate = createJavaScriptApplicationNode({
|
||||
kind: first.kind,
|
||||
identity: {
|
||||
strategy: "content-digest",
|
||||
stability: "global-exact",
|
||||
sha256: "c".repeat(64),
|
||||
},
|
||||
observations: first.observations.map((observation) => ({
|
||||
label: observation.label,
|
||||
properties: observation.properties,
|
||||
evidence: {
|
||||
...observation.evidence,
|
||||
artifact: {
|
||||
...observation.evidence.artifact,
|
||||
sha256: "c".repeat(64),
|
||||
artifact_id: `art_${"c".repeat(64)}`,
|
||||
},
|
||||
},
|
||||
})),
|
||||
});
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [first.node_id, duplicate.node_id],
|
||||
nodes: [first, duplicate],
|
||||
edges: [],
|
||||
coverage: analysis.graph.coverage,
|
||||
limitations: analysis.graph.limitations,
|
||||
});
|
||||
const parent = javascriptParent(
|
||||
analysisViewBindJavaScriptGraphs(analysis, graph),
|
||||
);
|
||||
const result = projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "modules",
|
||||
selector: { path: "renderer.js" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("expected ambiguous path");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisInputError",
|
||||
issues: [
|
||||
{
|
||||
path: ["view", "selector", "path"],
|
||||
expected: expect.arrayContaining([
|
||||
{ node_id: first.node_id },
|
||||
{ node_id: duplicate.node_id },
|
||||
]),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("pages actual modules, including unknown and empty paths, without unrelated roles", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const original = analysis.graph.nodes[0];
|
||||
if (original === undefined) throw new Error("missing module");
|
||||
const nodes = [
|
||||
original,
|
||||
...(
|
||||
[
|
||||
"asar-entry",
|
||||
"electron-renderer",
|
||||
"javascript-module",
|
||||
"source-module",
|
||||
] as const
|
||||
).map((kind) =>
|
||||
createJavaScriptApplicationNode({
|
||||
kind,
|
||||
identity:
|
||||
kind === "source-module"
|
||||
? {
|
||||
strategy: "source-map-original",
|
||||
stability: "source-map-exact",
|
||||
original_source: "",
|
||||
source_map_sha256: "c".repeat(64),
|
||||
source_sha256: null,
|
||||
}
|
||||
: kind === "javascript-module"
|
||||
? {
|
||||
strategy: "artifact-local-key",
|
||||
stability: "artifact-version",
|
||||
artifact_sha256: "c".repeat(64),
|
||||
namespace: kind,
|
||||
key: "module",
|
||||
}
|
||||
: original.identity,
|
||||
observations: original.observations.map((observation) => ({
|
||||
label: observation.label,
|
||||
properties: {},
|
||||
evidence:
|
||||
kind === "javascript-module" || kind === "source-module"
|
||||
? {
|
||||
...observation.evidence,
|
||||
artifact: {
|
||||
...observation.evidence.artifact,
|
||||
sha256: "c".repeat(64),
|
||||
artifact_id: `art_${"c".repeat(64)}`,
|
||||
},
|
||||
...(kind === "javascript-module"
|
||||
? {
|
||||
state: "unknown",
|
||||
confidence: "unknown",
|
||||
limitations: ["Source location unavailable"],
|
||||
location: {
|
||||
available: false,
|
||||
reason: "unknown",
|
||||
detail: "Source location unavailable",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
: observation.evidence,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
];
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: nodes.map((node) => node.node_id),
|
||||
nodes,
|
||||
edges: [],
|
||||
coverage: analysis.graph.coverage,
|
||||
limitations: [],
|
||||
});
|
||||
const parent = javascriptParent(
|
||||
analysisViewBindJavaScriptGraphs(analysis, graph),
|
||||
);
|
||||
const page = projectAnalysisView(parent, {
|
||||
kind: "page",
|
||||
collection: "modules",
|
||||
offset: 0,
|
||||
limit: 1000,
|
||||
});
|
||||
if (!page.ok) throw page.error;
|
||||
expect(page.value).toMatchObject({
|
||||
coverage: { total: 3, examined: 3, exhausted: true },
|
||||
});
|
||||
if (page.value.kind !== "page") throw new Error("expected page");
|
||||
expect(page.value.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ kind: "javascript-module", path: null }),
|
||||
expect.objectContaining({ kind: "source-module", path: "" }),
|
||||
]),
|
||||
);
|
||||
const emptyPath = projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "modules",
|
||||
selector: { path: "" },
|
||||
});
|
||||
expect(emptyPath.ok).toBe(true);
|
||||
const role = nodes.find((node) => node.kind === "electron-renderer");
|
||||
if (role === undefined) throw new Error("missing role");
|
||||
expect(
|
||||
projectAnalysisView(parent, {
|
||||
kind: "item",
|
||||
collection: "modules",
|
||||
selector: { node_id: role.node_id },
|
||||
}).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
const complete = <Value>(steps: Generator<void, Value>): Value => {
|
||||
let step = steps.next();
|
||||
while (!step.done) step = steps.next();
|
||||
return step.value;
|
||||
};
|
||||
|
||||
it("reuses authenticated graphs while rejecting malformed metadata and imported companions", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const { graph_id: _applicationId, ...application } = analysis.graph;
|
||||
const graph = complete(
|
||||
createImmutableJavaScriptApplicationGraphSteps(application),
|
||||
);
|
||||
const { graph_id: _semanticId, ...semantic } = analysis.semantic_graph;
|
||||
const semanticGraph = complete(
|
||||
createImmutableJavaScriptSemanticGraphSteps({
|
||||
...semantic,
|
||||
application_graph_id: graph.graph_id,
|
||||
}),
|
||||
);
|
||||
const owned = { ...analysis, graph, semantic_graph: semanticGraph };
|
||||
expect(
|
||||
projectAnalysisView(javascriptParent(owned), { kind: "summary" }).ok,
|
||||
).toBe(true);
|
||||
for (const malformed of [
|
||||
{ ...owned, root_artifact_sha256: "d".repeat(64) },
|
||||
{
|
||||
...owned,
|
||||
statistics: { ...owned.statistics, parsed_javascript_files: -1 },
|
||||
},
|
||||
{
|
||||
...owned,
|
||||
semantic_graph: Object.freeze({
|
||||
...semanticGraph,
|
||||
application_graph_id: "jag_" + "d".repeat(64),
|
||||
}),
|
||||
},
|
||||
]) {
|
||||
const result = projectAnalysisView(
|
||||
{ ...javascriptParent(owned), normalizedResult: malformed },
|
||||
{ kind: "summary" },
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisInputError",
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
path: expect.arrayContaining(["source", "normalized_result"]),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("retains semantic coverage and limitations in a summary", () => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const { graph_id: _graphId, ...semantic } = analysis.semantic_graph;
|
||||
const semanticGraph = createJavaScriptSemanticGraph({
|
||||
...semantic,
|
||||
coverage: {
|
||||
...semantic.coverage,
|
||||
status: "partial",
|
||||
truncated: true,
|
||||
omitted_nodes: null,
|
||||
omitted_relations: null,
|
||||
limits: [
|
||||
{ name: "semantic_graph_node_ceiling", value: 100000, unit: "items" },
|
||||
],
|
||||
},
|
||||
limitations: ["Some semantic nodes could not be retained."],
|
||||
});
|
||||
const result = projectAnalysisView(
|
||||
javascriptParent({ ...analysis, semantic_graph: semanticGraph }),
|
||||
{ kind: "summary" },
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
summary: { coverage: { semantic: { status: "partial", truncated: true } } },
|
||||
limitations: ["Some semantic nodes could not be retained."],
|
||||
unknowns: expect.arrayContaining([
|
||||
"semantic_graph.coverage.status is partial",
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not match unavailable layout names using a display placeholder", () => {
|
||||
const layout = analysisViewLayoutFixture();
|
||||
const section = layout.sections[2];
|
||||
if (section === undefined) throw new Error("missing section");
|
||||
const result = projectAnalysisView(
|
||||
{
|
||||
...layoutParent(),
|
||||
normalizedResult: {
|
||||
...layout,
|
||||
sections: [
|
||||
...layout.sections.slice(0, 2),
|
||||
{
|
||||
...section,
|
||||
name: {
|
||||
...section.name,
|
||||
display: "<unavailable>",
|
||||
unknown_reason: "Name bytes unavailable",
|
||||
bytes_base64: null,
|
||||
location: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: "<unavailable>" },
|
||||
},
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects unsupported parent operations and incompatible collections", () => {
|
||||
const unsupported = projectAnalysisView(
|
||||
{
|
||||
evidenceId: `ev_${"d".repeat(64)}`,
|
||||
operation: "inspect_recorded_crash",
|
||||
normalizedResult: { path: "/artifacts/crash.core" },
|
||||
limitations: [],
|
||||
},
|
||||
{ kind: "summary" },
|
||||
);
|
||||
expect(unsupported).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisUnsupportedTargetError" },
|
||||
});
|
||||
const modulesOnLayout = projectAnalysisView(layoutParent(), {
|
||||
kind: "page",
|
||||
collection: "modules",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
expect(modulesOnLayout).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
const sectionsOnJs = projectAnalysisView(javascriptParent(), {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: ".text" },
|
||||
});
|
||||
expect(sectionsOnJs).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,341 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AnalysisInputError,
|
||||
type AnalysisInputIssue,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../analysisErrorCore.js";
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import { digestSchema, prefixedDigestSchema } from "../digests.js";
|
||||
import { evidenceSchema } 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 {
|
||||
parseOwnedJavaScriptApplicationAnalysisSteps,
|
||||
type JavaScriptApplicationAnalysisResult,
|
||||
} from "../javascript/javascriptApplicationAnalysis.js";
|
||||
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
|
||||
import { projectBinaryLayoutView } from "./binaryLayoutView.js";
|
||||
import { projectJavaScriptApplicationView } from "./javascriptApplicationView.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 itemSelectorSchema = z.union([
|
||||
z.strictObject({ index: z.number().int().nonnegative() }),
|
||||
z.strictObject({ name: z.string() }),
|
||||
z.strictObject({
|
||||
path: z
|
||||
.string()
|
||||
.describe(
|
||||
"Exact observed module path or original source-map reference from a module page; an empty reference is valid.",
|
||||
),
|
||||
}),
|
||||
z.strictObject({ node_id: prefixedDigestSchema("jag_node") }),
|
||||
]);
|
||||
const itemViewSchema = z
|
||||
.strictObject({
|
||||
kind: z.literal("item"),
|
||||
collection: z.enum(["sections", "symbols", "modules"]),
|
||||
selector: itemSelectorSchema,
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
const selector = value.selector;
|
||||
if (value.collection === "modules") {
|
||||
if (!("path" in selector) && !("node_id" in selector))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["selector"],
|
||||
message:
|
||||
"Module items are selected by exact path or node_id, not index or name.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!("index" in selector) && !("name" in selector))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["selector"],
|
||||
message:
|
||||
"Section and symbol items are selected by index or exact name, not path or node_id.",
|
||||
});
|
||||
});
|
||||
const pageViewSchema = z.strictObject({
|
||||
kind: z.literal("page"),
|
||||
collection: z.enum(["sections", "symbols", "modules"]),
|
||||
offset: z.number().int().nonnegative(),
|
||||
limit: z.number().int().positive(),
|
||||
});
|
||||
|
||||
/** Caller-selected projection of already completed analysis Evidence. */
|
||||
export const analysisViewRequestSchema = z.discriminatedUnion("kind", [
|
||||
summaryViewSchema,
|
||||
facetViewSchema,
|
||||
itemViewSchema,
|
||||
pageViewSchema,
|
||||
]);
|
||||
|
||||
/** Inline Evidence 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: evidenceSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
/** 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().min(1),
|
||||
sha256: digestSchema,
|
||||
});
|
||||
|
||||
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,
|
||||
statistics: jsonObjectSchema,
|
||||
electron: jsonObjectSchema,
|
||||
coverage: jsonObjectSchema,
|
||||
limitation_count: z.number().int().nonnegative(),
|
||||
});
|
||||
|
||||
const viewResultBase = {
|
||||
parent_evidence_id: prefixedDigestSchema("ev"),
|
||||
parent_operation: z.enum([
|
||||
"inspect_binary_layout",
|
||||
"analyze_javascript_application",
|
||||
]),
|
||||
parent_digest: digestSchema,
|
||||
view_digest: digestSchema,
|
||||
artifact: artifactSchema,
|
||||
coverage: coverageSchema,
|
||||
limitations: z.array(z.string()),
|
||||
unknowns: z.array(z.string()),
|
||||
} as const;
|
||||
|
||||
/** Projected facts for one selected view of retained analysis Evidence. */
|
||||
export const analysisViewResultSchema = z.discriminatedUnion("kind", [
|
||||
z.strictObject({
|
||||
kind: z.literal("summary"),
|
||||
view: summaryViewSchema,
|
||||
summary: z.union([layoutSummarySchema, javascriptSummarySchema]),
|
||||
...viewResultBase,
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
]);
|
||||
|
||||
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;
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
export {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
|
||||
/** 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);
|
||||
|
||||
/** SHA-256 of the canonical projected JSON, excluding this digest field. */
|
||||
export const analysisViewDigest = (
|
||||
projection: Omit<AnalysisViewResult, "view_digest">,
|
||||
): string => digestCanonicalValue(projection, "Analysis view");
|
||||
|
||||
/** View payload before the content digest is attached. */
|
||||
export type UnsignedAnalysisView =
|
||||
| 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">;
|
||||
|
||||
/** Attach the content digest that must change when projected bytes change. */
|
||||
export const sealAnalysisView = (
|
||||
projection: UnsignedAnalysisView,
|
||||
): AnalysisViewResult =>
|
||||
analysisViewResultSchema.parse({
|
||||
...projection,
|
||||
view_digest: analysisViewDigest(projection),
|
||||
});
|
||||
|
||||
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)} is not inspect_binary_layout or analyze_javascript_application.`,
|
||||
);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/** Project one selected view from authenticated parent analysis Evidence. */
|
||||
export const projectAnalysisView = (
|
||||
parent: AnalysisViewParent,
|
||||
view: AnalysisViewRequest,
|
||||
): 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 (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;
|
||||
}
|
||||
return err(unsupportedParent(parent));
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { AnalysisViewCoverage } from "./analysisView.js";
|
||||
|
||||
/** Coverage for a summary, facet, or single selected object. */
|
||||
export const completeWithinViewCoverage = (
|
||||
examined: number,
|
||||
total: number,
|
||||
): AnalysisViewCoverage => ({
|
||||
status: "complete-within-view",
|
||||
examined,
|
||||
total,
|
||||
next_offset: null,
|
||||
exhausted: true,
|
||||
});
|
||||
|
||||
/** Coverage for a stable page, including an offset past the collection. */
|
||||
export const pageViewCoverage = (
|
||||
offset: number,
|
||||
examined: number,
|
||||
total: number,
|
||||
): AnalysisViewCoverage => {
|
||||
const exhausted = offset >= total || offset + examined >= total;
|
||||
return {
|
||||
status: examined === 0 ? "empty" : "page",
|
||||
examined,
|
||||
total,
|
||||
next_offset: exhausted ? null : offset + examined,
|
||||
exhausted,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,239 @@
|
||||
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: AnalysisViewRequest,
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
const shared = parentFields(parent, layout);
|
||||
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.",
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,318 @@
|
||||
import {
|
||||
AnalysisInputError,
|
||||
type AnalysisInputIssue,
|
||||
} from "../analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../analysisErrorBase.js";
|
||||
import { uniqueSorted } from "../canonicalOrdering.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../jsonValue.js";
|
||||
import type { JavaScriptApplicationAnalysisResult } from "../javascript/javascriptApplicationAnalysis.js";
|
||||
import type { ApplicationNode } from "../javascript/javascriptApplicationGraphSchemas.js";
|
||||
import { err, ok, type Result } from "../result.js";
|
||||
import {
|
||||
completeWithinViewCoverage as completeCoverage,
|
||||
pageViewCoverage as pageCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import type {
|
||||
AnalysisViewParent,
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
|
||||
const OPERATION = "inspect_analysis_view";
|
||||
|
||||
const inputError = (
|
||||
issues: readonly AnalysisInputIssue[],
|
||||
): AnalysisInputError => new AnalysisInputError(OPERATION, undefined, issues);
|
||||
|
||||
const jsIncompatible = (detail: string): AnalysisError =>
|
||||
inputError([
|
||||
{
|
||||
path: ["view"],
|
||||
reason: "invalid_value",
|
||||
message: detail,
|
||||
},
|
||||
]);
|
||||
|
||||
/** Exact module path or original source-map reference, when available. */
|
||||
export const javascriptModulePath = (node: ApplicationNode): string | null => {
|
||||
if (node.identity.strategy === "canonical-path") return node.identity.path;
|
||||
if (node.identity.strategy === "source-map-original")
|
||||
return node.identity.original_source;
|
||||
for (const observation of node.observations) {
|
||||
const path = observation.properties.path;
|
||||
if (typeof path === "string") return path;
|
||||
const location = observation.evidence.location;
|
||||
if (location.available && location.value.kind === "artifact-path")
|
||||
return location.value.path;
|
||||
if (location.available && location.value.kind === "source-range")
|
||||
return location.value.source;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const moduleEntries = (
|
||||
nodes: readonly ApplicationNode[],
|
||||
): readonly {
|
||||
readonly node: ApplicationNode;
|
||||
readonly path: string | null;
|
||||
}[] =>
|
||||
nodes
|
||||
.filter(
|
||||
(node) =>
|
||||
node.kind === "javascript-asset" ||
|
||||
node.kind === "javascript-module" ||
|
||||
node.kind === "source-module",
|
||||
)
|
||||
.map((node) => ({ node, path: javascriptModulePath(node) }));
|
||||
|
||||
const exportNames = (node: ApplicationNode): readonly string[] => {
|
||||
const names = new Set<string>();
|
||||
for (const observation of node.observations) {
|
||||
const value =
|
||||
observation.properties.export_names ?? observation.properties.exports;
|
||||
if (!Array.isArray(value)) continue;
|
||||
for (const entry of value) if (typeof entry === "string") names.add(entry);
|
||||
}
|
||||
return [...names];
|
||||
};
|
||||
|
||||
const nodeHashes = (node: ApplicationNode): readonly string[] => {
|
||||
const hashes = new Set<string>();
|
||||
const { identity } = node;
|
||||
if (identity.strategy === "content-digest") hashes.add(identity.sha256);
|
||||
if (identity.strategy === "canonical-path")
|
||||
hashes.add(identity.artifact_sha256);
|
||||
if (identity.strategy === "source-map-original") {
|
||||
hashes.add(identity.source_map_sha256);
|
||||
if (identity.source_sha256 !== null) hashes.add(identity.source_sha256);
|
||||
}
|
||||
for (const observation of node.observations) {
|
||||
for (const key of [
|
||||
"sha256",
|
||||
"container_sha256",
|
||||
"entry_sha256",
|
||||
"source_sha256",
|
||||
] as const) {
|
||||
const value = observation.properties[key];
|
||||
if (typeof value === "string" && /^[a-f0-9]{64}$/u.test(value))
|
||||
hashes.add(value);
|
||||
}
|
||||
}
|
||||
return [...hashes];
|
||||
};
|
||||
|
||||
const sourceRanges = (node: ApplicationNode): readonly JsonValue[] =>
|
||||
node.observations.flatMap((observation) => {
|
||||
const location = observation.evidence.location;
|
||||
return location.available && location.value.kind === "source-range"
|
||||
? [location.value]
|
||||
: [];
|
||||
});
|
||||
|
||||
const moduleItem = (node: ApplicationNode): JsonValue => ({
|
||||
node_id: node.node_id,
|
||||
kind: node.kind,
|
||||
path: javascriptModulePath(node),
|
||||
identity: node.identity,
|
||||
hashes: [...nodeHashes(node)],
|
||||
exports: [...exportNames(node)],
|
||||
source_ranges: [...sourceRanges(node)],
|
||||
observations: node.observations.map((observation) => ({
|
||||
observation_id: observation.observation_id,
|
||||
label: observation.label,
|
||||
properties: observation.properties,
|
||||
evidence: {
|
||||
...observation.evidence,
|
||||
location: observation.evidence.location,
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
const applicationUnknownCount = (
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
): number =>
|
||||
analysis.graph.nodes.reduce(
|
||||
(count, node) => count + Number(node.kind === "unknown"),
|
||||
0,
|
||||
);
|
||||
|
||||
const javascriptUnknowns = (
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
): readonly string[] => {
|
||||
const unknowns = [
|
||||
...analysis.limitations,
|
||||
...analysis.graph.limitations,
|
||||
...analysis.semantic_graph.limitations,
|
||||
];
|
||||
if (analysis.graph.coverage.status !== "complete")
|
||||
unknowns.push(`graph.coverage.status is ${analysis.graph.coverage.status}`);
|
||||
if (analysis.semantic_graph.coverage.status !== "complete")
|
||||
unknowns.push(
|
||||
`semantic_graph.coverage.status is ${analysis.semantic_graph.coverage.status}`,
|
||||
);
|
||||
if (applicationUnknownCount(analysis) > 0)
|
||||
unknowns.push(
|
||||
`${String(applicationUnknownCount(analysis))} application graph unknowns remain in the parent Evidence.`,
|
||||
);
|
||||
if (analysis.semantic_graph.unknowns.length > 0)
|
||||
unknowns.push(
|
||||
`${String(analysis.semantic_graph.unknowns.length)} semantic graph unknowns remain in the parent Evidence.`,
|
||||
);
|
||||
return unknowns;
|
||||
};
|
||||
|
||||
const parentFields = (
|
||||
parent: AnalysisViewParent,
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
) => ({
|
||||
parent_evidence_id: parent.evidenceId,
|
||||
parent_operation: "analyze_javascript_application" as const,
|
||||
parent_digest: parent.evidenceId.slice(3),
|
||||
artifact: {
|
||||
path: analysis.input_path,
|
||||
sha256: analysis.root_artifact_sha256,
|
||||
},
|
||||
limitations: uniqueSorted([
|
||||
...parent.limitations,
|
||||
...analysis.limitations,
|
||||
...analysis.graph.limitations,
|
||||
...analysis.semantic_graph.limitations,
|
||||
]),
|
||||
unknowns: uniqueSorted([...javascriptUnknowns(analysis)]),
|
||||
});
|
||||
|
||||
const selectModule = (
|
||||
modules: ReturnType<typeof moduleEntries>,
|
||||
selector: Extract<AnalysisViewRequest, { readonly kind: "item" }>["selector"],
|
||||
): Result<ApplicationNode, AnalysisError> => {
|
||||
if ("node_id" in selector) {
|
||||
const matches = modules.filter(
|
||||
({ node }) => node.node_id === selector.node_id,
|
||||
);
|
||||
if (matches.length === 1) {
|
||||
const selected = matches[0];
|
||||
if (selected !== undefined) return ok(selected.node);
|
||||
}
|
||||
return err(
|
||||
inputError([
|
||||
{
|
||||
path: ["view", "selector", "node_id"],
|
||||
reason: "invalid_value",
|
||||
message: `No module has node_id ${JSON.stringify(selector.node_id)}. Page the modules collection to inspect available identities.`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
if ("path" in selector) {
|
||||
const matches = modules.filter((entry) => entry.path === selector.path);
|
||||
if (matches.length === 1) {
|
||||
const selected = matches[0];
|
||||
if (selected !== undefined) return ok(selected.node);
|
||||
}
|
||||
if (matches.length === 0)
|
||||
return err(
|
||||
inputError([
|
||||
{
|
||||
path: ["view", "selector", "path"],
|
||||
reason: "invalid_value",
|
||||
message: `No module has path ${JSON.stringify(selector.path)}. Page the modules collection to inspect exact paths and node_id values.`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
return err(
|
||||
inputError([
|
||||
{
|
||||
path: ["view", "selector", "path"],
|
||||
reason: "invalid_value",
|
||||
message: `Multiple modules have path ${JSON.stringify(selector.path)}; select one by node_id.`,
|
||||
expected: matches.map((entry) => ({ node_id: entry.node.node_id })),
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
return err(
|
||||
jsIncompatible("Module items are selected by exact path or node_id."),
|
||||
);
|
||||
};
|
||||
|
||||
/** Project one JavaScript application view without re-running reconstruction. */
|
||||
export const projectJavaScriptApplicationView = (
|
||||
parent: AnalysisViewParent,
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
view: AnalysisViewRequest,
|
||||
): Result<UnsignedAnalysisView, AnalysisError> => {
|
||||
const shared = parentFields(parent, analysis);
|
||||
const modules = moduleEntries(analysis.graph.nodes);
|
||||
if (view.kind === "summary")
|
||||
return ok({
|
||||
kind: "summary",
|
||||
view,
|
||||
summary: {
|
||||
input_path: analysis.input_path,
|
||||
format: analysis.format,
|
||||
root_artifact_sha256: analysis.root_artifact_sha256,
|
||||
statistics: jsonObjectSchema.parse(analysis.statistics),
|
||||
electron: jsonObjectSchema.parse(analysis.summary),
|
||||
coverage: jsonObjectSchema.parse({
|
||||
application: analysis.graph.coverage,
|
||||
semantic: {
|
||||
...analysis.semantic_graph.coverage,
|
||||
families: analysis.semantic_graph.coverage.families.map(
|
||||
({ unknown_ids, ...family }) => ({
|
||||
...family,
|
||||
unknown_count: unknown_ids.length,
|
||||
}),
|
||||
),
|
||||
},
|
||||
application_unknowns: applicationUnknownCount(analysis),
|
||||
semantic_unknowns: analysis.semantic_graph.unknowns.length,
|
||||
}),
|
||||
limitation_count: analysis.limitations.length,
|
||||
},
|
||||
coverage: completeCoverage(modules.length, analysis.graph.nodes.length),
|
||||
...shared,
|
||||
});
|
||||
if (view.kind === "facet")
|
||||
return err(
|
||||
jsIncompatible(
|
||||
"facet views apply to inspect_binary_layout Evidence (mitigations or linkage).",
|
||||
),
|
||||
);
|
||||
if (view.kind === "item") {
|
||||
if (view.collection !== "modules")
|
||||
return err(
|
||||
jsIncompatible(
|
||||
"Section and symbol collections apply to inspect_binary_layout Evidence.",
|
||||
),
|
||||
);
|
||||
const selected = selectModule(modules, view.selector);
|
||||
if (!selected.ok) return selected;
|
||||
return ok({
|
||||
kind: "item",
|
||||
view,
|
||||
item: moduleItem(selected.value),
|
||||
coverage: completeCoverage(1, modules.length),
|
||||
...shared,
|
||||
});
|
||||
}
|
||||
if (view.collection !== "modules")
|
||||
return err(
|
||||
jsIncompatible(
|
||||
"Section and symbol collections apply to inspect_binary_layout Evidence.",
|
||||
),
|
||||
);
|
||||
const items = modules
|
||||
.slice(view.offset, view.offset + view.limit)
|
||||
.map(({ node, path }) => ({
|
||||
node_id: node.node_id,
|
||||
kind: node.kind,
|
||||
path,
|
||||
}));
|
||||
return ok({
|
||||
kind: "page",
|
||||
view,
|
||||
items,
|
||||
coverage: pageCoverage(view.offset, items.length, modules.length),
|
||||
...shared,
|
||||
});
|
||||
};
|
||||
@@ -5,5 +5,5 @@ export const PACKAGE_METADATA = {
|
||||
serverSdkVersion: "2.3.1",
|
||||
clientSdkVersion: "2.3.1",
|
||||
coreSdkVersion: "2.3.1",
|
||||
skillVersion: "33",
|
||||
skillVersion: "34",
|
||||
} as const;
|
||||
@@ -4,6 +4,7 @@ import { registerEvmTools } from "./registerEvmTools.js";
|
||||
import { registerRecordedCrashTools } from "./registerRecordedCrashTools.js";
|
||||
import { createRecordedCrashService } from "../composition/binaryDiagnostics.js";
|
||||
import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js";
|
||||
import { registerAnalysisViewTool } from "./registerAnalysisViewTool.js";
|
||||
import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js";
|
||||
import { createBinaryLayoutService } from "../composition/binaryDiagnostics.js";
|
||||
import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js";
|
||||
@@ -221,6 +222,7 @@ export const createServer = (
|
||||
toolLogger,
|
||||
recordEvidence,
|
||||
);
|
||||
registerAnalysisViewTool(server, toolLogger, evidenceById, recordEvidence);
|
||||
registerEvmTools(
|
||||
server,
|
||||
options.evmInterface ?? createEvmInterfaceService(),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { inspectAnalysisView } from "../application/analysisView/AnalysisViewService.js";
|
||||
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
|
||||
import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { logToolExecution } from "./toolLogging.js";
|
||||
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
|
||||
import { toCallToolResult, toEvidenceToolResult } from "./toolResult.js";
|
||||
|
||||
/** Bind selected-view projection to its named contract and session Evidence owner. */
|
||||
export const registerAnalysisViewTool = (
|
||||
server: McpServer,
|
||||
logger: Logger,
|
||||
evidenceById?: EvidenceLookup,
|
||||
recordEvidence?: EvidenceWriter["recordEvidence"],
|
||||
): void => {
|
||||
const contract = toolContract("inspect_analysis_view");
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
async (input) => {
|
||||
const result = await logToolExecution(logger, contract.name, () =>
|
||||
Promise.resolve(inspectAnalysisView(input, evidenceById)),
|
||||
);
|
||||
if (!result.ok) return toCallToolResult(result, contract);
|
||||
const recorded = recordEvidence?.(result.value);
|
||||
return toEvidenceToolResult(result.value, contract, recorded);
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { analysisViewLayoutEvidence } from "../../fixtures/analysisView.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"projects one section from a JSON file of inline layout Evidence",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-analysis-view-cli-");
|
||||
const parent = analysisViewLayoutEvidence();
|
||||
const request = {
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: ".data" },
|
||||
},
|
||||
};
|
||||
const input = join(root, "view.json");
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(input, JSON.stringify(request));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-analysis-view", input, "--json"],
|
||||
environment: {
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: root,
|
||||
XDG_CACHE_HOME: root,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.json).toMatchObject({
|
||||
operation: "inspect_analysis_view",
|
||||
normalized_result: {
|
||||
kind: "item",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
item: { name: { display: ".data" } },
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cliTest("rejects malformed JSON before projection", async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-analysis-view-cli-bad-");
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-analysis-view", "{", "--json"],
|
||||
environment: {
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: root,
|
||||
XDG_CACHE_HOME: root,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: { operation: "inspect-analysis-view" },
|
||||
});
|
||||
});
|
||||
|
||||
cliTest("rejects a request that omits source", async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-analysis-view-cli-missing-");
|
||||
const input = join(root, "missing-source.json");
|
||||
await writeFile(input, JSON.stringify({ view: { kind: "summary" } }));
|
||||
const result = await cli.run({
|
||||
arguments: ["inspect-analysis-view", input, "--json"],
|
||||
environment: {
|
||||
HOME: root,
|
||||
XDG_CONFIG_HOME: root,
|
||||
XDG_CACHE_HOME: root,
|
||||
},
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: { operation: "inspect-analysis-view" },
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const JSON_COMMANDS = [
|
||||
["inspect-analysis-view", "inspect-analysis-view"],
|
||||
["trace-application-feature", "trace-application-feature"],
|
||||
["trace-javascript-semantics", "trace-javascript-semantics"],
|
||||
["compare-application-versions", "compare-application-versions"],
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { SUPPORTED_CLIENT_DEFINITIONS } from "../../../src/application/SupportedClients.js";
|
||||
import {
|
||||
ANALYSIS_VIEW_PROVIDER,
|
||||
ANDROID_APPLICATION_PROVIDER,
|
||||
APPLE_APPLICATION_PROVIDER,
|
||||
ARTIFACT_GRAPH_PROVIDER,
|
||||
@@ -132,6 +133,7 @@ describe("canonical product catalog", () => {
|
||||
JAVASCRIPT_APPLICATION_PROVIDER,
|
||||
JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
|
||||
JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
|
||||
ANALYSIS_VIEW_PROVIDER,
|
||||
WEB_SCRIPT_EXPORT_PROVIDER,
|
||||
]
|
||||
.map(({ id }) => id)
|
||||
@@ -178,6 +180,10 @@ describe("canonical product catalog", () => {
|
||||
"trace_application_feature",
|
||||
"trace_javascript_semantics",
|
||||
]);
|
||||
expect(
|
||||
catalog.providers.find(({ id }) => id === ANALYSIS_VIEW_PROVIDER.id)
|
||||
?.capabilities,
|
||||
).toEqual(["inspect_analysis_view"]);
|
||||
expect(
|
||||
catalog.providers.find(({ id }) => id === ANDROID_APPLICATION_PROVIDER.id)
|
||||
?.capabilities,
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BinaryLayoutService } from "../../../src/application/binaryDiagnostics/BinaryLayoutService.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
} from "../../../src/domain/javascript/javascriptApplicationGraph.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import {
|
||||
analysisViewJavaScriptEvidence,
|
||||
analysisViewJavaScriptAnalysisWithSource,
|
||||
analysisViewBindJavaScriptGraphs,
|
||||
analysisViewLayoutFixture,
|
||||
} from "../../fixtures/analysisView.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { BINARY_LAYOUT_TEST_PROVIDER } from "../../fixtures/binaryDiagnostics/layout.js";
|
||||
|
||||
const connect = async (binaryLayout?: BinaryLayoutService) => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("selected views must not start a deep provider");
|
||||
});
|
||||
const server = createServer(
|
||||
session,
|
||||
session,
|
||||
binaryLayout === undefined ? {} : { binaryLayout },
|
||||
);
|
||||
const client = new Client({ name: "analysis-view-mcp", version: "1" });
|
||||
onTestFinished(async () => {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
return { client, session };
|
||||
};
|
||||
|
||||
it("advertises exact schemas and projects one layout section from retained Evidence", async () => {
|
||||
const layout = analysisViewLayoutFixture();
|
||||
const service = new BinaryLayoutService({
|
||||
identity: BINARY_LAYOUT_TEST_PROVIDER,
|
||||
inspect: () => Promise.resolve(ok(layout)),
|
||||
});
|
||||
const { client, session } = await connect(service);
|
||||
const advertised = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "inspect_analysis_view",
|
||||
);
|
||||
if (advertised?.outputSchema === undefined)
|
||||
throw new Error("inspect_analysis_view must publish both schemas");
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const inputSchema: Record<string, unknown> = advertised.inputSchema;
|
||||
const outputSchema: Record<string, unknown> = advertised.outputSchema;
|
||||
expect(ajv.validateSchema(inputSchema)).toBe(true);
|
||||
expect(ajv.validateSchema(outputSchema)).toBe(true);
|
||||
expect(
|
||||
ajv.validate(inputSchema, {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: `ev_${"a".repeat(64)}`,
|
||||
},
|
||||
view: { kind: "summary", approval: true },
|
||||
}),
|
||||
).toBe(false);
|
||||
const inspected = await client.callTool({
|
||||
name: "inspect_binary_layout",
|
||||
arguments: { path: layout.artifact.path },
|
||||
});
|
||||
expect(inspected.isError).not.toBe(true);
|
||||
const parent = toolContract("inspect_binary_layout").outputSchema.parse(
|
||||
inspected.structuredContent,
|
||||
);
|
||||
const response = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
},
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "sections",
|
||||
selector: { name: ".data" },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
expect(
|
||||
ajv.validate(outputSchema, response.structuredContent),
|
||||
JSON.stringify(ajv.errors),
|
||||
).toBe(true);
|
||||
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(parsed.result).toMatchObject({
|
||||
kind: "item",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
item: { name: { display: ".data" } },
|
||||
});
|
||||
expect(parsed.result).toEqual(
|
||||
parseEvidence(parsed.evidence).normalized_result,
|
||||
);
|
||||
expect(session.evidenceById(parsed.evidence_id)).toEqual(
|
||||
parseEvidence(parsed.evidence),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains an oversized selected observation and keeps subsequent views usable", async () => {
|
||||
const { client, session } = await connect();
|
||||
const analysis = analysisViewJavaScriptAnalysisWithSource();
|
||||
const original = analysis.graph.nodes[0];
|
||||
if (original === undefined) throw new Error("missing module");
|
||||
const source = "\0".repeat(500_000);
|
||||
const node = createJavaScriptApplicationNode({
|
||||
kind: original.kind,
|
||||
identity: original.identity,
|
||||
observations: original.observations.map((observation) => ({
|
||||
label: observation.label,
|
||||
properties: { ...observation.properties, source },
|
||||
evidence: observation.evidence,
|
||||
})),
|
||||
});
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [node.node_id],
|
||||
nodes: [node],
|
||||
edges: [],
|
||||
coverage: analysis.graph.coverage,
|
||||
limitations: analysis.graph.limitations,
|
||||
});
|
||||
const parent = analysisViewJavaScriptEvidence(
|
||||
analysisViewBindJavaScriptGraphs(analysis, graph),
|
||||
);
|
||||
expect(session.recordEvidence(parent).ok).toBe(true);
|
||||
const response = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: {
|
||||
kind: "item",
|
||||
collection: "modules",
|
||||
selector: { node_id: node.node_id },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
const reference = z
|
||||
.object({
|
||||
error: z.object({
|
||||
details: z.object({
|
||||
reported_limits: z.object({
|
||||
evidence_reference: z.object({
|
||||
kind: z.literal("retained-evidence"),
|
||||
evidence_id: z.string(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.parse(response.structuredContent).error.details
|
||||
.reported_limits.evidence_reference;
|
||||
expect(
|
||||
session.evidenceById(reference.evidence_id)?.normalized_result,
|
||||
).toMatchObject({
|
||||
item: {
|
||||
observations: [
|
||||
expect.objectContaining({
|
||||
properties: expect.objectContaining({ source }),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
await client.ping();
|
||||
const summary = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
});
|
||||
expect(summary.isError).not.toBe(true);
|
||||
});
|
||||
|
||||
it("returns a JavaScript summary without graph payloads from a retained reference", async () => {
|
||||
const { client, session } = await connect();
|
||||
const parent = analysisViewJavaScriptEvidence();
|
||||
expect(session.recordEvidence(parent).ok).toBe(true);
|
||||
const response = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(parsed.result).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_operation: "analyze_javascript_application",
|
||||
summary: { format: "directory" },
|
||||
});
|
||||
expect(parsed.result).not.toHaveProperty("summary.semantic_graph");
|
||||
const stale = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: `ev_${"e".repeat(64)}`,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
});
|
||||
expect(stale).toMatchObject({
|
||||
isError: true,
|
||||
structuredContent: {
|
||||
error: { details: { reason: "missing" } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a transport-constraint retained reference as the view source", async () => {
|
||||
const { client, session } = await connect();
|
||||
const parent = analysisViewJavaScriptEvidence();
|
||||
expect(session.recordEvidence(parent).ok).toBe(true);
|
||||
const evidenceReference = {
|
||||
kind: "retained-evidence" as const,
|
||||
evidence_id: parent.evidence_id,
|
||||
};
|
||||
const response = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: evidenceReference,
|
||||
view: {
|
||||
kind: "page",
|
||||
collection: "modules",
|
||||
offset: 0,
|
||||
limit: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const parsed = toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
response.structuredContent,
|
||||
);
|
||||
expect(parsed.result).toMatchObject({
|
||||
kind: "page",
|
||||
parent_evidence_id: parent.evidence_id,
|
||||
coverage: { exhausted: true },
|
||||
});
|
||||
if (parsed.result.kind !== "page") throw new Error("expected page view");
|
||||
expect(parsed.result.items).toEqual([
|
||||
{
|
||||
node_id: expect.any(String),
|
||||
kind: "javascript-asset",
|
||||
path: "renderer.js",
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -65,6 +65,26 @@ it("advertises exact valid schemas and records inline layout without a binary ta
|
||||
expect(parsed.result).toEqual(value);
|
||||
expect(parsed.result).toEqual(evidence.normalized_result);
|
||||
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
|
||||
const viewed = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: evidence.evidence_id,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
});
|
||||
expect(viewed.isError).not.toBe(true);
|
||||
expect(
|
||||
toolContract("inspect_analysis_view").outputSchema.parse(
|
||||
viewed.structuredContent,
|
||||
).result,
|
||||
).toMatchObject({
|
||||
kind: "summary",
|
||||
parent_evidence_id: evidence.evidence_id,
|
||||
summary: { format: "elf", bytes: value.artifact.bytes },
|
||||
});
|
||||
expect(
|
||||
(
|
||||
await client.callTool({
|
||||
|
||||
@@ -215,3 +215,27 @@ it("preserves validation for the referenced record's operation, predicate, and s
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
});
|
||||
|
||||
it("projects a retained JavaScript summary without graph or semantic_graph", async () => {
|
||||
const { client, session } = await connect();
|
||||
expect(session.recordEvidence(application).ok).toBe(true);
|
||||
const inline = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: { kind: "inline", evidence: application },
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
});
|
||||
const referenced = await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: retained(application.evidence_id),
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
});
|
||||
expect(inline.isError).not.toBe(true);
|
||||
expect(referenced.structuredContent).toEqual(inline.structuredContent);
|
||||
expect(inline.structuredContent).not.toHaveProperty(
|
||||
"result.summary.semantic_graph",
|
||||
);
|
||||
});
|
||||
Vendored
+195
@@ -0,0 +1,195 @@
|
||||
import { BINARY_LAYOUT_TEST_PROVIDER } from "./binaryDiagnostics/layout.js";
|
||||
import { binaryLayoutFixture } from "./binaryDiagnostics/layout.js";
|
||||
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js";
|
||||
import { createEvidence, type Evidence } from "../../src/domain/evidence.js";
|
||||
import type { JavaScriptApplicationAnalysisResult } from "../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
} from "../../src/domain/javascript/javascriptApplicationGraph.js";
|
||||
import { createJavaScriptSemanticGraph } from "../../src/domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { BinaryLayout } from "../../src/domain/native/binaryLayout.js";
|
||||
import { binaryLayoutSchema } from "../../src/domain/native/binaryLayout.js";
|
||||
|
||||
const hex = (value: number): string =>
|
||||
value === 0 ? "0x0" : `0x${value.toString(16)}`;
|
||||
|
||||
const layoutName = (display: string, offset: number) => ({
|
||||
display,
|
||||
bytes_base64: Buffer.from(display, "utf8").toString("base64"),
|
||||
location: { offset: hex(offset), bytes: hex(display.length + 1) },
|
||||
unknown_reason: null,
|
||||
});
|
||||
|
||||
const layoutSection = (
|
||||
index: number,
|
||||
display: string,
|
||||
nameOffset: number,
|
||||
): BinaryLayout["sections"][number] => ({
|
||||
index,
|
||||
name: layoutName(display, nameOffset),
|
||||
name_offset: hex(nameOffset),
|
||||
type: 1,
|
||||
header_location: { offset: hex(index * 16), bytes: "0x10" },
|
||||
address: "0x0",
|
||||
offset: hex(64 + index * 16),
|
||||
size: "0x10",
|
||||
alignment: "0x1",
|
||||
flags: "0x6",
|
||||
link: 0,
|
||||
info: 0,
|
||||
entry_size: "0x0",
|
||||
file_backing: "file",
|
||||
});
|
||||
|
||||
const layoutSymbol = (
|
||||
entryIndex: number,
|
||||
display: string,
|
||||
nameOffset: number,
|
||||
): BinaryLayout["symbols"][number] => ({
|
||||
table_index: 0,
|
||||
entry_index: entryIndex,
|
||||
name: layoutName(display, nameOffset),
|
||||
name_offset: hex(nameOffset),
|
||||
location: { offset: hex(nameOffset), bytes: hex(display.length + 1) },
|
||||
value: "0x0",
|
||||
value_meaning: "no-address",
|
||||
size: "0x0",
|
||||
binding: 1,
|
||||
type: 2,
|
||||
visibility: 0,
|
||||
section_index: 1,
|
||||
});
|
||||
|
||||
/** Layout fixture with duplicate section/symbol names for selected-view tests. */
|
||||
export const analysisViewLayoutFixture = (): BinaryLayout =>
|
||||
binaryLayoutSchema.parse({
|
||||
...binaryLayoutFixture("/artifacts/view-source.elf"),
|
||||
artifact: {
|
||||
path: "/artifacts/view-source.elf",
|
||||
sha256: "b".repeat(64),
|
||||
bytes: 512,
|
||||
},
|
||||
sections: [
|
||||
layoutSection(0, ".text", 256),
|
||||
layoutSection(1, ".text", 262),
|
||||
layoutSection(2, ".data", 268),
|
||||
],
|
||||
symbols: [
|
||||
layoutSymbol(0, "main", 280),
|
||||
layoutSymbol(1, "main", 285),
|
||||
layoutSymbol(2, "start", 290),
|
||||
],
|
||||
});
|
||||
|
||||
/** Wrap a layout observation as inspect_binary_layout Evidence. */
|
||||
export const analysisViewLayoutEvidence = (
|
||||
layout: BinaryLayout = analysisViewLayoutFixture(),
|
||||
): Evidence =>
|
||||
createEvidence(
|
||||
{
|
||||
path: layout.artifact.path,
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
sha256: layout.artifact.sha256,
|
||||
},
|
||||
BINARY_LAYOUT_TEST_PROVIDER,
|
||||
{
|
||||
operation: "inspect_binary_layout",
|
||||
parameters: { path: layout.artifact.path },
|
||||
result: layout,
|
||||
rawResult: layout,
|
||||
confidence: "observed",
|
||||
limitations: layout.limitations,
|
||||
locations: [{ kind: "artifact-path", path: layout.artifact.path }],
|
||||
},
|
||||
);
|
||||
|
||||
/** Minimal authenticated JavaScript application analysis used by view tests. */
|
||||
export const analysisViewJavaScriptAnalysis =
|
||||
(): JavaScriptApplicationAnalysisResult =>
|
||||
javascriptApplicationAnalysisResultSchema.parse(
|
||||
JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.normalized_result,
|
||||
);
|
||||
|
||||
/** Application analysis whose module observations include source text. */
|
||||
export const analysisViewJavaScriptAnalysisWithSource =
|
||||
(): JavaScriptApplicationAnalysisResult => {
|
||||
const analysis = analysisViewJavaScriptAnalysis();
|
||||
const original = analysis.graph.nodes[0];
|
||||
if (original === undefined)
|
||||
throw new Error("JavaScript application example is missing its module");
|
||||
const withSource = createJavaScriptApplicationNode({
|
||||
kind: original.kind,
|
||||
identity: original.identity,
|
||||
observations: original.observations.map((observation) => ({
|
||||
label: observation.label,
|
||||
properties: {
|
||||
...observation.properties,
|
||||
source: "export const secret = 1;\n",
|
||||
text: "should not leak",
|
||||
},
|
||||
evidence: observation.evidence,
|
||||
})),
|
||||
});
|
||||
const graph = createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [withSource.node_id],
|
||||
nodes: [withSource],
|
||||
edges: [],
|
||||
coverage: analysis.graph.coverage,
|
||||
limitations: analysis.graph.limitations,
|
||||
});
|
||||
const { graph_id: _graphId, ...semantic } = analysis.semantic_graph;
|
||||
return javascriptApplicationAnalysisResultSchema.parse({
|
||||
...analysis,
|
||||
graph,
|
||||
semantic_graph: createJavaScriptSemanticGraph({
|
||||
...semantic,
|
||||
application_graph_id: graph.graph_id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
/** Rebind a companion semantic graph after replacing the application graph. */
|
||||
export const analysisViewBindJavaScriptGraphs = (
|
||||
analysis: JavaScriptApplicationAnalysisResult,
|
||||
graph: ReturnType<typeof createJavaScriptApplicationGraph>,
|
||||
): JavaScriptApplicationAnalysisResult => {
|
||||
const { graph_id: _graphId, ...semantic } = analysis.semantic_graph;
|
||||
return javascriptApplicationAnalysisResultSchema.parse({
|
||||
...analysis,
|
||||
graph,
|
||||
semantic_graph: createJavaScriptSemanticGraph({
|
||||
...semantic,
|
||||
application_graph_id: graph.graph_id,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
/** Wrap JavaScript application analysis as producer Evidence. */
|
||||
export const analysisViewJavaScriptEvidence = (
|
||||
analysis: JavaScriptApplicationAnalysisResult = analysisViewJavaScriptAnalysis(),
|
||||
): Evidence =>
|
||||
createEvidence(
|
||||
{
|
||||
path: analysis.input_path,
|
||||
format: analysis.format,
|
||||
sha256: analysis.root_artifact_sha256,
|
||||
},
|
||||
{
|
||||
id: "rea-javascript-application",
|
||||
name: "REA JavaScript application analyzer",
|
||||
version: "1",
|
||||
},
|
||||
{
|
||||
predicateType: "rea.javascript-application-analysis",
|
||||
operation: "analyze_javascript_application",
|
||||
parameters: { input_path: analysis.input_path, format: analysis.format },
|
||||
result: analysis,
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
limitations: analysis.limitations,
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user