fix(browser): exclude sensitive binary lengths from historical captures (#2477)
Co-authored-by: chAngE <172472630+chAng-L19@users.noreply.github.com>
This commit is contained in:
1 parent
ab62ee9dcf
commit
68ecf3df8c
4 files changed
+105
-2
No files matched your search
@@ -93,7 +93,7 @@ def project_record(state, secrets):
|
||||
safe_bytes, credential_url = transport_value(value, path, transport_url)
|
||||
content_base64 = base64.b64encode(value).decode("ascii")
|
||||
digest = hashlib.sha256(value).hexdigest()
|
||||
hidden = any(secret in value or secret in safe_bytes for secret in byte_secrets) or any(secret in content_base64 or secret in digest for secret in secrets)
|
||||
hidden = any(secret in value or secret in safe_bytes for secret in byte_secrets) or any(secret in content_base64 or secret in digest or secret in str(len(value)) for secret in secrets)
|
||||
safe_text = None
|
||||
try:
|
||||
safe_text = safe_bytes.decode("utf-8", errors="strict")
|
||||
|
||||
@@ -45,6 +45,20 @@ class Generator:
|
||||
assert "ows-secret" not in json.dumps(projected), projected
|
||||
if isinstance(value, bytes):
|
||||
assert projected["binary_fields"][-1]["state"] == "redacted", projected
|
||||
body = b"A" * 12345
|
||||
state = {"response": {"content": body}}
|
||||
control = adapter.project_record(state, [])
|
||||
field = control["binary_fields"][0]
|
||||
assert field["state"] == "retained" and field["bytes"] == len(body), control
|
||||
for literal in ["12345", "2345"]:
|
||||
# Only the generated byte count matches this exclusion.
|
||||
assert literal not in control["reported"]["response"]["content"]
|
||||
assert literal not in field["content_base64"] and literal not in field["sha256"]
|
||||
projected = adapter.project_record(state, [literal])
|
||||
assert projected["reported"]["response"]["content"] is None, projected
|
||||
assert projected["binary_fields"] == [{"pointer": "/response/content", "representation": "producer-bytes", "state": "redacted", "content_base64": None, "bytes": None, "sha256": None}], projected
|
||||
assert {"pointer": "/response/content", "reason": "explicit-sensitive-value"} in projected["redactions"], projected
|
||||
assert literal not in json.dumps(projected), projected
|
||||
(root / "native-error-classifications.json").write_text(json.dumps({"passed": len(classifications) + 2}))
|
||||
first = http.HTTPFlow(connection.Client(peername=("127.0.0.1", 1), sockname=("127.0.0.1", 2)), connection.Server(address=("example.test", 80)))
|
||||
first.id = "original-producer-id"
|
||||
|
||||
@@ -144,7 +144,8 @@ const projectHar = (
|
||||
(literal) =>
|
||||
item.includes(literal) ||
|
||||
contentBase64.includes(literal) ||
|
||||
sha256.includes(literal),
|
||||
sha256.includes(literal) ||
|
||||
String(bytes.length).includes(literal),
|
||||
);
|
||||
binary_fields.push(
|
||||
hidden
|
||||
|
||||
@@ -7,6 +7,9 @@ import { execFile } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js";
|
||||
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
const entrypoint = fileURLToPath(
|
||||
new URL("../../../scripts/rea.mjs", import.meta.url),
|
||||
);
|
||||
@@ -41,6 +44,91 @@ const cli = async (
|
||||
};
|
||||
import { historicalHar } from "../../fixtures/historicalHar.js";
|
||||
|
||||
it("excludes declared binary lengths through CLI, MCP and retained capture pages", async () => {
|
||||
const root = await createTestTempDirectory("rea-har-byte-length-");
|
||||
const path = join(root, "capture.har");
|
||||
const fixture = historicalHar();
|
||||
const entry = fixture.log.entries[0];
|
||||
if (entry === undefined) throw new Error("fixture missing");
|
||||
const body = Buffer.alloc(12_345, "A");
|
||||
const content = body.toString("base64");
|
||||
const digest = createHash("sha256").update(body).digest("hex");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify({
|
||||
log: {
|
||||
...fixture.log,
|
||||
entries: [
|
||||
{
|
||||
...entry,
|
||||
response: {
|
||||
...entry.response,
|
||||
content: {
|
||||
size: 99,
|
||||
mimeType: "text/plain",
|
||||
encoding: "base64",
|
||||
text: content,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
const args = { capture_path: path, format: "har" };
|
||||
const control = await call("inspect_web_network_capture", args);
|
||||
expect(control.isError).not.toBe(true);
|
||||
const retained = parseEvidence(control.structuredContent);
|
||||
const controlCapture = webNetworkCaptureSchema.parse(
|
||||
retained.normalized_result,
|
||||
);
|
||||
expect(controlCapture.records[0]?.binary_fields[0]).toMatchObject({
|
||||
state: "retained",
|
||||
bytes: body.length,
|
||||
content_base64: content,
|
||||
sha256: digest,
|
||||
});
|
||||
for (const literal of ["12345", "2345"]) {
|
||||
// Only the derived count matches, so this cannot pass via another exclusion.
|
||||
expect(content).not.toContain(literal);
|
||||
expect(digest).not.toContain(literal);
|
||||
const fromCli = await cli(path, "har", ["--sensitive-value", literal]);
|
||||
expect(fromCli.ok, fromCli.stdout).toBe(true);
|
||||
const evidence = parseEvidence(JSON.parse(fromCli.stdout));
|
||||
const response = await call("inspect_web_network_capture", {
|
||||
...args,
|
||||
sensitive_values: [literal],
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const fromMcp = parseEvidence(response.structuredContent);
|
||||
expect(fromMcp.normalized_result).toEqual(evidence.normalized_result);
|
||||
const capture = webNetworkCaptureSchema.parse(evidence.normalized_result);
|
||||
expect(capture.records[0]?.reported).toMatchObject({
|
||||
response: { content: { size: 99, text: null } },
|
||||
});
|
||||
expect(capture.records[0]?.binary_fields[0]).toEqual({
|
||||
pointer: "/response/content/text",
|
||||
representation: "har-base64-content",
|
||||
state: "redacted",
|
||||
bytes: null,
|
||||
content_base64: null,
|
||||
sha256: null,
|
||||
});
|
||||
for (const detail of ["preview", "complete"]) {
|
||||
const page = await call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: fromMcp.evidence_id },
|
||||
view: { kind: "capture", detail },
|
||||
});
|
||||
expect(page.isError).not.toBe(true);
|
||||
const result = parseEvidence(page.structuredContent);
|
||||
expect(result.normalized_result).toMatchObject({
|
||||
items: [{ binary_fields: [{ state: "redacted", bytes: null }] }],
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 60_000);
|
||||
|
||||
it("reads actual upstream HAR packages in an owned process and retains selected producer order", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-history-har-"));
|
||||
onTestFinished(() => rm(root, { recursive: true, force: true }));
|
||||
|
||||
Reference in new issue
Block a user