mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
fix(analysis-view): reject accessor-backed inline evidence before parsing
This commit is contained in:
@@ -24,7 +24,6 @@ import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import { EvidenceLedger } from "../investigation/EvidenceLedger.js";
|
||||
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
|
||||
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js";
|
||||
|
||||
it.each(["/fixtures/unknown.exe", ""])(
|
||||
"preserves recorded empty native identity fields: path=%j",
|
||||
@@ -433,26 +432,39 @@ it("refuses an object-dense borrowed capture before making a full schema clone",
|
||||
expect(ledger.forAnalysis(parent.evidence_id)).toBe(borrowed);
|
||||
});
|
||||
|
||||
it("borrows inline Evidence until admission without invoking nested accessors", () => {
|
||||
let accessed = false;
|
||||
const evidence = {
|
||||
get normalized_result() {
|
||||
accessed = true;
|
||||
throw new Error("Evidence must be admitted before parsing");
|
||||
},
|
||||
};
|
||||
const input = {
|
||||
source: { kind: "inline", evidence },
|
||||
view: { kind: "api-surface" },
|
||||
};
|
||||
const parsed = inspectAnalysisViewInputSchema.parse(input);
|
||||
if (parsed.source.kind !== "inline") throw new Error("Expected inline input");
|
||||
expect(parsed.source.evidence).toBe(evidence);
|
||||
expect(inspectAnalysisView(input)).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
expect(accessed).toBe(false);
|
||||
it("rejects accessor-backed inline Evidence before all view projections", () => {
|
||||
const views = [
|
||||
{ kind: "summary" },
|
||||
{ kind: "capture" },
|
||||
{ kind: "api-surface" },
|
||||
] as const;
|
||||
for (const view of views) {
|
||||
let accessed = false;
|
||||
const evidence = {
|
||||
get normalized_result() {
|
||||
accessed = true;
|
||||
throw new Error("Evidence accessor must not be invoked");
|
||||
},
|
||||
};
|
||||
const result = inspectAnalysisView({
|
||||
source: { kind: "inline", evidence },
|
||||
view,
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisInputError" },
|
||||
});
|
||||
expect(accessed).toBe(false);
|
||||
}
|
||||
|
||||
const parent = analysisViewCaptureEvidence();
|
||||
for (const view of views)
|
||||
expect(
|
||||
inspectAnalysisView({
|
||||
source: { kind: "inline", evidence: parent },
|
||||
view,
|
||||
}).ok,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps inline-envelope admission live alongside capture validation and grouping", () => {
|
||||
|
||||
@@ -5,7 +5,10 @@ import type { EvidenceLookup } from "../EvidenceInputResolver.js";
|
||||
import { resolveEvidenceInput } from "../EvidenceInputResolver.js";
|
||||
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import {
|
||||
@@ -159,6 +162,18 @@ const resolveAnalysisViewSource = (
|
||||
): Result<Evidence, AnalysisError> => {
|
||||
if (source.kind === "retained-evidence")
|
||||
return resolveEvidenceInput(source, lookup);
|
||||
const invalidDataPath = inlineEvidenceInvalidDataPath(source.evidence);
|
||||
if (invalidDataPath !== undefined)
|
||||
return err(
|
||||
new AnalysisInputError(INSPECT_ANALYSIS_VIEW_OPERATION, undefined, [
|
||||
{
|
||||
path: ["source", "evidence", ...invalidDataPath],
|
||||
reason: "invalid_type",
|
||||
message:
|
||||
"Inline Evidence must contain plain JSON data properties without accessors or cycles.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
try {
|
||||
return ok(parseEvidence(source.evidence));
|
||||
} catch (cause: unknown) {
|
||||
@@ -185,6 +200,53 @@ const resolveAnalysisViewSource = (
|
||||
}
|
||||
};
|
||||
|
||||
/** Reject non-JSON object behavior before Zod reads caller-owned Evidence. */
|
||||
const inlineEvidenceInvalidDataPath = (
|
||||
root: unknown,
|
||||
): readonly (string | number)[] | undefined => {
|
||||
const active = new Set<object>();
|
||||
const pending: {
|
||||
readonly value: unknown;
|
||||
readonly path: readonly (string | number)[];
|
||||
readonly exit?: object;
|
||||
}[] = [{ value: root, path: [] }];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
if (current === undefined) continue;
|
||||
if (current.exit !== undefined) {
|
||||
active.delete(current.exit);
|
||||
continue;
|
||||
}
|
||||
const { value, path } = current;
|
||||
if (typeof value !== "object" || value === null) continue;
|
||||
const isArray = Array.isArray(value);
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
if (
|
||||
(isArray && prototype !== Array.prototype) ||
|
||||
(!isArray && prototype !== Object.prototype && prototype !== null) ||
|
||||
active.has(value)
|
||||
)
|
||||
return path;
|
||||
|
||||
active.add(value);
|
||||
pending.push({ value, path, exit: value });
|
||||
const keys = Object.getOwnPropertyNames(value);
|
||||
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
||||
const key = keys[index];
|
||||
if (key === undefined) continue;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
||||
const childPath = [
|
||||
...path,
|
||||
isArray && /^(0|[1-9]\d*)$/u.test(key) ? Number(key) : key,
|
||||
];
|
||||
if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
|
||||
return childPath;
|
||||
pending.push({ value: descriptor.value, path: childPath });
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const createAnalysisViewEvidence = (
|
||||
input: InspectAnalysisViewInput,
|
||||
parent: Evidence,
|
||||
|
||||
Reference in New Issue
Block a user