* feat(mcp): derive an API surface view from retained web network captures * fix(mcp): fail closed on sensitive parents and budget api-surface path expansion * fix(mcp): keep recorded api-surface paths and budget only templated segments * fix(mcp): read api-surface paths only from canonical URLs and charge new values once * fix(mcp): reject authority-less api-surface URLs and key groups by rule positions * fix(mcp): label api-surface confidence from all groups and cap transient path memory * fix(mcp): budget api-surface dedup sets and narrow detail outputs Charge retained deduplication sets before allocation, advertise only each analysis operation's summary from its detail output, and document that any templated group makes every API-surface page inferred. * fix(mcp): bound api-surface path text and record limitations Charge retained literal path copies before allocation, publish only valid media-type essences, and deduplicate examined record limitations onto the view. * fix(mcp): keep api-surface refusals and path splits inside the reply budget Sensitive-parent refusals now use the token sensitive_values_unretained so declared literals are not echoed. A later path split is charged against bytes already retained, and the published example includes the HAR record limitation with its matching digest. * fix(mcp): budget api-surface method text before group keys Charge method and origin text against the reply budget before the group key is serialized. Document that samples merge only when method, origin, and exact segment rules match. * fix(mcp): bound api-surface copies and control-character authorities Reject authorities containing a tab or newline so WHATWG cannot move userinfo into the recorded path. Charge four path copies, two method and origin copies, and each distinct content type against bytes already retained. * fix(mcp): preflight combined api-surface group charges Total path, method, origin, and segment charges before retaining a new group. Track content types in a set so distinct MIME essences are not compared quadratically. * fix(mcp): charge api-surface content-type sets on first use Create the per-group content-type set when the first essence is retained, and charge it before allocation. Groups with no content type no longer keep an empty set. * refactor(analysis): centralize API-surface ownership and admission Own grouping, deduplication, and combined retained/temporary resource admission in one accumulator. Charge fixed group storage before allocation and materialize only the requested page. Share recorded URL parsing between capture redaction and projection, preserving source paths and excluding ambiguous credential fields. Cover resource admission invariants and the compiled HAR-to-view workflow. * fix(analysis): admit capture schema clone before parsing * fix(javascript): share Electron mutation proof with wrapped calls * test(mcp): align graph and catalog checks with current contracts * fix(analysis): admit and validate portable capture views --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
270 lines
8.6 KiB
TypeScript
270 lines
8.6 KiB
TypeScript
import { apiSurfaceHar } from "./historicalHar.js";
|
|
import { webNetworkCaptureSchema } from "../../src/domain/webNetworkCapture.js";
|
|
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: null,
|
|
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,
|
|
},
|
|
);
|
|
|
|
/** Capture parent with explicit producer ordinals, including excluded URL values. */
|
|
export const analysisViewCaptureFixture = () =>
|
|
webNetworkCaptureSchema.parse({
|
|
artifact: {
|
|
path: "/fixtures/surface.har",
|
|
sha256: "c".repeat(64),
|
|
bytes: 1024,
|
|
},
|
|
format: "har",
|
|
decoder: { id: "fixture", name: "fixture", version: "1" },
|
|
container: {
|
|
reported: {},
|
|
numeric_literals: [],
|
|
redactions: [],
|
|
records_pointer: "/log/entries",
|
|
},
|
|
total_records: 15,
|
|
records: [
|
|
...apiSurfaceHar().log.entries,
|
|
{ request: { method: "GET", url: null } },
|
|
{ request: { method: "GET", url: "invalid URL" } },
|
|
].map((reported, ordinal) => ({
|
|
ordinal,
|
|
location: { kind: "json-pointer", pointer: `/log/entries/${ordinal}` },
|
|
reported,
|
|
binary_fields: [],
|
|
numeric_literals: [],
|
|
redactions: [],
|
|
limitations: [],
|
|
})),
|
|
runtime_attribution: "unknown",
|
|
limitations: [],
|
|
});
|
|
|
|
export const analysisViewCaptureEvidence = (
|
|
capture = analysisViewCaptureFixture(),
|
|
): Evidence =>
|
|
createEvidence(
|
|
capture.artifact.sha256 === null
|
|
? undefined
|
|
: {
|
|
path: capture.artifact.path,
|
|
sha256: capture.artifact.sha256,
|
|
format: "file",
|
|
},
|
|
{ id: "fixture", name: "fixture", version: "1" },
|
|
{
|
|
operation: "inspect_web_network_capture",
|
|
parameters: { sensitive_value_count: 0 },
|
|
result: capture,
|
|
confidence: "observed",
|
|
},
|
|
);
|
|
|
|
/** A small encoded capture whose many short objects expand during validation. */
|
|
export const analysisViewObjectDenseCapture = (headerCount = 100_000) => {
|
|
const capture = analysisViewCaptureFixture();
|
|
capture.records = capture.records.slice(0, 1);
|
|
capture.total_records = 1;
|
|
capture.records[0]!.reported = {
|
|
request: {
|
|
method: "GET",
|
|
url: "https://api.test/items/1",
|
|
headers: Array.from({ length: headerCount }, () => ({
|
|
name: "x",
|
|
value: "y",
|
|
})),
|
|
},
|
|
};
|
|
return capture;
|
|
};
|