mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
2bf3663f65
* feat(browser): inspect retained HAR and mitmproxy capture evidence Add independent offline format adapters, inline CLI/MCP evidence, bounded owned decoder processes, and real producer verification. Refs #882 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): preserve capture boundaries during cancellation and decoding Honor cancellation during owned-command cleanup, retain opaque HAR extension content, and clean verifier resources on every failure path. Refs #882 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): exclude sensitive capture fields across representations Normalize known native transport URLs for both bytes and strings, including backups. Exclude explicitly sensitive text fields with null so redaction markers cannot reintroduce declared literals. Cover both provider identities in catalog conformance. Refs #882 Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) * fix(browser): exclude sensitive capture identities and bound diagnostics * fix(browser): validate excluded HAR subtrees before projection * fix(browser): distinguish capture input limits from decoder failures * fix(browser): preserve decoder failure categories * fix(browser): validate excluded native credential values * test(browser): report capture verifier assertions directly * fix(browser): classify native representation failures precisely * fix(browser): exclude URL userinfo after header whitespace * fix(browser): preserve capture evidence and failure boundaries * fix(browser): exclude sensitive literals from capture limitations * test(browser): check limitation values without JSON syntax * fix(browser): preserve acquisition integrity and exclude selection diagnostics * fix(browser): preserve capture acquisition and declaration boundaries * fix(mcp): project sensitive capture argument validation * fix: preserve capture bytes and prelaunch cancellation * fix(browser): exclude sensitive HAR record ancestry * fix(browser): preserve transport URL and parameter exclusions * fix(browser): preserve numeric redaction and canonical capture provenance * fix(browser): cover derived capture values and ownership preparation deadlines * fix(browser): exclude explicitly sensitive artifact digests * chore: regenerate generated metadata * fix(browser): reject lossy HAR member normalization * test(browser): assert the advertised unsupported-profile outcome * fix(browser): preserve JSON visitor nesting failure locations * fix(browser): exclude explicitly sensitive native source ranges --------- Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
341 lines
12 KiB
TypeScript
341 lines
12 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { WebNetworkCaptureService } from "./WebNetworkCaptureService.js";
|
|
import { historicalHar } from "../../tests/fixtures/historicalHar.js";
|
|
import { ok } from "../domain/result.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { parseEvidence } from "../domain/evidence.js";
|
|
import {
|
|
webNetworkCaptureSchema,
|
|
type WebNetworkCapture,
|
|
} from "../domain/webNetworkCapture.js";
|
|
|
|
it.each(["REDACTED", "[", "]", "/capture.har"])(
|
|
"excludes sensitive path text even when a replacement marker would collide: %s",
|
|
async (literal) => {
|
|
const report = fixture();
|
|
report.artifact.path = `/capture.har/${literal}`;
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(report)),
|
|
}).inspect({
|
|
capture_path: report.artifact.path,
|
|
format: "har",
|
|
sensitive_values: [literal],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.subject?.local_path).not.toContain(literal);
|
|
expect(result.value.parameters.capture_path).not.toContain(literal);
|
|
expect(
|
|
result.value.locations.every(
|
|
(location) =>
|
|
location.kind !== "artifact-path" || !location.path.includes(literal),
|
|
),
|
|
).toBe(true);
|
|
expect(result.value.subject?.digest.sha256).toBe(report.artifact.sha256);
|
|
},
|
|
);
|
|
|
|
it("keeps observed artifact identity when the entire sensitive path is excluded", async () => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: ["/", "[", "REDACTED"],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.subject).toMatchObject({
|
|
local_path: "",
|
|
digest: { sha256: "a".repeat(64) },
|
|
});
|
|
expect(result.value.locations).toEqual([]);
|
|
});
|
|
|
|
const fixture = (): WebNetworkCapture => ({
|
|
decoder: { id: "test-historical-port", name: "test port", version: "1" },
|
|
container: {
|
|
reported: null,
|
|
numeric_literals: [],
|
|
redactions: [],
|
|
records_pointer: "/log/entries",
|
|
},
|
|
total_records: 1,
|
|
records: [
|
|
{
|
|
ordinal: 0,
|
|
location: { kind: "json-pointer", pointer: "/log/entries/0" },
|
|
reported: historicalHar().log.entries[0] ?? null,
|
|
numeric_literals: [],
|
|
binary_fields: [],
|
|
redactions: [],
|
|
limitations: [],
|
|
},
|
|
],
|
|
artifact: { path: "/capture.har", sha256: "a".repeat(64), bytes: 10 },
|
|
format: "har",
|
|
runtime_attribution: "unknown",
|
|
limitations: ["historical fixture"],
|
|
});
|
|
|
|
it.each(["a".repeat(64), "aaaa"])(
|
|
"marks an explicitly sensitive artifact digest unknown without inventing another identity: %s",
|
|
async (literal) => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: [literal],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.subject).toBeNull();
|
|
expect(result.value.normalized_result).toMatchObject({
|
|
artifact: { path: "/capture.har", sha256: null, bytes: 10 },
|
|
});
|
|
expect(result.value.parameters.capture_path).toBe("/capture.har");
|
|
expect(result.value.locations).toEqual([
|
|
{ kind: "artifact-path", path: "/capture.har" },
|
|
]);
|
|
expect(parseEvidence(result.value)).toEqual(result.value);
|
|
},
|
|
);
|
|
|
|
it("projects the unknown-subject explanation before generating its Evidence ID", async () => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: ["a"],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.subject).toBeNull();
|
|
expect(result.value.limitations.every((text) => !text.includes("a"))).toBe(
|
|
true,
|
|
);
|
|
expect(parseEvidence(result.value)).toEqual(result.value);
|
|
});
|
|
|
|
it("rejects an adapter omitting its original observed digest before caller projection", async () => {
|
|
const report = fixture();
|
|
report.artifact.sha256 = null;
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(report)),
|
|
}).inspect({ capture_path: report.artifact.path, format: "har" });
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
|
|
});
|
|
|
|
it.each([
|
|
["producer"],
|
|
["artifact"],
|
|
["coordinates"],
|
|
["producer", "REDACTED", "…"],
|
|
["producer", "[", "]"],
|
|
])(
|
|
"excludes declared literals from all authored limitations: %j",
|
|
async (...literals) => {
|
|
const report = fixture();
|
|
report.artifact.path = "/artifact/capture.har";
|
|
report.limitations = [
|
|
"Historical producer artifact coordinates are retained.",
|
|
];
|
|
const first = report.records[0];
|
|
if (first === undefined) throw new Error("fixture missing record");
|
|
first.limitations = ["This is producer-decoded artifact evidence."];
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(report)),
|
|
}).inspect({
|
|
capture_path: report.artifact.path,
|
|
format: "har",
|
|
sensitive_values: literals,
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
const projected = webNetworkCaptureSchema.parse(
|
|
result.value.normalized_result,
|
|
);
|
|
const texts = [
|
|
result.value.limitations,
|
|
projected.limitations,
|
|
...projected.records.map((record) => record.limitations),
|
|
].flat();
|
|
for (const literal of literals)
|
|
expect(texts.every((text) => !text.includes(literal))).toBe(true);
|
|
expect(result.value.subject?.digest.sha256).toBe(report.artifact.sha256);
|
|
expect(projected.decoder).toEqual(report.decoder);
|
|
expect(projected.records[0]?.reported).toEqual(first.reported);
|
|
},
|
|
);
|
|
|
|
it("keeps explicitly sensitive values out of Evidence parameters and preserves historical authority", async () => {
|
|
const service = new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
});
|
|
const result = await service.inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: ["explicit-private-value"],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.parameters).toMatchObject({
|
|
sensitive_value_count: 1,
|
|
record_ordinals: [0],
|
|
});
|
|
expect(JSON.stringify(result.value)).not.toContain("explicit-private-value");
|
|
expect(result.value.authority).toBe("historical-reference");
|
|
expect(result.value.provider).toEqual(fixture().decoder);
|
|
});
|
|
|
|
it.each(["har", "ha"])(
|
|
"omits a format parameter containing a declared sensitive literal: %s",
|
|
async (literal) => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: [literal],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.parameters).not.toHaveProperty("format");
|
|
expect(JSON.stringify(result.value.parameters)).not.toContain(literal);
|
|
expect(result.value.normalized_result).toMatchObject({ format: "har" });
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ record_ordinals: [0, 0] },
|
|
{ record_ordinals: [1] },
|
|
{ capture_path: "relative.har" },
|
|
])("rejects invalid caller selection %j", async (extra) => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({ capture_path: "/capture.har", format: "har", ...extra });
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError");
|
|
});
|
|
|
|
it("rejects a port changing the artifact identity or producer ordinal sequence", async () => {
|
|
const report = fixture();
|
|
report.artifact.path = "/different.har";
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(report)),
|
|
}).inspect({ capture_path: "/capture.har", format: "har" });
|
|
expect(result.ok).toBe(false);
|
|
if (!result.ok) expect(result.error._tag).toBe("AnalysisOutputError");
|
|
});
|
|
|
|
it("applies an explicit sensitive path literal after verifying the selected artifact", async () => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: ["capture.har"],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(JSON.stringify(result.value)).not.toContain("capture.har");
|
|
expect(result.value.parameters.capture_path).toBe("");
|
|
});
|
|
|
|
it.each(["log", "entries"])(
|
|
"excludes a sensitive producer coordinate without inventing a replacement: %s",
|
|
async (literal) => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: [literal],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.normalized_result).toMatchObject({
|
|
container: { records_pointer: null },
|
|
records: [
|
|
{
|
|
ordinal: 0,
|
|
location: { kind: "unknown", reason: "explicit-sensitive-value" },
|
|
},
|
|
],
|
|
});
|
|
expect(JSON.stringify(result.value)).not.toContain(literal);
|
|
},
|
|
);
|
|
|
|
it("excludes an escaped sensitive sidecar coordinate while preserving its reported field", async () => {
|
|
const report = fixture();
|
|
report.container.reported = { "field/name": 42 };
|
|
report.container.numeric_literals = [
|
|
{ pointer: "/field~1name", producer_type: "json-number", literal: "42" },
|
|
];
|
|
report.container.redactions = [
|
|
{ pointer: "/field~1name", reason: "explicit-sensitive-value" },
|
|
];
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(report)),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: ["~1"],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.normalized_result).toMatchObject({
|
|
container: {
|
|
reported: { "field/name": 42 },
|
|
numeric_literals: [],
|
|
redactions: [],
|
|
},
|
|
});
|
|
expect(JSON.stringify(result.value)).not.toContain("~1");
|
|
});
|
|
|
|
it.each([
|
|
{ literal: "12345", input: { record_ordinals: [12345] } },
|
|
{ literal: "Each", input: { record_ordinals: [0, 0] } },
|
|
{ literal: "absolute", input: { capture_path: "relative.har" } },
|
|
{ literal: "secret", input: { mysecret: true } },
|
|
])(
|
|
"excludes declared text from application-generated input errors: $literal",
|
|
async ({ literal, input }) => {
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(fixture())),
|
|
}).inspect({
|
|
capture_path: "/capture.har",
|
|
format: "har",
|
|
sensitive_values: [literal],
|
|
...input,
|
|
});
|
|
if (result.ok) throw new Error("Expected invalid selection");
|
|
expect(result.error._tag).toBe("AnalysisInputError");
|
|
expect(JSON.stringify(projectAnalysisError(result.error))).not.toContain(
|
|
literal,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(["12345", "67890", "234", "789"])(
|
|
"excludes sensitive native byte-range coordinates: %s",
|
|
async (literal) => {
|
|
const report = fixture();
|
|
report.format = "mitmproxy";
|
|
report.artifact.path = "/capture.mitm";
|
|
const record = report.records[0];
|
|
if (record === undefined) throw new Error("record missing");
|
|
record.location = { kind: "byte-range", offset: 12345, bytes: 67890 };
|
|
const result = await new WebNetworkCaptureService({
|
|
inspect: () => Promise.resolve(ok(report)),
|
|
}).inspect({
|
|
capture_path: report.artifact.path,
|
|
format: "mitmproxy",
|
|
sensitive_values: [literal],
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
expect(result.value.normalized_result).toMatchObject({
|
|
records: [
|
|
{ location: { kind: "unknown", reason: "explicit-sensitive-value" } },
|
|
],
|
|
});
|
|
expect(result.value.raw_result).toBeNull();
|
|
expect(parseEvidence(result.value)).toEqual(result.value);
|
|
},
|
|
);
|