mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(pcap): retain absolute HTTP request targets
Reuse parsed URI facts and preserve uncertainty when mixed request records cannot all be resolved from the producer output. Fixes #6537
This commit is contained in:
1 parent
4524bbd8df
commit
1ece9630e6
5 files changed
+117
-5
No files matched your search
@@ -86,6 +86,9 @@ not process identity or proof of the destination's authenticity.
|
||||
All examined inventories are retained within the resource budget; no fixed
|
||||
200/100/50-item truncation is applied. HTTP transport credentials are excluded
|
||||
without rewriting other URI bytes; ambiguous credential spans cause omission.
|
||||
The URI inventory includes absolute-form HTTP request targets when TShark cannot
|
||||
construct `http.request.full_uri` because no Host header was reported; relative
|
||||
request targets are not promoted to absolute URIs.
|
||||
Counts preserve distinct observed source URIs before omission or coalescing.
|
||||
`coverage: "partial"` records unresolved DNS attribution or URI item loss;
|
||||
component credential exclusion is also counted explicitly. Diagnostics retain
|
||||
|
||||
@@ -237,6 +237,56 @@ it("excludes transport credentials without normalizing observed URL bytes", asyn
|
||||
expect(result.partial).toBe(true);
|
||||
});
|
||||
|
||||
it("retains HTTP absolute-form request targets when Wireshark cannot synthesize full_uri", async () => {
|
||||
const result = await parseTsharkJsonOutput(
|
||||
capture([
|
||||
{ "http.request.uri": ["http://proxy.example.test/a/../resource"] },
|
||||
{ "http.request.uri": ["/relative/path"] },
|
||||
]),
|
||||
);
|
||||
expect(result.httpRequestUris).toEqual([
|
||||
"http://proxy.example.test/a/../resource",
|
||||
]);
|
||||
expect(result.httpRequestUriCount).toBe(1);
|
||||
expect(result.partial).toBe(true);
|
||||
});
|
||||
|
||||
it("marks mixed request arrays partial when a relative target has no full URI", async () => {
|
||||
const result = await parseTsharkJsonOutput(
|
||||
capture([
|
||||
{
|
||||
"http.request.uri": ["/resolved/with-host", "/unresolved/without-host"],
|
||||
"http.request.full_uri": ["http://host.example/resolved/with-host"],
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result.httpRequestUris).toEqual([
|
||||
"http://host.example/resolved/with-host",
|
||||
]);
|
||||
expect(result.partial).toBe(true);
|
||||
});
|
||||
|
||||
it("accounts for paired full URIs and absolute-form values without inventing array joins", async () => {
|
||||
const result = await parseTsharkJsonOutput(
|
||||
capture([
|
||||
{
|
||||
"http.request.uri": ["/first", "/second", "http://proxy.example/third"],
|
||||
"http.request.full_uri": [
|
||||
"http://host.example/first",
|
||||
"http://host.example/second",
|
||||
"http://proxy.example/third",
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result.httpRequestUris).toEqual([
|
||||
"http://host.example/first",
|
||||
"http://host.example/second",
|
||||
"http://proxy.example/third",
|
||||
]);
|
||||
expect(result.partial).toBe(false);
|
||||
});
|
||||
|
||||
it("refuses dense JSON before its expanded graph exceeds the decoding budget", async () => {
|
||||
await expect(
|
||||
parseTsharkJsonOutput(`[${"{},".repeat(600_000)}{}]`),
|
||||
|
||||
+45
-3
@@ -212,6 +212,7 @@ export const parseTsharkJsonOutput = async (
|
||||
const uris = new Set<string>();
|
||||
const observedUris = new Set<string>();
|
||||
let uriRedactions = 0;
|
||||
let unresolvedHttpUriTarget = false;
|
||||
const unattributed = new Set<string>();
|
||||
let unattributedPackets = 0;
|
||||
let retainedBytes = 0;
|
||||
@@ -319,10 +320,48 @@ export const parseTsharkJsonOutput = async (
|
||||
if (!protocols.has(protocol)) charge(protocol);
|
||||
protocols.set(protocol, (protocols.get(protocol) ?? 0) + 1);
|
||||
}
|
||||
for (const value of field("http.request.full_uri")) {
|
||||
const fullUris = field("http.request.full_uri");
|
||||
const fullUriCandidates = fullUris.map((value) => ({
|
||||
value,
|
||||
parsed: parseRecordedTransportUrl(value),
|
||||
}));
|
||||
const requestUriCandidates = field("http.request.uri").map((value) => ({
|
||||
value,
|
||||
parsed: parseRecordedTransportUrl(value),
|
||||
}));
|
||||
// Wireshark emits full_uri alongside each request URI it can resolve.
|
||||
// Raw absolute-form targets stand on their own; relative targets need a
|
||||
// corresponding full_uri occurrence. Compare counts only after consuming
|
||||
// exact absolute-form duplicates, since output arrays have no join key.
|
||||
const absoluteRequests = new Map<string, number>();
|
||||
let relativeRequestCount = 0;
|
||||
for (const candidate of requestUriCandidates) {
|
||||
if (candidate.value === "") continue;
|
||||
if (candidate.parsed.origin !== null)
|
||||
absoluteRequests.set(
|
||||
candidate.value,
|
||||
(absoluteRequests.get(candidate.value) ?? 0) + 1,
|
||||
);
|
||||
else relativeRequestCount += 1;
|
||||
}
|
||||
let unmatchedFullUriCount = 0;
|
||||
for (const candidate of fullUriCandidates) {
|
||||
const absoluteCount = absoluteRequests.get(candidate.value) ?? 0;
|
||||
if (absoluteCount > 0)
|
||||
absoluteRequests.set(candidate.value, absoluteCount - 1);
|
||||
else if (candidate.value !== "") unmatchedFullUriCount += 1;
|
||||
}
|
||||
if (relativeRequestCount > unmatchedFullUriCount)
|
||||
unresolvedHttpUriTarget = true;
|
||||
|
||||
for (const candidate of [
|
||||
...fullUriCandidates,
|
||||
...requestUriCandidates.filter(({ parsed }) => parsed.origin !== null),
|
||||
]) {
|
||||
const { value, parsed } = candidate;
|
||||
if (value === "" || observedUris.has(value)) continue;
|
||||
add(observedUris, value);
|
||||
const { credentials } = parseRecordedTransportUrl(value);
|
||||
const { credentials } = parsed;
|
||||
if (credentials !== null) uriRedactions += 1;
|
||||
if (credentials === "ambiguous") continue;
|
||||
add(
|
||||
@@ -362,6 +401,9 @@ export const parseTsharkJsonOutput = async (
|
||||
unattributedDnsAddressCount: unattributed.size,
|
||||
unattributedDnsPackets: unattributedPackets,
|
||||
packetCount: packets.length,
|
||||
partial: observedUris.size > uris.size || unattributedPackets > 0,
|
||||
partial:
|
||||
observedUris.size > uris.size ||
|
||||
unattributedPackets > 0 ||
|
||||
unresolvedHttpUriTarget,
|
||||
};
|
||||
};
|
||||
@@ -74,6 +74,7 @@ const FIELDS = [
|
||||
"dns.aaaa",
|
||||
"tls.handshake.extensions_server_name",
|
||||
"http.request.full_uri",
|
||||
"http.request.uri",
|
||||
];
|
||||
|
||||
const pcapPartialEvidence = (
|
||||
|
||||
@@ -74,7 +74,10 @@ const TSHARK_JSON = JSON.stringify([
|
||||
|
||||
const writeStubTools = async (
|
||||
root: string,
|
||||
options: { readonly failCapinfos?: boolean } = {},
|
||||
options: {
|
||||
readonly failCapinfos?: boolean;
|
||||
readonly httpAbsoluteUriOnly?: boolean;
|
||||
} = {},
|
||||
): Promise<{ tshark: string; capinfos: string }> => {
|
||||
const tshark = join(root, "tshark");
|
||||
const capinfos = join(root, "capinfos");
|
||||
@@ -83,7 +86,8 @@ const writeStubTools = async (
|
||||
`#!/bin/sh
|
||||
if [ "$1" = "--version" ]; then printf 'TShark (Wireshark) 4.6.6-stub\\n'; exit 0; fi
|
||||
[ "$1" = "-n" ] && [ "$2" = "-r" ] && [ "$4" = "-T" ] && [ "$5" = "json" ] || exit 2
|
||||
printf '%s' '${TSHARK_JSON.replace(/'/gu, "'\\''")}'
|
||||
case " $* " in *" -e http.request.uri "*) ;; *) exit 2 ;; esac
|
||||
printf '%s' '${(options.httpAbsoluteUriOnly ? TSHARK_JSON.replace('"http.request.full_uri":["http://api.example.com/v1/data"]', '"http.request.uri":["http://proxy.example.test/path"]') : TSHARK_JSON).replace(/'/gu, "'\\''")}'
|
||||
exit 0
|
||||
`,
|
||||
{ mode: 0o755 },
|
||||
@@ -109,6 +113,7 @@ exit 0
|
||||
const providerWith = async (
|
||||
options: {
|
||||
failCapinfos?: boolean;
|
||||
httpAbsoluteUriOnly?: boolean;
|
||||
launcher?: PcapCommandLauncher;
|
||||
beforeClose?: () => void;
|
||||
} = {},
|
||||
@@ -231,6 +236,17 @@ it("inspects a capture with endpoints, DNS, TLS SNI, and HTTP", async () => {
|
||||
expect(result.coverage).toBe("complete");
|
||||
});
|
||||
|
||||
it("retains an absolute HTTP request target without a Host-derived full URI", async () => {
|
||||
const { provider, root } = await providerWith({ httpAbsoluteUriOnly: true });
|
||||
const path = await captureFixture(root);
|
||||
const result = await evidenceResult(provider, {
|
||||
operation: "inspect_pcap_capture",
|
||||
input: { path },
|
||||
});
|
||||
expect(result.http_request_uris).toEqual(["http://proxy.example.test/path"]);
|
||||
expect(result.coverage).toBe("complete");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ stage: "capinfos", denySnapshotClose: false },
|
||||
{ stage: "tshark", denySnapshotClose: false },
|
||||
|
||||
Reference in new issue
Block a user