Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6cda443c1 | ||
|
|
e308f35c43 | ||
|
|
719d56ebfe | ||
|
|
87c8086fc1 | ||
|
|
eab4e47916 | ||
|
|
db87917daa | ||
|
|
183b4d4e7e | ||
|
|
00819258a7 |
No files matched your search
@@ -106,7 +106,12 @@ nodes. REA evaluates compatible static candidates conservatively:
|
||||
that disagreement. If a mapped candidate's digest is unavailable and no
|
||||
mapped content match is known, the comparison remains `unknown`.
|
||||
6. Missing authorization mappings and incomplete or truncated input coverage
|
||||
remain unknown or truncated rather than becoming absence claims.
|
||||
remain unknown or truncated rather than becoming absence claims. A runtime
|
||||
location with no automatic artifact-root or caller-declared mapping into
|
||||
any layer, and no content match, is `unknown` with
|
||||
`no-authorized-location-mapping`, with or without captured bytes; several
|
||||
content matches remain `ambiguous` under rule 4. Only a mapped location can
|
||||
be `unmatched` with `no-static-content-match`.
|
||||
|
||||
Every successful cross-layer `observed_as` edge has
|
||||
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { PacketCaptureService } from "./PacketCaptureService.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { inspectPacketCaptureInputSchema } from "../../domain/captures/packetCapture.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
|
||||
it("retains provider metadata in a late-cancellation observation", async () => {
|
||||
const input = inspectPacketCaptureInputSchema.parse({
|
||||
path: process.platform === "win32" ? "C:\\capture.pcap" : "/capture.pcap",
|
||||
fields: ["ip.addr"],
|
||||
});
|
||||
const provider = {
|
||||
id: "fixture-capture",
|
||||
name: "Fixture capture provider",
|
||||
version: "1",
|
||||
} as const;
|
||||
const subject = {
|
||||
path: input.path,
|
||||
sha256: "a".repeat(64),
|
||||
format: "file",
|
||||
} as const;
|
||||
const report = {
|
||||
artifact: { path: input.path, sha256: subject.sha256, bytes: 12 },
|
||||
selection: input,
|
||||
packets: [],
|
||||
output_window: {
|
||||
matching_packets: 0,
|
||||
returned_packets: 0,
|
||||
has_more: false,
|
||||
},
|
||||
read_scope: "to-eof",
|
||||
diagnostics: "",
|
||||
tool: {
|
||||
configured_path: "/fixture/tshark",
|
||||
path: "/fixture/tshark",
|
||||
sha256: "b".repeat(64),
|
||||
version_banner: "TShark fixture",
|
||||
version_diagnostics: "",
|
||||
arguments: [],
|
||||
},
|
||||
configuration: "private-empty-personal-profile",
|
||||
resource_limiter: {
|
||||
configured_path: "/fixture/prlimit",
|
||||
path: "/fixture/prlimit",
|
||||
sha256: "c".repeat(64),
|
||||
arguments: [],
|
||||
},
|
||||
limits: {
|
||||
input_bytes: 1,
|
||||
output_bytes: 1,
|
||||
command_timeout_ms: 1,
|
||||
address_space_bytes: 0,
|
||||
file_bytes: 0,
|
||||
cpu_seconds: 0,
|
||||
},
|
||||
hardware_acquisition: "not-performed",
|
||||
} as const;
|
||||
const rawResult = { producer: "fixture", bytes: 12 };
|
||||
const profile = createAnalysisProfile(provider, { filter: null });
|
||||
const relationshipCoverage = {
|
||||
status: "unknown",
|
||||
reason: "Packet relationships are not reported.",
|
||||
} as const;
|
||||
const execution = createAnalysisExecution(report, provider, {
|
||||
subject,
|
||||
rawResult,
|
||||
analysisProfile: profile,
|
||||
relationshipCoverage,
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const reason = Object.freeze({ requested: "cancel after capture result" });
|
||||
const service = new PacketCaptureService({
|
||||
inspect: async (_input, options?: ExecutionOptions) => {
|
||||
controller.abort(reason);
|
||||
expect(options?.signal).toBe(controller.signal);
|
||||
return ok(execution);
|
||||
},
|
||||
inspectFields: async () => {
|
||||
throw new Error("Unexpected field discovery call");
|
||||
},
|
||||
close: async () => {},
|
||||
});
|
||||
|
||||
const result = await service.inspect(input, { signal: controller.signal });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected late cancellation");
|
||||
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
operation: "inspect_packet_capture",
|
||||
raw_result: rawResult,
|
||||
analysis_profile: profile,
|
||||
relationship_coverage: relationshipCoverage,
|
||||
});
|
||||
});
|
||||
@@ -3,24 +3,25 @@ import {
|
||||
packetFieldsCatalogSchema,
|
||||
type InspectPacketFieldsInput,
|
||||
} from "../../domain/captures/packetFields.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
ExecutionOptions,
|
||||
import {
|
||||
createAnalysisExecutionEvidence,
|
||||
type AnalysisExecution,
|
||||
type ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import {
|
||||
inspectPacketCaptureInputSchema,
|
||||
packetCaptureSchema,
|
||||
type InspectPacketCaptureInput,
|
||||
} from "../../domain/captures/packetCapture.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
export interface PacketCapturePort {
|
||||
/** Release per-call resources before returning, or retain failed cleanup for close/retry. */
|
||||
@@ -74,7 +75,15 @@ export class PacketCaptureService {
|
||||
"Capture analyzer returned malformed data or changed the selected artifact identity/options.",
|
||||
),
|
||||
);
|
||||
return this.#evidence(operation, input.data, execution, options);
|
||||
return completeProviderEvidence(
|
||||
createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
input.data,
|
||||
execution,
|
||||
"observed",
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
async inspectFields(
|
||||
raw: unknown,
|
||||
@@ -108,33 +117,14 @@ export class PacketCaptureService {
|
||||
"Field discovery returned malformed data or changed the selected installation/options.",
|
||||
),
|
||||
);
|
||||
return this.#evidence(operation, input.data, execution, options);
|
||||
}
|
||||
#evidence(
|
||||
operation: string,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
execution: AnalysisExecution,
|
||||
options?: ExecutionOptions,
|
||||
): Result<Evidence, AnalysisError> {
|
||||
const evidence = createEvidence(
|
||||
execution.subject ?? undefined,
|
||||
execution.provider,
|
||||
{
|
||||
return completeProviderEvidence(
|
||||
createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
parameters,
|
||||
result: execution.result,
|
||||
confidence: "observed",
|
||||
locations: execution.locations,
|
||||
limitations: execution.limitations,
|
||||
},
|
||||
input.data,
|
||||
execution,
|
||||
"observed",
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
return options?.signal?.aborted
|
||||
? err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options.signal.reason,
|
||||
partialObservation: evidence,
|
||||
}),
|
||||
)
|
||||
: ok(evidence);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "../../domain/firmware/firmwareAnalysis.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
|
||||
const fixture = (
|
||||
operation: FirmwareOperation,
|
||||
@@ -68,15 +69,35 @@ it.each(["inspect_firmware_regions", "extract_firmware"] as const)(
|
||||
"retains %s Evidence after late cancellation and preserves provider failure precedence",
|
||||
async (operation) => {
|
||||
const { input, execution } = fixture(operation);
|
||||
const profile = createAnalysisProfile(
|
||||
{ ...execution.provider, version: "1" },
|
||||
{
|
||||
signature_database: "caller-selected snapshot",
|
||||
},
|
||||
);
|
||||
const coverage = {
|
||||
status: "unknown",
|
||||
reason: "No call graph examined",
|
||||
} as const;
|
||||
const observation = {
|
||||
...execution,
|
||||
analysisProfile: profile,
|
||||
relationshipCoverage: coverage,
|
||||
};
|
||||
const control = await new FirmwareAnalysisService({
|
||||
execute: async () => ok(execution),
|
||||
execute: async () => ok(observation),
|
||||
}).execute(operation, input);
|
||||
if (!control.ok) throw control.error;
|
||||
expect(control.value).toMatchObject({
|
||||
raw_result: { source: "controlled producer" },
|
||||
analysis_profile: profile,
|
||||
relationship_coverage: coverage,
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const result = await new FirmwareAnalysisService({
|
||||
execute: async () => {
|
||||
controller.abort();
|
||||
return ok(execution);
|
||||
return ok(observation);
|
||||
},
|
||||
}).execute(operation, input, { signal: controller.signal });
|
||||
expect(result).toMatchObject({
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import {
|
||||
createAnalysisExecutionEvidence,
|
||||
type ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { FirmwareAnalysisPort } from "./FirmwareAnalysisPort.js";
|
||||
import {
|
||||
firmwareRequestSchema,
|
||||
@@ -12,8 +15,7 @@ import {
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { jsonObjectSchema } from "../../domain/jsonValue.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { projectInputIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
@@ -71,15 +73,12 @@ export class FirmwareAnalysisService {
|
||||
),
|
||||
);
|
||||
return completeProviderEvidence(
|
||||
createEvidence(execution.subject, execution.provider, {
|
||||
createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
parameters: jsonObjectSchema.parse(parsed.data.input),
|
||||
result: execution.result,
|
||||
rawResult: execution.rawResult,
|
||||
locations: execution.locations,
|
||||
limitations: execution.limitations,
|
||||
confidence: "observed",
|
||||
}),
|
||||
parsed.data.input,
|
||||
execution,
|
||||
"observed",
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { SignalRecordingService } from "./SignalRecordingService.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import { decodeSignalRecordingInputSchema } from "../../domain/signals/signalRecording.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
|
||||
it("retains provider metadata in a late-cancellation observation", async () => {
|
||||
const input = decodeSignalRecordingInputSchema.parse({
|
||||
path: process.platform === "win32" ? "C:\\recording.vcd" : "/recording.vcd",
|
||||
decoders: "uart",
|
||||
});
|
||||
const provider = {
|
||||
id: "fixture-signal",
|
||||
name: "Fixture signal provider",
|
||||
version: "1",
|
||||
} as const;
|
||||
const subject = {
|
||||
path: input.path,
|
||||
sha256: "a".repeat(64),
|
||||
format: "file",
|
||||
} as const;
|
||||
const report = {
|
||||
artifact: { path: input.path, sha256: subject.sha256, bytes: 12 },
|
||||
selection: input,
|
||||
annotations_text: "10-20 uart-1: 42\n",
|
||||
output_window: { total_lines: 1, returned_lines: 1, has_more: false },
|
||||
diagnostics: "",
|
||||
tool: {
|
||||
configured_path: "/fixture/sigrok-cli",
|
||||
path: "/fixture/sigrok-cli",
|
||||
sha256: "b".repeat(64),
|
||||
version_banner: "sigrok-cli fixture",
|
||||
version_diagnostics: "",
|
||||
arguments: [],
|
||||
},
|
||||
resource_limiter: {
|
||||
configured_path: "/fixture/prlimit",
|
||||
path: "/fixture/prlimit",
|
||||
sha256: "c".repeat(64),
|
||||
arguments: [],
|
||||
},
|
||||
limits: {
|
||||
input_bytes: 1,
|
||||
output_bytes: 1,
|
||||
command_timeout_ms: 1,
|
||||
address_space_bytes: 0,
|
||||
file_bytes: 0,
|
||||
cpu_seconds: 0,
|
||||
},
|
||||
hardware_acquisition: "not-performed",
|
||||
} as const;
|
||||
const rawResult = { producer: "fixture", bytes: 12 };
|
||||
const profile = createAnalysisProfile(provider, { decoder: "uart" });
|
||||
const relationshipCoverage = {
|
||||
status: "unknown",
|
||||
reason: "Recording relationships are not reported.",
|
||||
} as const;
|
||||
const execution = createAnalysisExecution(report, provider, {
|
||||
subject,
|
||||
rawResult,
|
||||
analysisProfile: profile,
|
||||
relationshipCoverage,
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const reason = Object.freeze({ requested: "cancel after recording result" });
|
||||
const service = new SignalRecordingService({
|
||||
decode: async (_input, options?: ExecutionOptions) => {
|
||||
controller.abort(reason);
|
||||
expect(options?.signal).toBe(controller.signal);
|
||||
return ok(execution);
|
||||
},
|
||||
inspectDecoders: async () => {
|
||||
throw new Error("Unexpected decoder discovery call");
|
||||
},
|
||||
close: async () => {},
|
||||
});
|
||||
|
||||
const result = await service.decode(input, { signal: controller.signal });
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected late cancellation");
|
||||
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
operation: "decode_signal_recording",
|
||||
raw_result: rawResult,
|
||||
analysis_profile: profile,
|
||||
relationship_coverage: relationshipCoverage,
|
||||
});
|
||||
});
|
||||
@@ -3,24 +3,25 @@ import {
|
||||
signalDecodersSchema,
|
||||
type InspectSignalDecodersInput,
|
||||
} from "../../domain/signals/signalDecoders.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type {
|
||||
AnalysisExecution,
|
||||
ExecutionOptions,
|
||||
import {
|
||||
createAnalysisExecutionEvidence,
|
||||
type AnalysisExecution,
|
||||
type ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import {
|
||||
decodeSignalRecordingInputSchema,
|
||||
signalRecordingSchema,
|
||||
type DecodeSignalRecordingInput,
|
||||
} from "../../domain/signals/signalRecording.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
export interface SignalRecordingPort {
|
||||
/** Release per-call resources before returning, or retain failed cleanup for close/retry. */
|
||||
@@ -74,7 +75,15 @@ export class SignalRecordingService {
|
||||
"Decoder returned malformed data or changed the selected recording identity/options.",
|
||||
),
|
||||
);
|
||||
return this.#evidence(operation, input.data, execution, options);
|
||||
return completeProviderEvidence(
|
||||
createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
input.data,
|
||||
execution,
|
||||
"observed",
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
async inspectDecoders(
|
||||
raw: unknown,
|
||||
@@ -108,33 +117,14 @@ export class SignalRecordingService {
|
||||
"Decoder discovery returned malformed data or changed the selected installation/options.",
|
||||
),
|
||||
);
|
||||
return this.#evidence(operation, input.data, execution, options);
|
||||
}
|
||||
#evidence(
|
||||
operation: string,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
execution: AnalysisExecution,
|
||||
options?: ExecutionOptions,
|
||||
): Result<Evidence, AnalysisError> {
|
||||
const evidence = createEvidence(
|
||||
execution.subject ?? undefined,
|
||||
execution.provider,
|
||||
{
|
||||
return completeProviderEvidence(
|
||||
createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
parameters,
|
||||
result: execution.result,
|
||||
confidence: "observed",
|
||||
locations: execution.locations,
|
||||
limitations: execution.limitations,
|
||||
},
|
||||
input.data,
|
||||
execution,
|
||||
"observed",
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
return options?.signal?.aborted
|
||||
? err(
|
||||
new AnalysisCancelledError(operation, {
|
||||
cause: options.signal.reason,
|
||||
partialObservation: evidence,
|
||||
}),
|
||||
)
|
||||
: ok(evidence);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
wasmArtifactExecution,
|
||||
} from "../../../tests/fixtures/wasm/artifact.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
it.each([
|
||||
{ path: "relative" },
|
||||
{ path: "/module", glue_paths: ["relative"] },
|
||||
@@ -119,30 +120,54 @@ it("cancels before provider work and delivers observed Evidence on success", asy
|
||||
});
|
||||
});
|
||||
|
||||
it("retains completed Evidence when cancellation arrives after provider return", async () => {
|
||||
const value = wasmArtifactFixture();
|
||||
const controller = new AbortController();
|
||||
const service = new WasmArtifactService({
|
||||
close: () => Promise.resolve(),
|
||||
inspect: async () => {
|
||||
controller.abort();
|
||||
return ok(wasmArtifactExecution(value));
|
||||
},
|
||||
});
|
||||
it.each([false, true])(
|
||||
"retains provider provenance after completion (late cancellation: %s)",
|
||||
async (cancel) => {
|
||||
const value = wasmArtifactFixture();
|
||||
const execution = wasmArtifactExecution(value);
|
||||
const profile = createAnalysisProfile(
|
||||
{ ...execution.provider, version: "1" },
|
||||
{
|
||||
features: "selected",
|
||||
},
|
||||
);
|
||||
const coverage = {
|
||||
status: "unknown",
|
||||
reason: "No call graph examined",
|
||||
} as const;
|
||||
const controller = new AbortController();
|
||||
const service = new WasmArtifactService({
|
||||
close: () => Promise.resolve(),
|
||||
inspect: async () => {
|
||||
if (cancel) controller.abort();
|
||||
return ok({
|
||||
...execution,
|
||||
rawResult: { stdout: "retained command output" },
|
||||
analysisProfile: profile,
|
||||
relationshipCoverage: coverage,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
await service.inspect(
|
||||
const result = await service.inspect(
|
||||
{ path: value.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
_tag: "AnalysisCancelledError",
|
||||
partialObservation: { normalized_result: value },
|
||||
},
|
||||
});
|
||||
});
|
||||
);
|
||||
if (cancel)
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError" },
|
||||
});
|
||||
else expect(result.ok).toBe(true);
|
||||
const evidence = result.ok ? result.value : result.error.partialObservation;
|
||||
expect(evidence).toMatchObject({
|
||||
normalized_result: value,
|
||||
raw_result: { stdout: "retained command output" },
|
||||
analysis_profile: profile,
|
||||
relationship_coverage: coverage,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves malformed-output failure when cancellation arrives with it", async () => {
|
||||
const value = wasmArtifactFixture();
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import {
|
||||
createAnalysisExecutionEvidence,
|
||||
type ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { WasmArtifactPort } from "./WasmArtifactPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectWasmArtifactInputSchema,
|
||||
wasmArtifactSchema,
|
||||
@@ -69,17 +72,12 @@ export class WasmArtifactService {
|
||||
),
|
||||
);
|
||||
return completeProviderEvidence(
|
||||
createEvidence(execution.subject, execution.provider, {
|
||||
operation: OPERATION,
|
||||
parameters: input.data,
|
||||
result: report.data,
|
||||
...(execution.rawResult === null
|
||||
? {}
|
||||
: { rawResult: execution.rawResult }),
|
||||
confidence: "observed",
|
||||
locations: execution.locations,
|
||||
limitations: execution.limitations,
|
||||
}),
|
||||
createAnalysisExecutionEvidence(
|
||||
OPERATION,
|
||||
input.data,
|
||||
{ ...execution, result: report.data },
|
||||
"observed",
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type WebBundleAnalysis,
|
||||
} from "../domain/webBundleAnalysis.js";
|
||||
import { buildCapturedWebBundleAnalysis } from "../domain/webBundleAnalyzerResult.js";
|
||||
import { isIncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import { isIncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import type { WebBundleScriptAnalysis } from "../domain/webBundleScriptAnalysis.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { access } from "node:fs/promises";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
|
||||
@@ -31,7 +31,7 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { jsonParts, bufferedJsonParts } from "../domain/jsonSerialization.js";
|
||||
import { javaScriptRuntime } from "../domain/runtimeVersion.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import {
|
||||
webBundleScriptAnalysisSchema,
|
||||
type WebBundleScriptAnalysis,
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock("node:v8", async (importOriginal) => {
|
||||
|
||||
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import { WebBundleAnalysisWorker } from "./WebBundleAnalysisWorker.js";
|
||||
|
||||
it("refuses before spawning or reading source when no host result capacity remains", async () => {
|
||||
|
||||
@@ -47,7 +47,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
|
||||
});
|
||||
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createInterface } from "node:readline";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import {
|
||||
webBundleWorkerMessageSchema,
|
||||
type WebBundleWorkerMessage,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { withArtifactFileHandle } from "../artifacts/ArtifactFileOperation.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { jsonParts } from "../domain/jsonSerialization.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerInspection.js";
|
||||
import type { IncludedScript } from "../domain/webBundleAnalyzerState.js";
|
||||
import {
|
||||
webBundleScriptAnalysisSchema,
|
||||
type WebBundleScriptAnalysis,
|
||||
|
||||
@@ -77,14 +77,13 @@ const reconcileEntity = (
|
||||
reason: "runtime-location-unresolved",
|
||||
candidates: [],
|
||||
});
|
||||
const locations = candidatesWithLocation(entity, index, layers);
|
||||
const { locations, mapped } = locateEntity(entity, index, layers);
|
||||
if (entity.sourceSha256 !== null)
|
||||
return reconcileCapturedSource(
|
||||
entity,
|
||||
entity.sourceSha256,
|
||||
return reconcileCapturedSource(entity, entity.sourceSha256, {
|
||||
locations,
|
||||
mapped,
|
||||
index,
|
||||
);
|
||||
});
|
||||
|
||||
const location = locations.at(0);
|
||||
if (locations.length === 1 && location !== undefined) {
|
||||
@@ -103,20 +102,40 @@ const reconcileEntity = (
|
||||
reason: "ambiguous-static-candidates",
|
||||
candidates: locations.map(({ candidate }) => candidate),
|
||||
});
|
||||
const mapped = layers.some(
|
||||
(layer) => mappedRuntimePaths(layer, entity).length > 0,
|
||||
);
|
||||
return unresolved(entity, {
|
||||
status: mapped ? "unmatched" : "unknown",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
reason: mapped
|
||||
? "no-static-content-match"
|
||||
: "no-authorized-location-mapping",
|
||||
candidates: [],
|
||||
});
|
||||
return withoutStaticCandidate(entity, mapped, "unknown");
|
||||
};
|
||||
|
||||
/**
|
||||
* Classify an entity with no qualifying static candidate. Absence is claimed
|
||||
* only beneath an automatic or caller-declared mapping; without one, the
|
||||
* runtime location has no authorized static counterpart and stays unknown.
|
||||
* Captured bytes compared against every static digest support a high-confidence
|
||||
* absence; a location-only absence does not.
|
||||
*/
|
||||
const withoutStaticCandidate = (
|
||||
entity: RuntimeReconciliationEntity,
|
||||
mapped: boolean,
|
||||
absenceConfidence: JavaScriptRuntimeReconciliationItem["confidence"],
|
||||
): EvaluatedMatch =>
|
||||
unresolved(
|
||||
entity,
|
||||
mapped
|
||||
? {
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: absenceConfidence,
|
||||
reason: "no-static-content-match",
|
||||
candidates: [],
|
||||
}
|
||||
: {
|
||||
status: "unknown",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
reason: "no-authorized-location-mapping",
|
||||
candidates: [],
|
||||
},
|
||||
);
|
||||
|
||||
interface LocatedCandidate {
|
||||
readonly candidate: StaticRuntimeCandidate;
|
||||
readonly mapping: MappedRuntimePath;
|
||||
@@ -125,8 +144,15 @@ interface LocatedCandidate {
|
||||
const reconcileCapturedSource = (
|
||||
entity: RuntimeReconciliationEntity,
|
||||
sourceSha256: string,
|
||||
locations: readonly LocatedCandidate[],
|
||||
index: CandidateIndex,
|
||||
{
|
||||
locations,
|
||||
mapped,
|
||||
index,
|
||||
}: {
|
||||
readonly locations: readonly LocatedCandidate[];
|
||||
readonly mapped: boolean;
|
||||
readonly index: CandidateIndex;
|
||||
},
|
||||
): EvaluatedMatch => {
|
||||
// A known mapped candidate owns the comparison. Unrelated equal bytes cannot
|
||||
// repair a disagreement or establish the content of a candidate without a digest.
|
||||
@@ -176,23 +202,19 @@ const reconcileCapturedSource = (
|
||||
candidates: locations.map(({ candidate: located }) => located),
|
||||
});
|
||||
}
|
||||
return unresolved(entity, {
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: "high",
|
||||
reason: "no-static-content-match",
|
||||
candidates: [],
|
||||
});
|
||||
return withoutStaticCandidate(entity, mapped, "high");
|
||||
};
|
||||
|
||||
const candidatesWithLocation = (
|
||||
const locateEntity = (
|
||||
entity: RuntimeReconciliationEntity,
|
||||
index: CandidateIndex,
|
||||
layers: readonly ParsedStaticLayer[],
|
||||
): LocatedCandidate[] => {
|
||||
): { readonly locations: LocatedCandidate[]; readonly mapped: boolean } => {
|
||||
const located = new Map<string, LocatedCandidate>();
|
||||
let mapped = false;
|
||||
for (const layer of layers)
|
||||
for (const mapping of mappedRuntimePaths(layer, entity))
|
||||
for (const mapping of mappedRuntimePaths(layer, entity)) {
|
||||
mapped = true;
|
||||
for (const candidate of [
|
||||
...(index.byLayerCategoryPath.get(
|
||||
layerCategoryPathKey(layer.layerId, entity.staticKind, mapping.path),
|
||||
@@ -215,12 +237,16 @@ const candidatesWithLocation = (
|
||||
const key = candidateKey(candidate);
|
||||
if (!located.has(key)) located.set(key, { candidate, mapping });
|
||||
}
|
||||
return [...located.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(
|
||||
candidateKey(left.candidate),
|
||||
candidateKey(right.candidate),
|
||||
}
|
||||
return {
|
||||
locations: [...located.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(
|
||||
candidateKey(left.candidate),
|
||||
candidateKey(right.candidate),
|
||||
),
|
||||
),
|
||||
);
|
||||
mapped,
|
||||
};
|
||||
};
|
||||
|
||||
const indexCandidates = (
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, type Evidence } from "../evidence.js";
|
||||
@@ -20,7 +17,7 @@ import {
|
||||
type ManagedMemberInspection,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import {
|
||||
assessManagedGraphOmissions,
|
||||
managedGraphEvidenceCoverage,
|
||||
@@ -120,13 +117,6 @@ export interface GraphBuildState {
|
||||
readonly fieldNodes: Map<string, ApplicationNode>;
|
||||
}
|
||||
|
||||
const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed application graph canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
/** Project authenticated managed Evidence records into the application graph. */
|
||||
export const projectManagedApplicationGraph = (
|
||||
input: ProjectManagedApplicationGraphInput,
|
||||
@@ -194,7 +184,10 @@ export const projectManagedApplicationGraph = (
|
||||
};
|
||||
return managedApplicationGraphResultSchema.parse({
|
||||
...withoutId,
|
||||
projection_id: `magp_${sha256(withoutId)}`,
|
||||
projection_id: `magp_${digestCanonicalValue(
|
||||
withoutId,
|
||||
"Managed application graph",
|
||||
)}`,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
/** Build-local ECMA-335 metadata token emitted as an eight-digit lowercase hex value. */
|
||||
export const managedMetadataTokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const offsetSchema = z.number().int().min(0);
|
||||
|
||||
/** A 16-byte CLI metadata GUID without RFC 4122 version/variant restrictions. */
|
||||
@@ -26,7 +27,7 @@ const assemblyIdentitySchema = z.object({
|
||||
flags: z.number().int().min(0).max(0xffff_ffff),
|
||||
hash_algorithm: z.number().int().min(0).max(0xffff_ffff),
|
||||
public_key: publicKeyIdentitySchema,
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
});
|
||||
|
||||
@@ -36,7 +37,7 @@ const moduleIdentitySchema = z.object({
|
||||
mvid: cliMetadataGuidSchema.nullable(),
|
||||
enc_id: cliMetadataGuidSchema.nullable(),
|
||||
enc_base_id: cliMetadataGuidSchema.nullable(),
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
});
|
||||
|
||||
@@ -70,7 +71,7 @@ const assemblyReferenceSchema = z.object({
|
||||
public_key_or_token: publicKeyIdentitySchema,
|
||||
hash_value_sha256: digestSchema.nullable(),
|
||||
hash_value_length: z.number().int().min(0),
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
});
|
||||
|
||||
@@ -78,23 +79,23 @@ const managedResourceSchema = z.object({
|
||||
name: z.string(),
|
||||
flags: z.number().int().min(0).max(0xffff_ffff),
|
||||
visibility: z.enum(["public", "private", "unknown"]),
|
||||
implementation_token: tokenSchema.nullable(),
|
||||
implementation_token: managedMetadataTokenSchema.nullable(),
|
||||
embedded: z.boolean(),
|
||||
declared_offset: z.number().int().min(0).max(0xffff_ffff),
|
||||
data_length: z.number().int().min(0).nullable(),
|
||||
data_sha256: digestSchema.nullable(),
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
});
|
||||
|
||||
const customAttributeSchema = z.object({
|
||||
parent_token: tokenSchema,
|
||||
constructor_token: tokenSchema.nullable(),
|
||||
parent_token: managedMetadataTokenSchema,
|
||||
constructor_token: managedMetadataTokenSchema.nullable(),
|
||||
type_name: z.string().nullable(),
|
||||
value_length: z.number().int().min(0),
|
||||
value_sha256: digestSchema,
|
||||
decoded_fixed_string: z.string().nullable(),
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
});
|
||||
|
||||
@@ -120,7 +121,7 @@ const managedSignatureSchema = z.object({
|
||||
});
|
||||
|
||||
const managedTypeSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
namespace: z.string(),
|
||||
name: z.string(),
|
||||
@@ -132,8 +133,8 @@ const managedTypeSchema = z.object({
|
||||
"Framed nominal namespace/name/enclosing-name digest, independent of build-local rows. Null for historical Evidence or unresolved nesting; defining assemblies and CLR runtime identity are not established.",
|
||||
),
|
||||
flags: z.number().int().min(0).max(0xffff_ffff),
|
||||
extends_token: tokenSchema.nullable(),
|
||||
enclosing_type_token: tokenSchema
|
||||
extends_token: managedMetadataTokenSchema.nullable(),
|
||||
enclosing_type_token: managedMetadataTokenSchema
|
||||
.nullable()
|
||||
.default(null)
|
||||
.describe(
|
||||
@@ -153,20 +154,20 @@ const managedTypeSchema = z.object({
|
||||
|
||||
/** One ExportedType row: a type this assembly exports from another module or forwards. */
|
||||
const managedExportedTypeSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
namespace: z.string(),
|
||||
name: z.string(),
|
||||
flags: z.number().int().min(0).max(0xffff_ffff),
|
||||
forwarder: z.boolean(),
|
||||
type_def_id: z.number().int().min(0).max(0xffff_ffff),
|
||||
implementation_token: tokenSchema.nullable(),
|
||||
implementation_token: managedMetadataTokenSchema.nullable(),
|
||||
});
|
||||
|
||||
const managedFieldSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
declaring_type_token: tokenSchema.nullable(),
|
||||
declaring_type_token: managedMetadataTokenSchema.nullable(),
|
||||
declaring_type: z.string().nullable(),
|
||||
name: z.string(),
|
||||
flags: z.number().int().min(0).max(0xffff),
|
||||
@@ -179,7 +180,7 @@ const managedExceptionRegionSchema = z.object({
|
||||
try_length: z.number().int().min(0),
|
||||
handler_offset: z.number().int().min(0),
|
||||
handler_length: z.number().int().min(0),
|
||||
class_token: tokenSchema.nullable(),
|
||||
class_token: managedMetadataTokenSchema.nullable(),
|
||||
filter_offset: z.number().int().min(0).nullable(),
|
||||
});
|
||||
|
||||
@@ -210,7 +211,7 @@ const managedMethodBodySchema = z.object({
|
||||
file_offset: offsetSchema.nullable(),
|
||||
max_stack: z.number().int().min(0).nullable(),
|
||||
init_locals: z.boolean().nullable(),
|
||||
local_var_sig_token: tokenSchema.nullable(),
|
||||
local_var_sig_token: managedMetadataTokenSchema.nullable(),
|
||||
il_size: z.number().int().min(0),
|
||||
il_sha256: digestSchema
|
||||
.nullable()
|
||||
@@ -232,9 +233,9 @@ const managedMethodBodySchema = z.object({
|
||||
});
|
||||
|
||||
const managedMethodSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
declaring_type_token: tokenSchema.nullable(),
|
||||
declaring_type_token: managedMetadataTokenSchema.nullable(),
|
||||
declaring_type: z.string().nullable(),
|
||||
name: z.string(),
|
||||
rva: z.number().int().min(0).max(0xffff_ffff),
|
||||
@@ -250,9 +251,9 @@ const managedMethodSchema = z.object({
|
||||
const declaringTypeNameSchema = z.string().nullable().default(null);
|
||||
|
||||
const managedMemberReferenceSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
parent_token: tokenSchema.nullable(),
|
||||
parent_token: managedMetadataTokenSchema.nullable(),
|
||||
parent_name: declaringTypeNameSchema.describe(
|
||||
"Name of a TypeDef, TypeRef, or generic-instantiation TypeSpec parent; null for other parents or when unknown",
|
||||
),
|
||||
@@ -261,7 +262,7 @@ const managedMemberReferenceSchema = z.object({
|
||||
});
|
||||
|
||||
const managedUserStringSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
offset: offsetSchema.describe("File offset of the #US item"),
|
||||
value: z
|
||||
.string()
|
||||
@@ -271,21 +272,21 @@ const managedUserStringSchema = z.object({
|
||||
});
|
||||
|
||||
const managedModuleReferenceSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const managedNativeImportSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
mapping_flags: z.number().int().min(0).max(0xffff),
|
||||
mapping_flags_hex: z.string().regex(/^0x[0-9a-f]{4}$/u),
|
||||
member_token: tokenSchema.nullable(),
|
||||
member_token: managedMetadataTokenSchema.nullable(),
|
||||
member_kind: z.enum(["method", "field", "unknown"]),
|
||||
member_name: z.string().nullable(),
|
||||
import_name: z.string(),
|
||||
import_scope_token: tokenSchema.nullable(),
|
||||
import_scope_token: managedMetadataTokenSchema.nullable(),
|
||||
import_scope_name: z.string().nullable(),
|
||||
no_mangle: z.boolean(),
|
||||
char_set: z.enum(["not-specified", "ansi", "unicode", "auto", "unknown"]),
|
||||
@@ -310,7 +311,7 @@ const managedNativeImportSchema = z.object({
|
||||
});
|
||||
|
||||
const managedNativeImplementationSchema = z.object({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
name: z.string(),
|
||||
rva: z.number().int().min(0).max(0xffff_ffff),
|
||||
@@ -335,10 +336,10 @@ const managedNativeImplementationSchema = z.object({
|
||||
});
|
||||
|
||||
const managedCallEdgeSchema = z.object({
|
||||
caller_token: tokenSchema,
|
||||
caller_token: managedMetadataTokenSchema,
|
||||
caller: z.string().nullable(),
|
||||
opcode: z.string(),
|
||||
target_token: tokenSchema,
|
||||
target_token: managedMetadataTokenSchema,
|
||||
target_kind: z.enum(["method-def", "member-ref", "method-spec", "unknown"]),
|
||||
target_name: z
|
||||
.string()
|
||||
@@ -350,10 +351,10 @@ const managedCallEdgeSchema = z.object({
|
||||
});
|
||||
|
||||
const managedFieldAccessSchema = z.object({
|
||||
method_token: tokenSchema,
|
||||
method_token: managedMetadataTokenSchema,
|
||||
method: z.string().nullable(),
|
||||
opcode: z.string(),
|
||||
field_token: tokenSchema,
|
||||
field_token: managedMetadataTokenSchema,
|
||||
field_name: z.string().nullable(),
|
||||
field_declaring_type: declaringTypeNameSchema.describe(
|
||||
"Declaring type of the accessed field; null when unknown",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import { emptyArraySchema } from "../emptyArraySchema.js";
|
||||
import { evidenceSchema } from "../evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedMetadataTokenSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import {
|
||||
@@ -17,13 +19,12 @@ import {
|
||||
buildFieldItems,
|
||||
buildMethodItems,
|
||||
} from "./managedMemberComparisonItems.js";
|
||||
import { keyMembers, sha256 } from "./managedMemberComparisonMatch.js";
|
||||
import { keyMembers } from "./managedMemberComparisonMatch.js";
|
||||
import { hasSignatureResolution } from "./managedMemberSignatureIdentity.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const boundedTextSchema = z.string().min(1);
|
||||
|
||||
/** Two authenticated managed member observations. */
|
||||
@@ -47,7 +48,7 @@ const concreteMatchBasisSchema = z.enum([
|
||||
"structural-method-shape",
|
||||
"field-signature",
|
||||
]);
|
||||
const candidateTokensSchema = z.array(tokenSchema).min(1);
|
||||
const candidateTokensSchema = z.array(managedMetadataTokenSchema).min(1);
|
||||
const matchedComparisonSchema = z.strictObject({
|
||||
status: z.literal("matched"),
|
||||
basis: concreteMatchBasisSchema,
|
||||
@@ -70,7 +71,7 @@ const ambiguousComparisonSchema = z.strictObject({
|
||||
candidate_right_tokens: candidateTokensSchema,
|
||||
});
|
||||
const memberIdentitySchema = z.strictObject({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
declaring_type: z.string().nullable(),
|
||||
name: z.string(),
|
||||
signature_sha256: digestSchema,
|
||||
@@ -247,12 +248,15 @@ export const compareManagedMembers = (
|
||||
});
|
||||
const limitations = comparisonLimitations(left.result, right.result);
|
||||
const result = {
|
||||
comparison_id: `mmc_${sha256({
|
||||
left: left.evidenceId,
|
||||
right: right.evidenceId,
|
||||
methods: [...methodItems],
|
||||
fields: [...fieldItems],
|
||||
})}`,
|
||||
comparison_id: `mmc_${digestCanonicalValue(
|
||||
{
|
||||
left: left.evidenceId,
|
||||
right: right.evidenceId,
|
||||
methods: [...methodItems],
|
||||
fields: [...fieldItems],
|
||||
},
|
||||
"Managed comparison",
|
||||
)}`,
|
||||
algorithm: {
|
||||
name: "rea-managed-member-comparison" as const,
|
||||
token_identity: "build-local" as const,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import type { ManagedMemberInspection } from "./managedArtifact.js";
|
||||
import type { ManagedMemberComparisonResult } from "./managedMemberComparison.js";
|
||||
import {
|
||||
type ManagedFieldMatches,
|
||||
type ManagedMethodMatches,
|
||||
sha256,
|
||||
import type {
|
||||
ManagedFieldMatches,
|
||||
ManagedMethodMatches,
|
||||
} from "./managedMemberComparisonMatch.js";
|
||||
import { signatureIdentity } from "./managedMemberSignatureIdentity.js";
|
||||
|
||||
@@ -180,7 +180,7 @@ const methodOnlyItem = (
|
||||
inventory: "method",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: member.left.token, side: "left" })}`,
|
||||
item_id: `mmc_method_${digestCanonicalValue({ token: member.left.token, side: "left" }, "Managed comparison")}`,
|
||||
status: limitations.length === 0 ? "removed" : "unknown",
|
||||
left: methodIdentity(member.left),
|
||||
right: null,
|
||||
@@ -203,7 +203,7 @@ const methodOnlyItem = (
|
||||
inventory: "method",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_method_${sha256({ token: member.right.token, side: "right" })}`,
|
||||
item_id: `mmc_method_${digestCanonicalValue({ token: member.right.token, side: "right" }, "Managed comparison")}`,
|
||||
status: limitations.length === 0 ? "added" : "unknown",
|
||||
left: null,
|
||||
right: methodIdentity(member.right),
|
||||
@@ -377,7 +377,7 @@ const fieldOnlyItem = (
|
||||
inventory: "field",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: member.left.token, side: "left" })}`,
|
||||
item_id: `mmc_field_${digestCanonicalValue({ token: member.left.token, side: "left" }, "Managed comparison")}`,
|
||||
status: limitations.length === 0 ? "removed" : "unknown",
|
||||
left: fieldIdentity(member.left),
|
||||
right: null,
|
||||
@@ -399,7 +399,7 @@ const fieldOnlyItem = (
|
||||
inventory: "field",
|
||||
});
|
||||
return {
|
||||
item_id: `mmc_field_${sha256({ token: member.right.token, side: "right" })}`,
|
||||
item_id: `mmc_field_${digestCanonicalValue({ token: member.right.token, side: "right" }, "Managed comparison")}`,
|
||||
status: limitations.length === 0 ? "added" : "unknown",
|
||||
left: null,
|
||||
right: fieldIdentity(member.right),
|
||||
@@ -448,11 +448,14 @@ export const buildMethodItems = (
|
||||
: observedDimensions;
|
||||
const observedChange = observedDimensions.length > 0;
|
||||
items.push({
|
||||
item_id: `mmc_method_${sha256({
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
basis: pair.basis,
|
||||
})}`,
|
||||
item_id: `mmc_method_${digestCanonicalValue(
|
||||
{
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
basis: pair.basis,
|
||||
},
|
||||
"Managed comparison",
|
||||
)}`,
|
||||
status: observedChange
|
||||
? "changed"
|
||||
: bodyUnknown || signatureLimitations.length > 0
|
||||
@@ -481,11 +484,14 @@ export const buildMethodItems = (
|
||||
}
|
||||
for (const ambiguous of matches.ambiguous) {
|
||||
items.push({
|
||||
item_id: `mmc_method_${sha256({
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
basis: ambiguous.basis,
|
||||
})}`,
|
||||
item_id: `mmc_method_${digestCanonicalValue(
|
||||
{
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
basis: ambiguous.basis,
|
||||
},
|
||||
"Managed comparison",
|
||||
)}`,
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: null,
|
||||
@@ -551,10 +557,13 @@ export const buildFieldItems = (
|
||||
signatureIdentity(pair.right.item.signature) ||
|
||||
pair.left.item.flags !== pair.right.item.flags;
|
||||
items.push({
|
||||
item_id: `mmc_field_${sha256({
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
})}`,
|
||||
item_id: `mmc_field_${digestCanonicalValue(
|
||||
{
|
||||
left: pair.left.item.token,
|
||||
right: pair.right.item.token,
|
||||
},
|
||||
"Managed comparison",
|
||||
)}`,
|
||||
status: changed
|
||||
? "changed"
|
||||
: signatureLimitations.length > 0
|
||||
@@ -575,10 +584,13 @@ export const buildFieldItems = (
|
||||
}
|
||||
for (const ambiguous of matches.ambiguous)
|
||||
items.push({
|
||||
item_id: `mmc_field_${sha256({
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
})}`,
|
||||
item_id: `mmc_field_${digestCanonicalValue(
|
||||
{
|
||||
left: ambiguous.left.map(({ item }) => item.token),
|
||||
right: ambiguous.right.map(({ item }) => item.token),
|
||||
},
|
||||
"Managed comparison",
|
||||
)}`,
|
||||
status: "unknown",
|
||||
left: null,
|
||||
right: null,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import {
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
@@ -15,15 +12,6 @@ import {
|
||||
comparableSides,
|
||||
signatureIdentity,
|
||||
} from "./managedMemberSignatureIdentity.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
|
||||
export const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed comparison canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
type Method = ManagedMemberInspection["methods"][number];
|
||||
type Field = ManagedMemberInspection["fields"][number];
|
||||
|
||||
@@ -69,49 +57,58 @@ const keyMethod = (item: Method, declared: DeclaredNameOf): Keyed<Method> => ({
|
||||
item.body.status === "present" &&
|
||||
item.body.il_sha256 !== null &&
|
||||
item.body.normalized_il_sha256 !== null
|
||||
? sha256([
|
||||
"method-exact",
|
||||
signatureIdentity(item.signature),
|
||||
item.body.il_sha256,
|
||||
item.body.normalized_il_sha256,
|
||||
])
|
||||
? digestCanonicalValue(
|
||||
[
|
||||
"method-exact",
|
||||
signatureIdentity(item.signature),
|
||||
item.body.il_sha256,
|
||||
item.body.normalized_il_sha256,
|
||||
],
|
||||
"Managed comparison",
|
||||
)
|
||||
: null,
|
||||
// The blob, with type operands named, is the exact signature; decoding
|
||||
// only gates the shape keys.
|
||||
signatureKey:
|
||||
declared(item) === null
|
||||
? null
|
||||
: sha256([
|
||||
"method-signature",
|
||||
declared(item),
|
||||
item.name,
|
||||
signatureIdentity(item.signature),
|
||||
]),
|
||||
: digestCanonicalValue(
|
||||
[
|
||||
"method-signature",
|
||||
declared(item),
|
||||
item.name,
|
||||
signatureIdentity(item.signature),
|
||||
],
|
||||
"Managed comparison",
|
||||
),
|
||||
structuralKey:
|
||||
item.signature.parse_status === "decoded" && item.body.status === "present"
|
||||
? sha256([
|
||||
"method-structural",
|
||||
// Decoded type strings carry build-local rows; the identity names them.
|
||||
signatureIdentity(item.signature),
|
||||
item.body.status,
|
||||
item.body.header_format,
|
||||
item.body.il_size,
|
||||
item.body.max_stack,
|
||||
item.body.init_locals,
|
||||
item.body.opcode_counts,
|
||||
item.body.anchors.map(({ opcode, operand_kind, operand }) => [
|
||||
opcode,
|
||||
operand_kind,
|
||||
operand_kind === "constant" ? operand : null,
|
||||
]),
|
||||
item.body.exception_regions.map((region) => [
|
||||
region.flags,
|
||||
region.try_length,
|
||||
region.handler_length,
|
||||
region.class_token === null ? null : "type-token",
|
||||
region.filter_offset === null ? null : "filter",
|
||||
]),
|
||||
])
|
||||
? digestCanonicalValue(
|
||||
[
|
||||
"method-structural",
|
||||
// Decoded type strings carry build-local rows; the identity names them.
|
||||
signatureIdentity(item.signature),
|
||||
item.body.status,
|
||||
item.body.header_format,
|
||||
item.body.il_size,
|
||||
item.body.max_stack,
|
||||
item.body.init_locals,
|
||||
item.body.opcode_counts,
|
||||
item.body.anchors.map(({ opcode, operand_kind, operand }) => [
|
||||
opcode,
|
||||
operand_kind,
|
||||
operand_kind === "constant" ? operand : null,
|
||||
]),
|
||||
item.body.exception_regions.map((region) => [
|
||||
region.flags,
|
||||
region.try_length,
|
||||
region.handler_length,
|
||||
region.class_token === null ? null : "type-token",
|
||||
region.filter_offset === null ? null : "filter",
|
||||
]),
|
||||
],
|
||||
"Managed comparison",
|
||||
)
|
||||
: null,
|
||||
});
|
||||
|
||||
@@ -122,25 +119,34 @@ const keyField = (item: Field, declared: DeclaredNameOf): Keyed<Field> => ({
|
||||
signatureId: signatureIdentity(item.signature),
|
||||
exactKey:
|
||||
item.signature.parse_status === "decoded"
|
||||
? sha256(["field-exact", signatureIdentity(item.signature)])
|
||||
? digestCanonicalValue(
|
||||
["field-exact", signatureIdentity(item.signature)],
|
||||
"Managed comparison",
|
||||
)
|
||||
: null,
|
||||
// Only an undecoded field reaches this round: a decoded field with the
|
||||
// same signature already met its counterpart in the exact round.
|
||||
signatureKey:
|
||||
declared(item) === null
|
||||
? null
|
||||
: sha256([
|
||||
"field-signature",
|
||||
declared(item),
|
||||
item.name,
|
||||
signatureIdentity(item.signature),
|
||||
]),
|
||||
structuralKey: sha256([
|
||||
"field-structural",
|
||||
item.signature.kind,
|
||||
item.signature.field_type,
|
||||
item.flags,
|
||||
]),
|
||||
: digestCanonicalValue(
|
||||
[
|
||||
"field-signature",
|
||||
declared(item),
|
||||
item.name,
|
||||
signatureIdentity(item.signature),
|
||||
],
|
||||
"Managed comparison",
|
||||
),
|
||||
structuralKey: digestCanonicalValue(
|
||||
[
|
||||
"field-structural",
|
||||
item.signature.kind,
|
||||
item.signature.field_type,
|
||||
item.flags,
|
||||
],
|
||||
"Managed comparison",
|
||||
),
|
||||
});
|
||||
|
||||
const matchMethods = (
|
||||
@@ -237,7 +243,8 @@ export const identityOf = ({
|
||||
}: {
|
||||
readonly declaring_type: DeclaredName;
|
||||
readonly name: string;
|
||||
}): string => sha256([declaring_type, name]);
|
||||
}): string =>
|
||||
digestCanonicalValue([declaring_type, name], "Managed comparison");
|
||||
|
||||
/** Narrow a key to members of one declared type and name. */
|
||||
const declaredAs =
|
||||
@@ -246,7 +253,7 @@ const declaredAs =
|
||||
const value = key(item);
|
||||
return value === null || item.identityUnavailable
|
||||
? null
|
||||
: sha256([item.identity, value]);
|
||||
: digestCanonicalValue([item.identity, value], "Managed comparison");
|
||||
};
|
||||
|
||||
const matchByKeys = <Item extends Declared>({
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import type { Evidence } from "../evidence.js";
|
||||
import {
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import { parseManagedInspectionEvidence } from "./managedInspectionEvidence.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import {
|
||||
managedNativeVerificationResultSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
@@ -20,13 +16,6 @@ import {
|
||||
verifyPinvoke,
|
||||
} from "./managedNativeVerificationMatch.js";
|
||||
|
||||
const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed/native verification canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
const parseManagedBoundary = (
|
||||
input: ManagedNativeVerificationInput,
|
||||
): {
|
||||
@@ -65,6 +54,9 @@ export const verifyManagedNativeBoundaries = (
|
||||
});
|
||||
return managedNativeVerificationResultSchema.parse({
|
||||
...withoutId,
|
||||
verification_id: `mnv_${sha256(withoutId)}`,
|
||||
verification_id: `mnv_${digestCanonicalValue(
|
||||
withoutId,
|
||||
"Managed/native verification",
|
||||
)}`,
|
||||
});
|
||||
};
|
||||
@@ -1,13 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { parseEvidence } from "../evidence.js";
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import { functionDossierSchema } from "../hopperValues.js";
|
||||
import { inspectMachoSchema } from "../native/nativeInspection.js";
|
||||
import type { ManagedNativeBoundaryInspection } from "./managedArtifact.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import {
|
||||
pinvokeVerificationSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
@@ -16,13 +13,6 @@ import {
|
||||
type PinvokeVerification,
|
||||
} from "./managedNativeVerificationSchemas.js";
|
||||
|
||||
const digest = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed/native verification canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
type PinvokeImport = ManagedNativeBoundaryInspection["pinvoke_imports"][number];
|
||||
type VerifiedPinvoke = {
|
||||
readonly verification: PinvokeVerification;
|
||||
@@ -326,13 +316,16 @@ export const verifyPinvoke = ({
|
||||
const limitations = pinvokeLimitations(selection.match, item);
|
||||
return {
|
||||
verification: pinvokeVerificationSchema.parse({
|
||||
item_id: `mnv_pinvoke_${digest({
|
||||
managedEvidenceId,
|
||||
token: item.token,
|
||||
importName: item.import_name,
|
||||
importScope: item.import_scope_name,
|
||||
candidates: selection.match.candidates,
|
||||
})}`,
|
||||
item_id: `mnv_pinvoke_${digestCanonicalValue(
|
||||
{
|
||||
managedEvidenceId,
|
||||
token: item.token,
|
||||
importName: item.import_name,
|
||||
importScope: item.import_scope_name,
|
||||
candidates: selection.match.candidates,
|
||||
},
|
||||
"Managed/native verification",
|
||||
)}`,
|
||||
managed,
|
||||
...selection.match,
|
||||
evidence_links: [
|
||||
|
||||
@@ -2,12 +2,14 @@ import { z } from "zod";
|
||||
|
||||
import { emptyArraySchema } from "../emptyArraySchema.js";
|
||||
import { evidenceSchema } from "../evidence.js";
|
||||
import { cliMetadataGuidSchema } from "./managedArtifact.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedMetadataTokenSchema,
|
||||
} from "./managedArtifact.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const textSchema = z.string().min(1);
|
||||
|
||||
const nativeSymbolSchema = z.strictObject({
|
||||
@@ -25,8 +27,8 @@ export type NativeSymbol = z.infer<typeof nativeSymbolSchema>;
|
||||
const pinvokeVerificationContextShape = {
|
||||
item_id: prefixedDigestSchema("mnv_pinvoke"),
|
||||
managed: z.strictObject({
|
||||
token: tokenSchema,
|
||||
member_token: tokenSchema.nullable(),
|
||||
token: managedMetadataTokenSchema,
|
||||
member_token: managedMetadataTokenSchema.nullable(),
|
||||
member_name: z.string().nullable(),
|
||||
import_name: z.string().min(1),
|
||||
import_scope_name: z.string().nullable(),
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, type Evidence } from "../evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedMetadataTokenSchema,
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { digestCanonicalValue } from "../canonicalDigest.js";
|
||||
import { digestSchema, prefixedDigestSchema } from "../digests.js";
|
||||
import { parseManagedInspectionEvidence } from "./managedInspectionEvidence.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const textSchema = z.string().min(1);
|
||||
|
||||
const decompilerSchema = z.strictObject({
|
||||
@@ -26,7 +25,7 @@ const decompilerSchema = z.strictObject({
|
||||
});
|
||||
|
||||
const managedReconstructionMethodInputSchema = z.strictObject({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
signature_sha256: digestSchema,
|
||||
normalized_il_sha256: digestSchema.nullable(),
|
||||
reconstruction: z.strictObject({
|
||||
@@ -52,7 +51,7 @@ export const managedReconstructionImportInputSchema = z.strictObject({
|
||||
});
|
||||
|
||||
const importedMethodSchema = z.strictObject({
|
||||
token: tokenSchema,
|
||||
token: managedMetadataTokenSchema,
|
||||
declaring_type: z.string().nullable(),
|
||||
name: z.string(),
|
||||
signature_sha256: digestSchema,
|
||||
@@ -122,13 +121,6 @@ type StaticObservation =
|
||||
const sha256Text = (value: string): string =>
|
||||
createHash("sha256").update(value).digest("hex");
|
||||
|
||||
const sha256Json = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
if (serialized === undefined)
|
||||
throw new TypeError("Managed reconstruction canonicalization failed");
|
||||
return createHash("sha256").update(serialized).digest("hex");
|
||||
};
|
||||
|
||||
const countLines = (text: string): number =>
|
||||
text.length === 0 ? 0 : text.split(/\r\n|\r|\n/u).length;
|
||||
|
||||
@@ -227,7 +219,10 @@ const buildDecompilerRecord = (
|
||||
decompiler: ManagedReconstructionImportInput["decompiler"],
|
||||
): ManagedReconstructionImportResult["decompiler"] => ({
|
||||
...decompiler,
|
||||
options_sha256: sha256Json(decompiler.options),
|
||||
options_sha256: digestCanonicalValue(
|
||||
decompiler.options,
|
||||
"Managed reconstruction",
|
||||
),
|
||||
});
|
||||
|
||||
const RECONSTRUCTION_LIMITATIONS = [
|
||||
@@ -282,6 +277,9 @@ export const importManagedReconstruction = (
|
||||
);
|
||||
return managedReconstructionImportResultSchema.parse({
|
||||
...withoutId,
|
||||
reconstruction_id: `mre_${sha256Json(withoutId)}`,
|
||||
reconstruction_id: `mre_${digestCanonicalValue(
|
||||
withoutId,
|
||||
"Managed reconstruction",
|
||||
)}`,
|
||||
});
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "./webBundleAnalyzer.js";
|
||||
import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js";
|
||||
import { webBundleAnalysisSchema } from "./webBundleAnalysis.js";
|
||||
import { isIncludedScript } from "./webBundleAnalyzerInspection.js";
|
||||
import { isIncludedScript } from "./webBundleAnalyzerState.js";
|
||||
import { webBundleScriptAnalysisSchema } from "./webBundleScriptAnalysis.js";
|
||||
import { createWebTextArtifact } from "./webContentArtifact.js";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { WebPageInspection } from "./browserObservationSchemas.js";
|
||||
import { analyzeScript } from "./webBundleAnalyzerInspection.js";
|
||||
import {
|
||||
analyzeScript,
|
||||
emptyAccumulator,
|
||||
createWebBundleAnalysisAccumulator,
|
||||
isIncludedScript,
|
||||
type IncludedScript,
|
||||
} from "./webBundleAnalyzerInspection.js";
|
||||
} from "./webBundleAnalyzerState.js";
|
||||
import { buildCapturedWebBundleAnalysis } from "./webBundleAnalyzerResult.js";
|
||||
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
|
||||
import {
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
export const analyzeCapturedWebScript = (
|
||||
script: IncludedScript,
|
||||
): WebBundleScriptAnalysis => {
|
||||
const accumulator = emptyAccumulator();
|
||||
const accumulator = createWebBundleAnalysisAccumulator();
|
||||
analyzeScript(script, accumulator);
|
||||
const failed = accumulator.failedScripts.length > 0;
|
||||
const recovered = accumulator.recoveredScripts.length > 0;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type {
|
||||
AnalysisAccumulator,
|
||||
IncludedScript,
|
||||
} from "./webBundleAnalyzerState.js";
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import { sanitizeEndpointCandidate } from "./browserObservation.js";
|
||||
import { compositeKey } from "./unicodeCodePointOrder.js";
|
||||
import type { WebPageInspection } from "./browserObservationSchemas.js";
|
||||
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
|
||||
import { traverseJavaScriptAst } from "./javascript/javascriptSemanticTraversal.js";
|
||||
import { indexJavaScriptBindingsWithReceiversSteps } from "./javascript/javascriptSemanticAnalysis.js";
|
||||
@@ -37,26 +40,6 @@ type ChunkEdge = BundleObservations["chunks"]["edges"][number];
|
||||
type Finding = BundleObservations["routes"][number];
|
||||
type WebMcpDeclaration = BundleObservations["webmcp_declarations"][number];
|
||||
type Inference = WebBundleAnalysis["inferences"][number];
|
||||
type BrowserScript = WebPageInspection["scripts"]["items"][number];
|
||||
type IncludedSource = Extract<BrowserScript["source"], { included: true }>;
|
||||
export type IncludedScript = Omit<BrowserScript, "source"> & {
|
||||
readonly source: IncludedSource;
|
||||
};
|
||||
|
||||
export interface AnalysisAccumulator {
|
||||
readonly edges: ChunkEdge[];
|
||||
readonly routes: Finding[];
|
||||
readonly endpoints: Finding[];
|
||||
readonly webMcp: WebMcpDeclaration[];
|
||||
readonly inferences: Inference[];
|
||||
readonly seen: Set<string>;
|
||||
visitedNodes: number;
|
||||
parsedScripts: number;
|
||||
readonly failedScripts: string[];
|
||||
readonly recoveredScripts: string[];
|
||||
readonly widenedScripts: string[];
|
||||
}
|
||||
|
||||
export const analyzeScript = (
|
||||
script: IncludedScript,
|
||||
accumulator: AnalysisAccumulator,
|
||||
@@ -501,24 +484,6 @@ const basis = (script: IncludedScript, detector: string) => ({
|
||||
detector,
|
||||
});
|
||||
|
||||
export const emptyAccumulator = (): AnalysisAccumulator => ({
|
||||
edges: [],
|
||||
routes: [],
|
||||
endpoints: [],
|
||||
webMcp: [],
|
||||
inferences: [],
|
||||
seen: new Set(),
|
||||
visitedNodes: 0,
|
||||
parsedScripts: 0,
|
||||
failedScripts: [],
|
||||
recoveredScripts: [],
|
||||
widenedScripts: [],
|
||||
});
|
||||
|
||||
export const isIncludedScript = (
|
||||
script: BrowserScript,
|
||||
): script is IncludedScript => script.source.included;
|
||||
|
||||
const routeCallNames = [
|
||||
"route",
|
||||
"addRoute",
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
webBundleAnalysisSchema,
|
||||
type WebBundleAnalysis,
|
||||
} from "./webBundleAnalysis.js";
|
||||
import type {
|
||||
AnalysisAccumulator,
|
||||
IncludedScript,
|
||||
} from "./webBundleAnalyzerInspection.js";
|
||||
import {
|
||||
createWebBundleAnalysisAccumulator,
|
||||
isIncludedScript,
|
||||
type AnalysisAccumulator,
|
||||
type IncludedScript,
|
||||
} from "./webBundleAnalyzerState.js";
|
||||
import {
|
||||
webBundleScriptAnalysisSchema,
|
||||
type WebBundleScriptAnalysis,
|
||||
@@ -29,9 +31,7 @@ export const buildCapturedWebBundleAnalysis = (
|
||||
);
|
||||
byKey.set(result.script_key, result);
|
||||
}
|
||||
const sourceScripts = inspection.scripts.items.filter(
|
||||
(script): script is IncludedScript => script.source.included,
|
||||
);
|
||||
const sourceScripts = inspection.scripts.items.filter(isIncludedScript);
|
||||
const expectedKeys = new Set(
|
||||
sourceScripts.map(({ script_key }) => script_key),
|
||||
);
|
||||
@@ -145,19 +145,7 @@ const mergeScriptResults = (
|
||||
{ readonly status: "analyzed" }
|
||||
>[],
|
||||
): AnalysisAccumulator => {
|
||||
const accumulator: AnalysisAccumulator = {
|
||||
edges: [],
|
||||
routes: [],
|
||||
endpoints: [],
|
||||
webMcp: [],
|
||||
inferences: [],
|
||||
seen: new Set(),
|
||||
visitedNodes: 0,
|
||||
parsedScripts: 0,
|
||||
failedScripts: [],
|
||||
recoveredScripts: [],
|
||||
widenedScripts: [],
|
||||
};
|
||||
const accumulator = createWebBundleAnalysisAccumulator();
|
||||
for (const result of results) {
|
||||
accumulator.visitedNodes += result.visited_ast_nodes;
|
||||
if (result.parse_status === "failed") {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { WebPageInspection } from "./browserObservationSchemas.js";
|
||||
import type { WebBundleAnalysis } from "./webBundleAnalysis.js";
|
||||
|
||||
type BundleObservations = WebBundleAnalysis["observations"];
|
||||
type BrowserScript = WebPageInspection["scripts"]["items"][number];
|
||||
type IncludedSource = Extract<BrowserScript["source"], { included: true }>;
|
||||
|
||||
/** Captured script whose source bytes are available for inert analysis. */
|
||||
export type IncludedScript = Omit<BrowserScript, "source"> & {
|
||||
readonly source: IncludedSource;
|
||||
};
|
||||
|
||||
/** Mutable facts shared by per-script inspection and bundle result assembly. */
|
||||
export interface AnalysisAccumulator {
|
||||
readonly edges: BundleObservations["chunks"]["edges"];
|
||||
readonly routes: BundleObservations["routes"];
|
||||
readonly endpoints: BundleObservations["endpoints"];
|
||||
readonly webMcp: BundleObservations["webmcp_declarations"];
|
||||
readonly inferences: WebBundleAnalysis["inferences"];
|
||||
readonly seen: Set<string>;
|
||||
visitedNodes: number;
|
||||
parsedScripts: number;
|
||||
readonly failedScripts: string[];
|
||||
readonly recoveredScripts: string[];
|
||||
readonly widenedScripts: string[];
|
||||
}
|
||||
|
||||
export const createWebBundleAnalysisAccumulator = (): AnalysisAccumulator => ({
|
||||
edges: [],
|
||||
routes: [],
|
||||
endpoints: [],
|
||||
webMcp: [],
|
||||
inferences: [],
|
||||
seen: new Set(),
|
||||
visitedNodes: 0,
|
||||
parsedScripts: 0,
|
||||
failedScripts: [],
|
||||
recoveredScripts: [],
|
||||
widenedScripts: [],
|
||||
});
|
||||
|
||||
export const isIncludedScript = (
|
||||
script: BrowserScript,
|
||||
): script is IncludedScript => script.source.included;
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type AnalysisClient,
|
||||
type AnalysisClientContext,
|
||||
type AnalysisOperation,
|
||||
type ExecutionOptions,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { AppConfig } from "../config/types.js";
|
||||
import {
|
||||
@@ -478,6 +479,24 @@ export const createGhidraProviderClient = (input: {
|
||||
operation,
|
||||
);
|
||||
};
|
||||
const startSession = async (
|
||||
operation: AnalysisOperation,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<GhidraSessionInfo, AnalysisError>> => {
|
||||
const started = await client.start(options?.signal, options?.progress);
|
||||
if (!started.ok)
|
||||
return err(
|
||||
projectSessionError(
|
||||
operation,
|
||||
started.error,
|
||||
config.ghidraStartupTimeoutMs,
|
||||
lostProviderHealth(),
|
||||
),
|
||||
);
|
||||
providerStarted = true;
|
||||
const failed = await checkSessionObservations(operation, started.value);
|
||||
return failed === undefined ? started : err(failed);
|
||||
};
|
||||
const releaseLimitation = unverifiedGhidraBuildLimitation(
|
||||
prerequisites.value.providerVersion,
|
||||
);
|
||||
@@ -603,19 +622,8 @@ export const createGhidraProviderClient = (input: {
|
||||
),
|
||||
);
|
||||
if (operation === "health") {
|
||||
const started = await client.start(options?.signal, options?.progress);
|
||||
if (started.ok) providerStarted = true;
|
||||
if (!started.ok)
|
||||
return err(
|
||||
projectSessionError(
|
||||
operation,
|
||||
started.error,
|
||||
config.ghidraStartupTimeoutMs,
|
||||
lostProviderHealth(),
|
||||
),
|
||||
);
|
||||
const failed = await checkSessionObservations(operation, started.value);
|
||||
if (failed !== undefined) return err(failed);
|
||||
const started = await startSession(operation, options);
|
||||
if (!started.ok) return started;
|
||||
return ok(
|
||||
createAnalysisExecution(started.value, committedProfile.provider, {
|
||||
analysisProfile: committedProfile,
|
||||
@@ -633,19 +641,8 @@ export const createGhidraProviderClient = (input: {
|
||||
seeds !== undefined ||
|
||||
softFloat
|
||||
) {
|
||||
const started = await client.start(options?.signal, options?.progress);
|
||||
if (started.ok) providerStarted = true;
|
||||
if (!started.ok)
|
||||
return err(
|
||||
projectSessionError(
|
||||
operation,
|
||||
started.error,
|
||||
config.ghidraStartupTimeoutMs,
|
||||
lostProviderHealth(),
|
||||
),
|
||||
);
|
||||
const failed = await checkSessionObservations(operation, started.value);
|
||||
if (failed !== undefined) return err(failed);
|
||||
const started = await startSession(operation, options);
|
||||
if (!started.ok) return started;
|
||||
}
|
||||
const called = await client.callTool(operation, input.value, {
|
||||
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
||||
|
||||
@@ -19,11 +19,6 @@ import type {
|
||||
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
|
||||
import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS } from "../domain/native/nativeDeadlines.js";
|
||||
|
||||
/**
|
||||
* Omitted `native_command_timeout_ms`. Distinct from objdump/rizin
|
||||
* `command_timeout_ms`, which is a separate 120000 ms one-shot budget.
|
||||
*/
|
||||
export const NATIVE_COMMAND_TIMEOUT_MS = DEFAULT_NATIVE_COMMAND_TIMEOUT_MS;
|
||||
/** Retain complete command output up to a bounded aggregate size. */
|
||||
export const NATIVE_COMMAND_OUTPUT_BUDGET_BYTES = 64 * 1024 * 1024;
|
||||
/**
|
||||
@@ -131,7 +126,7 @@ export interface NativeCommandFailureCapture {
|
||||
export interface NativeCommandOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
readonly acceptNonZero?: boolean;
|
||||
/** Selected wall clock; omitted uses `NATIVE_COMMAND_TIMEOUT_MS`. */
|
||||
/** Selected wall clock; omitted uses `DEFAULT_NATIVE_COMMAND_TIMEOUT_MS`. */
|
||||
readonly timeoutMs?: number;
|
||||
}
|
||||
|
||||
@@ -155,7 +150,7 @@ export type NativeToolResolver = (
|
||||
tool: string,
|
||||
signal?: AbortSignal,
|
||||
launcher?: NativeCommandLauncher,
|
||||
/** Selected command deadline. Omitted uses `NATIVE_COMMAND_TIMEOUT_MS`. */
|
||||
/** Selected command deadline. Omitted uses `DEFAULT_NATIVE_COMMAND_TIMEOUT_MS`. */
|
||||
timeoutMs?: number,
|
||||
) => Promise<Result<ResolvedTool, NativeCommandFailure>>;
|
||||
|
||||
@@ -533,7 +528,7 @@ const captureProcess = async (
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
},
|
||||
{
|
||||
timeoutMs: options.timeoutMs ?? NATIVE_COMMAND_TIMEOUT_MS,
|
||||
timeoutMs: options.timeoutMs ?? DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
diagnosticBytes: NATIVE_COMMAND_OUTPUT_BUDGET_BYTES,
|
||||
acceptNonZeroExit: options.acceptNonZero === true,
|
||||
},
|
||||
|
||||
@@ -14,10 +14,8 @@ import type {
|
||||
NativeCommandOptions,
|
||||
NativeCommandRunner,
|
||||
} from "./CommandRunner.js";
|
||||
import {
|
||||
NATIVE_COMMAND_TIMEOUT_MS,
|
||||
XcrunCommandRunner,
|
||||
} from "./CommandRunner.js";
|
||||
import { XcrunCommandRunner } from "./CommandRunner.js";
|
||||
import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS } from "../domain/native/nativeDeadlines.js";
|
||||
import { NativeMacOSProvider } from "./NativeMacOSProvider.js";
|
||||
|
||||
vi.mock("../process/OwnedCommand.js", async (original) => ({
|
||||
@@ -59,7 +57,7 @@ describe("native_command_timeout_ms", () => {
|
||||
const result = await execute(runner, {});
|
||||
expect(result.ok, JSON.stringify(result)).toBe(true);
|
||||
expect(runner.timeouts.every((timeout) => timeout === 60_000)).toBe(true);
|
||||
expect(NATIVE_COMMAND_TIMEOUT_MS).toBe(60_000);
|
||||
expect(DEFAULT_NATIVE_COMMAND_TIMEOUT_MS).toBe(60_000);
|
||||
expect(runner.timeouts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_NATIVE_UI_DEADLINE_MS,
|
||||
MAX_NATIVE_TIMER_MS,
|
||||
fixedNativeAllowance,
|
||||
@@ -95,7 +96,6 @@ import {
|
||||
} from "../domain/native/nativeInspection.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
NATIVE_COMMAND_TIMEOUT_MS,
|
||||
NATIVE_COMMAND_OUTPUT_BUDGET_BYTES,
|
||||
NATIVE_FAILURE_DIAGNOSTIC_BYTES,
|
||||
NativeCommandFailure,
|
||||
@@ -603,7 +603,7 @@ class NativeMacOSClient implements AnalysisClient {
|
||||
): Promise<Result<NativeObservation, AnalysisError>> {
|
||||
const timeout = readNativeDeadline(
|
||||
parameters.native_command_timeout_ms,
|
||||
NATIVE_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
);
|
||||
if (!timeout.ok)
|
||||
return Promise.resolve(
|
||||
@@ -1295,7 +1295,7 @@ const isNativeOperation = (
|
||||
const translateCommandFailure = (
|
||||
operation: NativeToolName,
|
||||
failure: NativeCommandFailure,
|
||||
timeoutMs = NATIVE_COMMAND_TIMEOUT_MS,
|
||||
timeoutMs = DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
): AnalysisError => {
|
||||
const cleanupOwners = [
|
||||
...failure.cleanupFailures,
|
||||
@@ -1367,7 +1367,7 @@ const translateCommandFailure = (
|
||||
timeoutMs,
|
||||
setting: "native_command_timeout_ms",
|
||||
cliFlag: "--native-command-timeout-ms",
|
||||
defaultMs: NATIVE_COMMAND_TIMEOUT_MS,
|
||||
defaultMs: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
phase: "macOS native command deadline",
|
||||
unchanged: `The ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)}-byte command output allowance and the ${String(NATIVE_FAILURE_DIAGNOSTIC_BYTES)}-byte failure-diagnostic excerpt have no runtime override.`,
|
||||
}),
|
||||
@@ -1429,7 +1429,7 @@ const translateCommandFailure = (
|
||||
timeoutMs,
|
||||
setting: "native_command_timeout_ms",
|
||||
cliFlag: "--native-command-timeout-ms",
|
||||
defaultMs: NATIVE_COMMAND_TIMEOUT_MS,
|
||||
defaultMs: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
phase: "macOS native command deadline",
|
||||
unchanged: `The ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)}-byte command output allowance and the ${String(NATIVE_FAILURE_DIAGNOSTIC_BYTES)}-byte failure-diagnostic excerpt have no runtime override.`,
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
import { createNativeUiHelperRuntime } from "./NativeUiHelperRuntime.js";
|
||||
import { NATIVE_UI_DEADLINE_MS } from "./NativeUiInterruptionControl.js";
|
||||
import { DEFAULT_NATIVE_UI_DEADLINE_MS } from "../domain/native/nativeDeadlines.js";
|
||||
|
||||
vi.mock("../process/ExecFileOutput.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../process/ExecFileOutput.js")>()),
|
||||
@@ -37,7 +37,7 @@ it.each([250_000, undefined])(
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
const expected = deadlineMs ?? NATIVE_UI_DEADLINE_MS;
|
||||
const expected = deadlineMs ?? DEFAULT_NATIVE_UI_DEADLINE_MS;
|
||||
const calls = vi.mocked(execFileOutput).mock.calls;
|
||||
expect(calls).toHaveLength(2);
|
||||
expect(calls[0]?.[0]).toBe("/usr/bin/xcrun");
|
||||
|
||||
@@ -8,9 +8,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { SWIFTC_INTERRUPT } from "../process/DarwinProcessRunTokenReader.js";
|
||||
import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
import { safeParseJson } from "../domain/safeJson.js";
|
||||
import { DEFAULT_NATIVE_UI_DEADLINE_MS } from "../domain/native/nativeDeadlines.js";
|
||||
import type { NativeUiHelper } from "./NativeUiObservation.js";
|
||||
import { NATIVE_UI_HELPER_MAX_BUFFER } from "./NativeUiOutputBudget.js";
|
||||
import { NATIVE_UI_DEADLINE_MS } from "./NativeUiInterruptionControl.js";
|
||||
|
||||
export interface NativeUiHelperRuntime {
|
||||
readonly invoke: NativeUiHelper;
|
||||
@@ -25,7 +25,7 @@ export interface NativeUiHelperRuntime {
|
||||
/** Lazily compile one owned helper per observation/scenario and remove its compiler cache. */
|
||||
export const createNativeUiHelperRuntime = (
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
deadlineMs = NATIVE_UI_DEADLINE_MS,
|
||||
deadlineMs = DEFAULT_NATIVE_UI_DEADLINE_MS,
|
||||
): NativeUiHelperRuntime => {
|
||||
const selectedEnvironment = snapshotEnvironment(environment);
|
||||
const deadlineSignal = AbortSignal.timeout(deadlineMs);
|
||||
|
||||
@@ -8,9 +8,6 @@ import {
|
||||
nativeDeadlineRecovery,
|
||||
} from "../domain/native/nativeDeadlines.js";
|
||||
|
||||
/** Omitted `native_ui_deadline_ms`. */
|
||||
export const NATIVE_UI_DEADLINE_MS = DEFAULT_NATIVE_UI_DEADLINE_MS;
|
||||
|
||||
type Interruption =
|
||||
| { readonly kind: "caller"; readonly reason: unknown }
|
||||
| { readonly kind: "deadline"; readonly reason: unknown };
|
||||
@@ -38,7 +35,7 @@ export class NativeUiInterruptionControl {
|
||||
constructor(
|
||||
caller: AbortSignal | undefined,
|
||||
deadline: AbortSignal,
|
||||
deadlineMs = NATIVE_UI_DEADLINE_MS,
|
||||
deadlineMs = DEFAULT_NATIVE_UI_DEADLINE_MS,
|
||||
) {
|
||||
this.#caller = caller;
|
||||
this.#deadline = deadline;
|
||||
@@ -86,7 +83,7 @@ export class NativeUiInterruptionControl {
|
||||
timeoutMs: this.#deadlineMs,
|
||||
setting: "native_ui_deadline_ms",
|
||||
cliFlag: "--native-ui-deadline-ms",
|
||||
defaultMs: NATIVE_UI_DEADLINE_MS,
|
||||
defaultMs: DEFAULT_NATIVE_UI_DEADLINE_MS,
|
||||
phase: "native UI observation deadline",
|
||||
detail: " A partial observation may already be retained.",
|
||||
unchanged:
|
||||
|
||||
@@ -34,10 +34,7 @@ import {
|
||||
NATIVE_UI_OUTPUT_BUDGET_BYTES,
|
||||
NATIVE_UI_OUTPUT_WEIGHT,
|
||||
} from "./NativeUiOutputBudget.js";
|
||||
import {
|
||||
NATIVE_UI_DEADLINE_MS,
|
||||
NativeUiInterruptionControl,
|
||||
} from "./NativeUiInterruptionControl.js";
|
||||
import { NativeUiInterruptionControl } from "./NativeUiInterruptionControl.js";
|
||||
|
||||
const helper = fileURLToPath(
|
||||
new URL("../../bridge/native/ReaNativeUI.swift", import.meta.url),
|
||||
@@ -79,7 +76,7 @@ export const observeNativeUi = async (
|
||||
new NativeUiInterruptionControl(
|
||||
options.signal,
|
||||
runtime.signal,
|
||||
runtime.deadlineMs ?? NATIVE_UI_DEADLINE_MS,
|
||||
runtime.deadlineMs ?? DEFAULT_NATIVE_UI_DEADLINE_MS,
|
||||
);
|
||||
const ownsInterruption = options.interruption === undefined;
|
||||
let result: Result<z.infer<typeof nativeUiResultSchema>, AnalysisError>;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
it("loads the isolated coordinator without loading its worker's JavaScript parser", async () => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
`
|
||||
import { registerHooks } from "node:module";
|
||||
const parser = import.meta.resolve("@babel/parser");
|
||||
let parserLoaded = false;
|
||||
const hook = registerHooks({
|
||||
load(url, context, nextLoad) {
|
||||
if (url === parser) parserLoaded = true;
|
||||
return nextLoad(url, context);
|
||||
},
|
||||
});
|
||||
const { IsolatedWebBundleAnalyzer } = await import(process.argv[1]);
|
||||
hook.deregister();
|
||||
process.stdout.write(JSON.stringify({
|
||||
coordinatorLoaded: typeof IsolatedWebBundleAnalyzer === "function",
|
||||
parserLoaded,
|
||||
}));
|
||||
`,
|
||||
new URL(
|
||||
"../../../dist/browser/IsolatedWebBundleAnalyzer.js",
|
||||
import.meta.url,
|
||||
).href,
|
||||
],
|
||||
{ timeout: 10_000 },
|
||||
);
|
||||
expect(JSON.parse(stdout)).toEqual({
|
||||
coordinatorLoaded: true,
|
||||
parserLoaded: false,
|
||||
});
|
||||
});
|
||||
+201
@@ -229,3 +229,204 @@ it("keeps unavailable mapped digests unknown instead of borrowing unrelated matc
|
||||
reason: "static-or-runtime-coverage-incomplete",
|
||||
});
|
||||
});
|
||||
|
||||
const UNRELATED = "export const unrelated = 17;\n";
|
||||
|
||||
type LayerSetup = {
|
||||
readonly root: string;
|
||||
readonly mapping: "none" | "explicit";
|
||||
};
|
||||
|
||||
/** Reconcile one captured script, located at absent.js under live, against layers. */
|
||||
const reconcileCapturedScript = async (input: {
|
||||
readonly layers: readonly LayerSetup[];
|
||||
readonly live: string;
|
||||
readonly source: string;
|
||||
readonly includeWorker?: boolean;
|
||||
}) => {
|
||||
const result = reconcileJavaScriptRuntime(
|
||||
parseRuntimeReconciliationInput(
|
||||
reconcileJavaScriptRuntimeInputSchema.parse({
|
||||
static_layers: await Promise.all(
|
||||
input.layers.map(async ({ root, mapping }, index) => ({
|
||||
role: (["application", "cache", "assets"] as const)[index],
|
||||
analysis: await analyzeFixture(root),
|
||||
runtime_mappings:
|
||||
mapping === "explicit"
|
||||
? [{ kind: "file-root", root: input.live, artifact_prefix: "" }]
|
||||
: [],
|
||||
})),
|
||||
),
|
||||
runtime_observations: [
|
||||
electronRuntimeEvidence(input.live, input.source, {
|
||||
scriptFile: "absent.js",
|
||||
includeWorker: input.includeWorker ?? false,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
),
|
||||
);
|
||||
const item = (kind: string) =>
|
||||
result.reconciliations.find(({ entity_kind }) => entity_kind === kind);
|
||||
return { result, script: item("script"), worker: item("worker") };
|
||||
};
|
||||
|
||||
it("keeps an unmapped captured script without a content match unknown", async () => {
|
||||
const root = await applicationFixture();
|
||||
const live = await createTestTempDirectory("rea-runtime-unmapped-");
|
||||
const { result, script, worker } = await reconcileCapturedScript({
|
||||
layers: [{ root, mapping: "none" }],
|
||||
live,
|
||||
source: UNRELATED,
|
||||
includeWorker: true,
|
||||
});
|
||||
const unknown = {
|
||||
status: "unknown",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
reason: "no-authorized-location-mapping",
|
||||
static_layer_id: null,
|
||||
static_node_id: null,
|
||||
candidate_static_count: 0,
|
||||
};
|
||||
expect(script).toMatchObject(unknown);
|
||||
// A source-free worker at an unmapped location shares the same rule.
|
||||
expect(worker).toMatchObject(unknown);
|
||||
expect(result.summary).toMatchObject({ matched: 0, unmatched: 0 });
|
||||
expect(
|
||||
result.graph.edges.some(({ relation }) => relation === "observed_as"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each(["explicit", "implicit-root"] as const)(
|
||||
"reports a mapped captured script without a content match as unmatched (%s)",
|
||||
async (mapping) => {
|
||||
const root = await applicationFixture();
|
||||
const live =
|
||||
mapping === "explicit"
|
||||
? await createTestTempDirectory("rea-runtime-mapped-absent-")
|
||||
: root;
|
||||
const { result, script } = await reconcileCapturedScript({
|
||||
layers: [{ root, mapping: mapping === "explicit" ? "explicit" : "none" }],
|
||||
live,
|
||||
source: UNRELATED,
|
||||
});
|
||||
expect(script).toMatchObject({
|
||||
status: "unmatched",
|
||||
basis: "none",
|
||||
confidence: "high",
|
||||
reason: "no-static-content-match",
|
||||
candidate_static_count: 0,
|
||||
});
|
||||
expect(result.summary.unmatched).toBe(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["only the second", ["none", "explicit"], "unmatched"],
|
||||
["only the first", ["explicit", "none"], "unmatched"],
|
||||
["neither", ["none", "none"], "unknown"],
|
||||
] as const)(
|
||||
"classifies an unmatched captured script when %s of two layers maps it",
|
||||
async (_label, mappings, status) => {
|
||||
const live = await createTestTempDirectory("rea-runtime-layer-mapping-");
|
||||
const { script } = await reconcileCapturedScript({
|
||||
layers: await Promise.all(
|
||||
mappings.map(async (mapping) => ({
|
||||
root: await applicationFixture(),
|
||||
mapping,
|
||||
})),
|
||||
),
|
||||
live,
|
||||
source: UNRELATED,
|
||||
});
|
||||
expect(script).toMatchObject({
|
||||
status,
|
||||
reason:
|
||||
status === "unknown"
|
||||
? "no-authorized-location-mapping"
|
||||
: "no-static-content-match",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[1, "matched", "unique-content-match"],
|
||||
[2, "ambiguous", "ambiguous-static-candidates"],
|
||||
] as const)(
|
||||
"keeps unmapped content-only correspondence with %i digest candidates",
|
||||
async (copies, status, reason) => {
|
||||
const live = await createTestTempDirectory("rea-runtime-unmapped-content-");
|
||||
const { result, script } = await reconcileCapturedScript({
|
||||
layers: await Promise.all(
|
||||
Array.from({ length: copies }, async () => {
|
||||
const root = await applicationFixture();
|
||||
await writeFile(join(root, "other.js"), CAPTURED);
|
||||
return { root, mapping: "none" as const };
|
||||
}),
|
||||
),
|
||||
live,
|
||||
source: CAPTURED,
|
||||
});
|
||||
expect(script).toMatchObject({
|
||||
status,
|
||||
basis: copies === 1 ? "content-sha256" : "none",
|
||||
reason,
|
||||
candidate_static_count: copies,
|
||||
});
|
||||
expect(result.summary.unmatched).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["https://example.test/assets/absent.js", "unmatched"],
|
||||
["https://other.test/assets/absent.js", "unknown"],
|
||||
] as const)(
|
||||
"applies URL-prefix mapping presence to captured script %s",
|
||||
async (url, status) => {
|
||||
const root = await applicationFixture();
|
||||
const parsed = parseRuntimeReconciliationInput(
|
||||
reconcileJavaScriptRuntimeInputSchema.parse({
|
||||
static_layers: [
|
||||
{
|
||||
role: "application",
|
||||
analysis: await analyzeFixture(root),
|
||||
runtime_mappings: [
|
||||
{
|
||||
kind: "url-prefix",
|
||||
prefix: "https://example.test/assets/",
|
||||
artifact_prefix: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
runtime_observations: [
|
||||
electronRuntimeEvidence(root, UNRELATED, { includeWorker: false }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
// Model a URL-located capture at the matcher boundary; browser and
|
||||
// Inspector projections supply URL locations to the same classifier.
|
||||
const entities = projectRuntimeCaptures(parsed.captures).entities.map(
|
||||
(entity) =>
|
||||
entity.kind === "script"
|
||||
? { ...entity, location: { kind: "url" as const, value: url } }
|
||||
: entity,
|
||||
);
|
||||
const result = reconcileRuntimeEntities({
|
||||
entities,
|
||||
candidates: collectStaticRuntimeCandidates(parsed.layers),
|
||||
layers: parsed.layers,
|
||||
});
|
||||
expect(
|
||||
result.items.find(({ entity_kind }) => entity_kind === "script"),
|
||||
).toMatchObject({
|
||||
status,
|
||||
confidence: status === "unknown" ? "unknown" : "high",
|
||||
reason:
|
||||
status === "unknown"
|
||||
? "no-authorized-location-mapping"
|
||||
: "no-static-content-match",
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user