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);
|
|
},
|
|
);
|