fix(evidence)!: validate native view parent predicates (#8451)
Reject native view sources whose predicate disagrees with the selected operation. Use the original predicate for existing Evidence; do not alter a record without recomputing its identity.
This commit is contained in:
1 parent
3842809c37
commit
53363166f0
41 files changed
+307
-10
No files matched your search
@@ -529,7 +529,11 @@ names, the native API record, and high-pcode facets without starting a provider.
|
||||
and limit default to 0 and 64; pseudocode uses UTF-16 code units and never
|
||||
splits surrogate pairs, while other pages count rows. Unavailable facts and
|
||||
provider limitations remain explicit. Source is
|
||||
an exact same-session retained reference or portable inline Evidence. Views are
|
||||
an exact same-session retained reference, portable inline Evidence, or an
|
||||
Evidence record or bundle file. Native function, inventory, and observation
|
||||
views require the `rea.analysis` predicate; function-matching views require
|
||||
`rea.native-function-matching`. A mismatched predicate returns an
|
||||
`invalid_request` identifying `source.predicate_type`. Views are
|
||||
a summary, a layout mitigations or linkage facet, one section/symbol/module, or
|
||||
a stable page with a caller-selected positive `limit`. Module pages include
|
||||
JavaScript assets, bundled modules, and source modules. Select an exact
|
||||
|
||||
@@ -225,6 +225,7 @@ const projectResolvedAnalysisView = (
|
||||
const projected = projection.project({
|
||||
evidenceId: parent.evidence_id,
|
||||
operation: parent.operation,
|
||||
predicateType: parent.predicate_type,
|
||||
provider: parent.provider,
|
||||
analysisProfile: parent.analysis_profile,
|
||||
authority: parent.authority,
|
||||
|
||||
@@ -436,6 +436,7 @@ describe("browser scenario network observation", () => {
|
||||
const project = (capture: unknown) => {
|
||||
const result = projectBrowserScenarioView(
|
||||
{
|
||||
predicateType: "rea.browser-scenario-capture",
|
||||
evidenceId: `ev:${"a".repeat(64)}`,
|
||||
operation: "capture_browser_scenario",
|
||||
normalizedResult: capture,
|
||||
|
||||
@@ -302,6 +302,7 @@ it("rejects collection-inapplicable fields and malformed sibling facts even for
|
||||
).toBe(false);
|
||||
const report = source();
|
||||
const parent = {
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: "ev_" + "b".repeat(64),
|
||||
operation: "inspect_native_load_image",
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -20,6 +20,7 @@ import { resolveNativeMatchingSources } from "./NativeMatchingSources.js";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { NativeMatchingAllowance } from "../../config/nativeMatchingAllowance.js";
|
||||
import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
|
||||
import { NATIVE_FUNCTION_MATCHING_PREDICATE } from "../../domain/native/functionMatching.js";
|
||||
|
||||
const OPERATION = "match_native_functions";
|
||||
|
||||
@@ -144,7 +145,7 @@ const matchWithAdmission = async (
|
||||
await checkpoint();
|
||||
const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, {
|
||||
operation: OPERATION,
|
||||
predicateType: "rea.native-function-matching",
|
||||
predicateType: NATIVE_FUNCTION_MATCHING_PREDICATE,
|
||||
confidence: "derived",
|
||||
authority: "analyst-inference",
|
||||
parameters: {
|
||||
|
||||
@@ -119,6 +119,7 @@ it("rebuilds native row selection when a caller's sealed flag covers only a shal
|
||||
);
|
||||
Object.freeze(report);
|
||||
const parent = {
|
||||
predicateType: matched.value.evidence.predicate_type,
|
||||
evidenceId: matched.value.evidence.evidence_id,
|
||||
operation: matched.value.evidence.operation,
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { MatchNativeFunctionsInput } from "../../contracts/native/functionMatchingContracts.js";
|
||||
import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { isImmutableEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
ANALYSIS_EVIDENCE_PREDICATE,
|
||||
isImmutableEvidence,
|
||||
type Evidence,
|
||||
} from "../../domain/evidence.js";
|
||||
import { readEvidence } from "../../domain/evidenceValidation.js";
|
||||
import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js";
|
||||
import {
|
||||
@@ -82,7 +86,7 @@ export const resolveNativeMatchingSources = async (
|
||||
): Promise<Evidence> => {
|
||||
const evidence = await resolve(raw, path, borrowable);
|
||||
if (
|
||||
evidence.predicate_type !== "rea.analysis" ||
|
||||
evidence.predicate_type !== ANALYSIS_EVIDENCE_PREDICATE ||
|
||||
!operations.includes(evidence.operation) ||
|
||||
evidence.subject === null
|
||||
) {
|
||||
|
||||
@@ -18,12 +18,14 @@ import { toolContract } from "./toolContracts.js";
|
||||
|
||||
const projectSummary = (parent: {
|
||||
readonly evidence_id: string;
|
||||
readonly predicate_type: string;
|
||||
readonly operation: string;
|
||||
readonly normalized_result: unknown;
|
||||
readonly limitations: readonly string[];
|
||||
}) =>
|
||||
projectAnalysisView(
|
||||
{
|
||||
predicateType: parent.predicate_type,
|
||||
evidenceId: parent.evidence_id,
|
||||
operation: parent.operation,
|
||||
normalizedResult: parent.normalized_result,
|
||||
@@ -74,6 +76,7 @@ it("advertises only that operation's summary from analysis detail outputs", () =
|
||||
);
|
||||
const parent = analysisViewCaptureEvidence(environmentStateCaptureFixture());
|
||||
const captureParent = {
|
||||
predicateType: parent.predicate_type,
|
||||
evidenceId: parent.evidence_id,
|
||||
operation: parent.operation,
|
||||
normalizedResult: parent.normalized_result,
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
const layoutParent = (): AnalysisViewParent => {
|
||||
const evidence = analysisViewLayoutEvidence();
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: evidence.normalized_result,
|
||||
@@ -48,6 +49,7 @@ const layoutParent = (): AnalysisViewParent => {
|
||||
const javascriptParent = (
|
||||
analysis = analysisViewJavaScriptAnalysis(),
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.predicate_type,
|
||||
evidenceId: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.evidence_id,
|
||||
operation: "analyze_javascript_application",
|
||||
normalizedResult: analysis,
|
||||
@@ -57,6 +59,7 @@ const javascriptParent = (
|
||||
const nativeParent = (
|
||||
result: unknown = ghidraFunctionDossier(),
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: "ev_" + "b".repeat(64),
|
||||
operation: "analyze_function",
|
||||
normalizedResult: result,
|
||||
@@ -363,6 +366,7 @@ it("rejects artifact summary views when inspection substep coverage count disagr
|
||||
const source = ARTIFACT_COMPARISON_EXAMPLE.left;
|
||||
const inspection = createArtifactInspection(source);
|
||||
const parent: AnalysisViewParent = {
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_artifact",
|
||||
normalizedResult: {
|
||||
@@ -1577,6 +1581,7 @@ it("rejects unsupported parent operations and incompatible collections", () => {
|
||||
).toMatchObject({ ok: false, error: { _tag: "AnalysisInputError" } });
|
||||
const unsupported = projectAnalysisView(
|
||||
{
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"d".repeat(64)}`,
|
||||
operation: "inspect_recorded_crash",
|
||||
normalizedResult: { path: "/artifacts/crash.core" },
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { NATIVE_OBSERVATION_OPERATIONS } from "../native/nativeObservation.js";
|
||||
import {
|
||||
isNativeObservationOperation,
|
||||
NATIVE_OBSERVATION_OPERATIONS,
|
||||
} from "../native/nativeObservation.js";
|
||||
import { projectNativeObservationView } from "./nativeObservationView.js";
|
||||
import { projectDevicetreeView } from "./devicetreeView.js";
|
||||
import { projectApkSignatureView } from "./apkSignatureView.js";
|
||||
@@ -22,7 +25,12 @@ import { projectGoBinaryView } from "./goBinaryView.js";
|
||||
import { projectRustBinaryView } from "./rustBinaryView.js";
|
||||
import { projectNativeFunctionMatchingView } from "./nativeFunctionMatchingView.js";
|
||||
import { projectNativeInventoryView } from "./nativeInventoryView.js";
|
||||
import { NATIVE_INVENTORY_OPERATIONS } from "../native/nativeInventory.js";
|
||||
import {
|
||||
isNativeInventoryOperation,
|
||||
NATIVE_INVENTORY_OPERATIONS,
|
||||
} from "../native/nativeInventory.js";
|
||||
import { ANALYSIS_EVIDENCE_PREDICATE } from "../evidence.js";
|
||||
import { NATIVE_FUNCTION_MATCHING_PREDICATE } from "../native/functionMatching.js";
|
||||
import { z } from "zod";
|
||||
import { projectBrowserScenarioView } from "./browserScenarioView.js";
|
||||
import { projectNativeLoadImageView } from "./nativeLoadImageView.js";
|
||||
@@ -414,6 +422,28 @@ export const projectAnalysisView = (
|
||||
},
|
||||
]),
|
||||
);
|
||||
const expectedPredicate =
|
||||
parent.operation === "match_native_functions"
|
||||
? NATIVE_FUNCTION_MATCHING_PREDICATE
|
||||
: parent.operation === "analyze_function" ||
|
||||
isNativeObservationOperation(parent.operation) ||
|
||||
isNativeInventoryOperation(parent.operation)
|
||||
? ANALYSIS_EVIDENCE_PREDICATE
|
||||
: undefined;
|
||||
if (
|
||||
expectedPredicate !== undefined &&
|
||||
parent.predicateType !== expectedPredicate
|
||||
)
|
||||
return err(
|
||||
analysisViewInputError([
|
||||
{
|
||||
path: ["source", "predicate_type"],
|
||||
reason: "invalid_value",
|
||||
expected: expectedPredicate,
|
||||
message: `${parent.operation} requires ${expectedPredicate} Evidence; received ${parent.predicateType}.`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
if (view.kind === "javascript-routes") {
|
||||
const projected = projectJavaScriptRoutesView(parent, view);
|
||||
return sealProjected(projected);
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { AnalysisViewParent } from "./analysisView.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
|
||||
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_python_pyc",
|
||||
normalizedResult,
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ownedJsonValueSchema } from "../jsonValue.js";
|
||||
export interface AnalysisViewParent {
|
||||
readonly evidenceId: string;
|
||||
readonly operation: string;
|
||||
readonly predicateType: string;
|
||||
readonly normalizedResult: unknown;
|
||||
readonly provider?: Evidence["provider"];
|
||||
readonly analysisProfile?: Evidence["analysis_profile"];
|
||||
|
||||
@@ -90,6 +90,7 @@ const parent = (value = report()): AnalysisViewParent => {
|
||||
},
|
||||
);
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: value,
|
||||
|
||||
@@ -166,6 +166,7 @@ const fixture = (operation: Operation, report: unknown, rawMetadata = "") => {
|
||||
{ immutable: true },
|
||||
);
|
||||
const parent: AnalysisViewParent = {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation,
|
||||
normalizedResult:
|
||||
|
||||
@@ -84,6 +84,7 @@ const freshInventory = () => {
|
||||
return inventory;
|
||||
};
|
||||
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
predicateType: carrier.predicate_type,
|
||||
evidenceId: carrier.evidence_id,
|
||||
operation: "inventory_artifact",
|
||||
subject:
|
||||
|
||||
@@ -35,6 +35,7 @@ const parentOf = (
|
||||
environmentStateCaptureFixture(),
|
||||
),
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: evidence.normalized_result,
|
||||
|
||||
@@ -39,6 +39,7 @@ const parentOf = (
|
||||
) => {
|
||||
const evidence = analysisViewCaptureEvidence(capture);
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: evidence.normalized_result,
|
||||
@@ -854,6 +855,7 @@ it("refuses non-HAR formats, sensitive parents, unsupported parents and malforme
|
||||
expect(
|
||||
projectAnalysisView(
|
||||
{
|
||||
predicateType: layout.predicate_type,
|
||||
evidenceId: layout.evidence_id,
|
||||
operation: layout.operation,
|
||||
normalizedResult: layout.normalized_result,
|
||||
@@ -871,6 +873,7 @@ it("refuses non-HAR formats, sensitive parents, unsupported parents and malforme
|
||||
expect(
|
||||
projectAnalysisView(
|
||||
{
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_web_network_capture",
|
||||
normalizedResult,
|
||||
|
||||
@@ -57,6 +57,7 @@ const parentOf = (
|
||||
environmentStateCaptureFixture(),
|
||||
),
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: evidence.normalized_result,
|
||||
|
||||
@@ -38,6 +38,7 @@ const parent = (): AnalysisViewParent => {
|
||||
},
|
||||
);
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
subject: evidence.subject,
|
||||
|
||||
@@ -32,6 +32,7 @@ const report = () =>
|
||||
});
|
||||
|
||||
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"b".repeat(64)}`,
|
||||
operation: "inspect_gpu_code_objects",
|
||||
normalizedResult,
|
||||
|
||||
@@ -67,6 +67,7 @@ const report: Il2cppMetadata = {
|
||||
};
|
||||
|
||||
const parent = (normalizedResult: unknown, sealedResult: boolean) => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"b".repeat(64)}`,
|
||||
operation: "inspect_il2cpp_metadata",
|
||||
normalizedResult,
|
||||
|
||||
@@ -91,6 +91,7 @@ const parent = (
|
||||
normalizedResult: unknown = report(),
|
||||
sha256 = target,
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"c".repeat(64)}`,
|
||||
operation: "decode_interface_builder",
|
||||
normalizedResult,
|
||||
|
||||
@@ -28,6 +28,7 @@ const inventory = (
|
||||
]),
|
||||
);
|
||||
const parent = (): AnalysisViewParent => ({
|
||||
predicateType: "rea.javascript-application-analysis",
|
||||
evidenceId: `ev_${"b".repeat(64)}`,
|
||||
operation: "analyze_javascript_application",
|
||||
limitations: [],
|
||||
|
||||
@@ -194,6 +194,7 @@ const parent = (
|
||||
local_path: "/bin/tool",
|
||||
},
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"c".repeat(64)}`,
|
||||
operation: "inspect_macho",
|
||||
normalizedResult,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { projectManagedMemberView } from "./managedMemberView.js";
|
||||
|
||||
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_managed_members",
|
||||
normalizedResult,
|
||||
|
||||
@@ -19,6 +19,7 @@ const rows = () => [
|
||||
{ address: "0x20", value: "last" },
|
||||
];
|
||||
const parent = (normalizedResult: unknown): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "search_procedures",
|
||||
normalizedResult,
|
||||
|
||||
@@ -65,6 +65,7 @@ const capture = (
|
||||
},
|
||||
);
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -113,6 +113,7 @@ const capture = async () => {
|
||||
return {
|
||||
report,
|
||||
parent: {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -54,6 +54,7 @@ const parent = (value = structuredClone(report)): AnalysisViewParent => {
|
||||
},
|
||||
);
|
||||
return {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
subject: evidence.subject,
|
||||
|
||||
@@ -282,6 +282,7 @@ it("reserves retained request-body expansion before schema decoding or projectio
|
||||
try {
|
||||
const projected = admitScenarioCaptureParent(
|
||||
{
|
||||
predicateType: "rea.browser-scenario-capture",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "capture_browser_scenario",
|
||||
normalizedResult: capture,
|
||||
@@ -330,6 +331,7 @@ it("reserves request text even when another event's body was not captured", () =
|
||||
// The capture's containers fit this budget; only its request text does not.
|
||||
const projected = admitScenarioCaptureParent(
|
||||
{
|
||||
predicateType: "rea.browser-scenario-capture",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "capture_browser_scenario",
|
||||
normalizedResult: { ...capture, events: { ...capture.events, items } },
|
||||
|
||||
@@ -109,6 +109,7 @@ const sampleAnalysis = (): WebBundleAnalysis => ({
|
||||
const makeParent = (
|
||||
overrides?: Partial<WebBundleAnalysis>,
|
||||
): AnalysisViewParent => ({
|
||||
predicateType: "rea.web-bundle-analysis",
|
||||
evidenceId: `ev_${"c".repeat(64)}`,
|
||||
operation: "analyze_web_bundle",
|
||||
limitations: ["Parent run was bounded"],
|
||||
@@ -476,6 +477,7 @@ it.each(["indices", "values", "script_keys", "mechanisms"])(
|
||||
|
||||
it("rejects non-web-bundle parent operation with invalid_value", () => {
|
||||
const invalidParent: AnalysisViewParent = {
|
||||
predicateType: "rea.javascript-application-analysis",
|
||||
evidenceId: `ev_${"d".repeat(64)}`,
|
||||
operation: "analyze_javascript_application",
|
||||
limitations: [],
|
||||
@@ -627,6 +629,7 @@ it("correctly counts zero findings in summary for an empty web bundle", () => {
|
||||
|
||||
it("rejects malformed parent analysis report", () => {
|
||||
const malformedParent: AnalysisViewParent = {
|
||||
predicateType: "rea.web-bundle-analysis",
|
||||
evidenceId: `ev_${"e".repeat(64)}`,
|
||||
operation: "analyze_web_bundle",
|
||||
limitations: [],
|
||||
|
||||
@@ -19,6 +19,7 @@ const project = (
|
||||
const parent = analysisViewCaptureEvidence(capture);
|
||||
return projectAnalysisView(
|
||||
{
|
||||
predicateType: parent.predicate_type,
|
||||
evidenceId: parent.evidence_id,
|
||||
operation: parent.operation,
|
||||
normalizedResult: parent.normalized_result,
|
||||
@@ -202,6 +203,7 @@ it("rejects native captures and malformed parents with typed errors", () => {
|
||||
expect(
|
||||
projectAnalysisView(
|
||||
{
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_web_network_capture",
|
||||
normalizedResult: {},
|
||||
@@ -218,6 +220,7 @@ it.each(["api-surface", "environment-manifest", "environment-state"])(
|
||||
const capture = analysisViewCaptureFixture();
|
||||
const result = projectAnalysisView(
|
||||
{
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_web_network_capture",
|
||||
limitations: [],
|
||||
@@ -357,6 +360,7 @@ it("rejects cyclic captures and accessor members without invoking them", () => {
|
||||
expect(
|
||||
projectAnalysisView(
|
||||
{
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "inspect_web_network_capture",
|
||||
normalizedResult,
|
||||
|
||||
@@ -69,6 +69,7 @@ const capture = () => {
|
||||
return {
|
||||
report,
|
||||
parent: {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: report,
|
||||
@@ -126,6 +127,7 @@ it("uses qlog event headlines without promoting HTTP-shaped extensions", () => {
|
||||
);
|
||||
const preview = projectAnalysisView(
|
||||
{
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: qlog,
|
||||
@@ -349,6 +351,7 @@ const nativePreview = (
|
||||
});
|
||||
const preview = projectAnalysisView(
|
||||
{
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -32,6 +32,9 @@ import {
|
||||
type RelationshipCoverage,
|
||||
} from "./relationshipCoverage.js";
|
||||
|
||||
/** Predicate shared by ordinary provider analysis observations. */
|
||||
export const ANALYSIS_EVIDENCE_PREDICATE = "rea.analysis";
|
||||
|
||||
/** Provider identity schema shared by evidence-bearing persistence formats. */
|
||||
export const providerSchema = z.strictObject({
|
||||
id: z.string().min(1),
|
||||
@@ -453,7 +456,7 @@ const normalizeEvidenceObservation = (
|
||||
version: provider.version,
|
||||
},
|
||||
analysis_profile: observation.analysisProfile ?? null,
|
||||
predicate_type: observation.predicateType ?? "rea.analysis",
|
||||
predicate_type: observation.predicateType ?? ANALYSIS_EVIDENCE_PREDICATE,
|
||||
operation: observation.operation,
|
||||
parameters: observation.parameters,
|
||||
raw_result: observation.rawResult ?? null,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Evidence } from "./evidence.js";
|
||||
import { ANALYSIS_EVIDENCE_PREDICATE, type Evidence } from "./evidence.js";
|
||||
import { AnalysisInputError } from "./analysisErrorCore.js";
|
||||
import type { EvidenceIntegrityError } from "./evidenceErrors.js";
|
||||
import { readEvidence, readEvidencePayload } from "./evidenceValidation.js";
|
||||
@@ -70,7 +70,7 @@ export const parseFunctionEvidence = (
|
||||
);
|
||||
if (
|
||||
evidence.operation !== "analyze_function" ||
|
||||
evidence.predicate_type !== "rea.analysis"
|
||||
evidence.predicate_type !== ANALYSIS_EVIDENCE_PREDICATE
|
||||
) {
|
||||
const location = selection.path.map(String).join(".");
|
||||
const subject =
|
||||
@@ -81,7 +81,10 @@ export const parseFunctionEvidence = (
|
||||
: `Evidence at ${location}`;
|
||||
return invalidSelection(
|
||||
`Function analysis requires analyze_function Evidence with the rea.analysis predicate; the ${subject} has operation ${evidence.operation} and predicate ${evidence.predicate_type}.`,
|
||||
{ operation: "analyze_function", predicate_type: "rea.analysis" },
|
||||
{
|
||||
operation: "analyze_function",
|
||||
predicate_type: ANALYSIS_EVIDENCE_PREDICATE,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (evidence.subject === null)
|
||||
|
||||
@@ -3,6 +3,9 @@ import { z } from "zod";
|
||||
import { digestSchema, prefixedDigestSchema } from "../digests.js";
|
||||
import { retainedEvidenceReferenceSchema } from "../evidenceReference.js";
|
||||
|
||||
export const NATIVE_FUNCTION_MATCHING_PREDICATE =
|
||||
"rea.native-function-matching";
|
||||
|
||||
const evidenceId = prefixedDigestSchema("ev");
|
||||
export const nativeMatchStateSchema = z.enum([
|
||||
"matched",
|
||||
|
||||
@@ -2066,6 +2066,7 @@ it("keeps known capture omissions in compact route summaries and queryable gap f
|
||||
);
|
||||
const report = analyzeWebBundleFixture(captured),
|
||||
parent = {
|
||||
predicateType: "rea.web-bundle-analysis",
|
||||
evidenceId: `ev_${"a".repeat(64)}`,
|
||||
operation: "analyze_web_bundle",
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -18,6 +18,7 @@ const artifact = {
|
||||
bytes: 2048,
|
||||
};
|
||||
const parent = (report: unknown): AnalysisViewParent => ({
|
||||
predicateType: "rea.analysis",
|
||||
evidenceId: `ev_${"b".repeat(64)}`,
|
||||
operation: "inspect_native_exports",
|
||||
normalizedResult: report,
|
||||
|
||||
@@ -26,6 +26,7 @@ it("checks parent subject identity again when signing facts are already cached",
|
||||
if (evidence.subject === null || evidence.subject.format !== "apk")
|
||||
throw new Error("selected file subject missing");
|
||||
const parent = {
|
||||
predicateType: evidence.predicate_type,
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
normalizedResult: freezeJsonSnapshot(evidence.normalized_result),
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { expect, it } from "vitest";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
type Evidence,
|
||||
} from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
|
||||
const fixture = FUNCTION_COMPARISON_EXAMPLE.left;
|
||||
if (fixture.subject === null)
|
||||
throw new Error("Expected artifact-bound fixture");
|
||||
const subject = {
|
||||
path: fixture.subject.local_path,
|
||||
sha256: fixture.subject.digest.sha256,
|
||||
format: fixture.subject.format,
|
||||
};
|
||||
const selected = (
|
||||
operation: string,
|
||||
result: Evidence["normalized_result"],
|
||||
predicateType: string,
|
||||
) =>
|
||||
createEvidence(subject, fixture.provider, {
|
||||
operation,
|
||||
parameters: {},
|
||||
result,
|
||||
predicateType,
|
||||
confidence: "observed",
|
||||
authority: "shipped-artifact",
|
||||
});
|
||||
|
||||
const wrongMatchingPredicate = (matching: Evidence) =>
|
||||
createEvidence(undefined, matching.provider, {
|
||||
operation: matching.operation,
|
||||
parameters: matching.parameters,
|
||||
result: matching.normalized_result,
|
||||
predicateType: "rea.analysis",
|
||||
confidence: matching.confidence,
|
||||
authority: matching.authority,
|
||||
evidenceLinks: matching.evidence_links,
|
||||
});
|
||||
|
||||
it.each(["full", "compact"] as const)(
|
||||
"validates native parent predicates across public sources (%s schema)",
|
||||
async (profile) => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("No provider should start");
|
||||
});
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{
|
||||
environment: { ...process.env, REA_MCP_OUTPUT_SCHEMA_PROFILE: profile },
|
||||
},
|
||||
);
|
||||
const client = new Client({ name: "native-view-identity", version: "1" });
|
||||
const validation = createMcpSchemaValidation();
|
||||
const [ct, st] = InMemoryTransport.createLinkedPair();
|
||||
const root = await createTestTempDirectory("rea-native-view-identity-");
|
||||
try {
|
||||
await server.connect(st);
|
||||
await client.connect(ct);
|
||||
const definitions = new Map(
|
||||
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
|
||||
);
|
||||
const viewTool = definitions.get("inspect_analysis_view");
|
||||
if (viewTool?.outputSchema === undefined)
|
||||
throw new Error("Missing advertised view schema");
|
||||
expect(validation.assertions.validateSchema(viewTool.outputSchema)).toBe(
|
||||
true,
|
||||
);
|
||||
const call = (name: string, args: Record<string, unknown>) => {
|
||||
const toolDefinition = definitions.get(name);
|
||||
if (toolDefinition === undefined) throw new Error(`Missing ${name}`);
|
||||
return client.callTool({ name, arguments: args }, { toolDefinition });
|
||||
};
|
||||
const inventory = selected(
|
||||
"list_procedures",
|
||||
[{ address: "0x1000", value: "main" }],
|
||||
"rea.analysis",
|
||||
);
|
||||
const matchingResult = await call("match_native_functions", {
|
||||
left: inventory,
|
||||
right: inventory,
|
||||
detail: "complete",
|
||||
});
|
||||
expect(
|
||||
matchingResult.isError,
|
||||
JSON.stringify(matchingResult.content),
|
||||
).not.toBe(true);
|
||||
const matching = parseEvidence(matchingResult.structuredContent);
|
||||
expect(matching.predicate_type).toBe("rea.native-function-matching");
|
||||
const wrongMatching = wrongMatchingPredicate(matching);
|
||||
const graph = {
|
||||
graph: {
|
||||
"0": [
|
||||
{
|
||||
address: "0x1000",
|
||||
status: "ok",
|
||||
calls: [],
|
||||
relationship_coverage: { status: "complete" },
|
||||
},
|
||||
],
|
||||
},
|
||||
limitations: [],
|
||||
};
|
||||
const cases = [
|
||||
{
|
||||
good: selected(
|
||||
"analyze_function",
|
||||
fixture.normalized_result,
|
||||
"rea.analysis",
|
||||
),
|
||||
bad: selected(
|
||||
"analyze_function",
|
||||
fixture.normalized_result,
|
||||
"rea.unrelated",
|
||||
),
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
{
|
||||
good: inventory,
|
||||
bad: selected(
|
||||
"list_procedures",
|
||||
inventory.normalized_result,
|
||||
"rea.unrelated",
|
||||
),
|
||||
view: { kind: "native-inventory", offset: 0, limit: 1 },
|
||||
},
|
||||
{
|
||||
good: selected("procedure_address", "0x1000", "rea.analysis"),
|
||||
bad: selected("procedure_address", "0x1000", "rea.unrelated"),
|
||||
view: { kind: "native-observation", offset: 0, limit: 1 },
|
||||
},
|
||||
{
|
||||
good: selected("get_call_graph", graph, "rea.analysis"),
|
||||
bad: selected("get_call_graph", graph, "rea.unrelated"),
|
||||
view: { kind: "native-observation", offset: 0, limit: 1 },
|
||||
},
|
||||
{ good: matching, bad: wrongMatching, view: { kind: "summary" } },
|
||||
];
|
||||
for (const [index, { good, bad, view }] of cases.entries()) {
|
||||
expect(session.recordEvidence(good).ok).toBe(true);
|
||||
expect(session.recordEvidence(bad).ok).toBe(true);
|
||||
const goodReply = await call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: good.evidence_id },
|
||||
view,
|
||||
});
|
||||
expect(goodReply.isError, JSON.stringify(goodReply.content)).not.toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
validation.assertions.validate(
|
||||
viewTool.outputSchema,
|
||||
goodReply.structuredContent,
|
||||
),
|
||||
JSON.stringify(validation.assertions.errors),
|
||||
).toBe(true);
|
||||
const goodView = parseEvidence(goodReply.structuredContent);
|
||||
expect(goodView.evidence_links).toContain(good.evidence_id);
|
||||
const raw = join(root, `${index}.json`),
|
||||
bundle = join(root, `${index}-bundle.json`);
|
||||
await writeFile(raw, JSON.stringify(bad));
|
||||
await writeFile(bundle, JSON.stringify(createEvidenceBundle([bad])));
|
||||
for (const source of [
|
||||
{ kind: "inline", evidence: bad },
|
||||
{ kind: "retained-evidence", evidence_id: bad.evidence_id },
|
||||
{ kind: "evidence-file", path: raw },
|
||||
{ kind: "evidence-file", path: bundle },
|
||||
]) {
|
||||
const reply = await call("inspect_analysis_view", { source, view });
|
||||
expect(parseMcpToolError(reply).error).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
expect.objectContaining({
|
||||
path: ["source", "predicate_type"],
|
||||
reason: "invalid_value",
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
expect(session.activeTarget()).toBeUndefined();
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
validation.clear();
|
||||
}
|
||||
},
|
||||
);
|
||||
+1
@@ -455,6 +455,7 @@ export const environmentStateViewOf = (
|
||||
): EnvironmentStateView => {
|
||||
const result = projectAnalysisView(
|
||||
{
|
||||
predicateType: capture.predicate_type,
|
||||
evidenceId: capture.evidence_id,
|
||||
operation: capture.operation,
|
||||
normalizedResult: capture.normalized_result,
|
||||
|
||||
Reference in new issue
Block a user