mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 16:22:46 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2b7c544cc | ||
|
|
606c5064a8 | ||
|
|
11f380264a | ||
|
|
05824daa01 |
No files matched your search
@@ -82,15 +82,69 @@ const parse = (
|
||||
bytes: Buffer,
|
||||
queries: readonly string[] = ["source display"],
|
||||
addresses: readonly string[] = ["192.0.2.9"],
|
||||
family: "dns" | "mdns" | "llmnr" = "dns",
|
||||
) =>
|
||||
parseDnsMessageRaw([bytes.toString("hex"), 42, bytes.length, 0, 1], {
|
||||
"dns.id_raw": ["0001", 42, 2, 0, 5],
|
||||
Queries: { "dns.qry.name": queries },
|
||||
Answers: { "dns.a": addresses },
|
||||
});
|
||||
parseDnsMessageRaw(
|
||||
[bytes.toString("hex"), 42, bytes.length, 0, 1],
|
||||
{
|
||||
"dns.id_raw": ["0001", 42, 2, 0, 5],
|
||||
Queries: { "dns.qry.name": queries },
|
||||
Answers: { "dns.a": addresses },
|
||||
},
|
||||
family,
|
||||
);
|
||||
const pointer = Buffer.from([0xc0, 0x0c]);
|
||||
const address = Buffer.from([192, 0, 2, 9]);
|
||||
|
||||
it.each([
|
||||
{ family: "dns", hasAnswers: false },
|
||||
{ family: "dns", hasAnswers: true },
|
||||
{ family: "llmnr", hasAnswers: false },
|
||||
{ family: "llmnr", hasAnswers: true },
|
||||
] as const)(
|
||||
"retains observed records but marks conventional TC responses incomplete: %j",
|
||||
({ family, hasAnswers }) => {
|
||||
const bytes = packet(
|
||||
label("61"),
|
||||
hasAnswers ? [rr(pointer, 1, address)] : [],
|
||||
);
|
||||
bytes.writeUInt16BE(0x8380, 2);
|
||||
const facts = parse(bytes, ["a"], hasAnswers ? ["192.0.2.9"] : [], family);
|
||||
expect(facts.questions).toEqual([{ query: "a", labels_hex: ["61"] }]);
|
||||
expect(facts.assigned).toEqual(hasAnswers ? [[0, "192.0.2.9"]] : []);
|
||||
expect(facts.unattributed).toEqual([]);
|
||||
expect(facts.incomplete).toBe(true);
|
||||
expect(facts.incomplete_reasons).toContain(
|
||||
"DNS message declares truncation (TC); captured records are incomplete",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
"does not infer conventional truncation from multicast DNS TC (has answers: %s)",
|
||||
(hasAnswers) => {
|
||||
const bytes = packet(
|
||||
label("61"),
|
||||
hasAnswers ? [rr(pointer, 1, address)] : [],
|
||||
);
|
||||
bytes.writeUInt16BE(0x8400, 2);
|
||||
const addresses = hasAnswers ? ["192.0.2.9"] : [];
|
||||
const complete = parse(bytes, ["a"], addresses, "mdns");
|
||||
expect(complete.incomplete).toBe(false);
|
||||
bytes.writeUInt16BE(0x8600, 2);
|
||||
expect(parse(bytes, ["a"], addresses, "mdns")).toEqual(complete);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves ownership limitations alongside declared DNS truncation", () => {
|
||||
const bytes = packet(label("61"), [], [rr(pointer, 1, address)]);
|
||||
bytes.writeUInt16BE(0x8380, 2);
|
||||
const facts = parse(bytes);
|
||||
expect(facts.assigned).toEqual([]);
|
||||
expect(facts.unattributed).toEqual(["192.0.2.9"]);
|
||||
expect(facts.incomplete_reasons).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("joins compressed CNAMEs using ASCII-folded bytes while retaining original labels", () => {
|
||||
const query = label("414243", "6578616d706c65");
|
||||
const alias = label("616263", "6578616d706c65");
|
||||
|
||||
+15
-1
@@ -424,10 +424,24 @@ export const parseDnsMessageRaw = (
|
||||
}));
|
||||
const records = decodeRecords(message, at, classValue, observed);
|
||||
if (!records.ok) return unknown(records.error.reason);
|
||||
return attributeAddresses(
|
||||
const facts = attributeAddresses(
|
||||
questions,
|
||||
eligibleQuestion(decodedQuestions, flags),
|
||||
records.value,
|
||||
observed,
|
||||
);
|
||||
// A structurally complete packet can carry only part of the DNS response.
|
||||
// RFC 1035 section 4.1.1 makes TC an explicit producer truncation fact.
|
||||
// RFC 6762 section 18.5 gives mDNS TC distinct semantics: multicast responses
|
||||
// ignore it; queries signal further Known Answers. This owner has no transport
|
||||
// context to identify legacy unicast responses, so do not infer that reading.
|
||||
if (family === "mdns" || (flags & 0x0200) === 0) return facts;
|
||||
return {
|
||||
...facts,
|
||||
incomplete: true,
|
||||
incomplete_reasons: [
|
||||
...(facts.incomplete_reasons ?? []),
|
||||
"DNS message declares truncation (TC); captured records are incomplete",
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -104,6 +104,26 @@ it("rejects duplicate frame identities even when protocol stacks are empty", asy
|
||||
).rejects.toMatchObject({ _tag: "AnalysisProtocolError" });
|
||||
});
|
||||
|
||||
it("marks a TC response partial without losing its observed DNS answer", async () => {
|
||||
const layers = dnsResponseLayers("api.example", "203.0.113.5");
|
||||
const bytes = Buffer.from(layers.dns_raw[0] as string, "hex");
|
||||
bytes.writeUInt16BE(0x8380, 2);
|
||||
layers.dns_raw[0] = bytes.toString("hex");
|
||||
const result = await parseTsharkJsonOutput(capture([layers]));
|
||||
expect(result.dnsQueries).toEqual([
|
||||
{
|
||||
query: "api.example",
|
||||
query_labels_hex: ["617069", "6578616d706c65"],
|
||||
response_addresses: ["203.0.113.5"],
|
||||
response_address_count: 1,
|
||||
},
|
||||
]);
|
||||
expect(result.partial).toBe(true);
|
||||
expect(result.dnsLimitations).toContain(
|
||||
"DNS message declares truncation (TC); captured records are incomplete",
|
||||
);
|
||||
});
|
||||
|
||||
it("binds interleaved DNS answers to each response packet", async () => {
|
||||
const result = await parseTsharkJsonOutput(
|
||||
capture([
|
||||
|
||||
@@ -1194,12 +1194,12 @@ export class PcapProvider {
|
||||
...(dnsLimitation === undefined ? [] : [dnsLimitation]),
|
||||
...this.nativeLimitations,
|
||||
"Both tools read one owned snapshot; target identity comes from its admitted source descriptor",
|
||||
"DNS address attribution requires one wire-identified question in one complete DNS message; only matching answer owners or unambiguous answer-section CNAME chains are associated. Authority, additional, unrelated and ambiguous addresses are retained separately as unattributed; these packet-local relationships do not prove final resolution",
|
||||
"DNS address attribution requires one wire-identified question in one decoded DNS message; declared truncation marks coverage partial. Only matching answer owners or unambiguous answer-section CNAME chains are associated. Authority, additional, unrelated and ambiguous addresses are retained separately as unattributed; these packet-local relationships do not prove final resolution",
|
||||
"Only bytes present in the saved capture are examined; encryption and capture truncation can hide higher-level fields",
|
||||
"HTTP URI inventory examines decoded HTTP/1 and HTTP/2 request fields; HTTP/3 URI semantics are outside this profile",
|
||||
...(fields.partial
|
||||
? [
|
||||
"One or more projected categories were capped, credential-bearing URIs were omitted or coalesced, or DNS address attribution or HTTP URI targets remain unresolved; counts retain the examined totals",
|
||||
"One or more projected categories were capped, credential-bearing URIs were omitted or coalesced, DNS messages declare truncation, or DNS address attribution or HTTP URI targets remain unresolved; counts retain the examined totals",
|
||||
]
|
||||
: []),
|
||||
...(fields.httpUriRedactionCount > 0
|
||||
|
||||
@@ -129,6 +129,29 @@ it("inspects a capture with endpoints, DNS, TLS SNI, and HTTP", async () => {
|
||||
expect(result.coverage).toBe("complete");
|
||||
});
|
||||
|
||||
it("retains DNS answers and exposes declared truncation as partial coverage", async () => {
|
||||
const { provider, root } = await providerWith({
|
||||
dnsJson: TSHARK_DNS_JSON.replace("00018180", "00018380"),
|
||||
});
|
||||
const result = await provider.execute({
|
||||
operation: "inspect_pcap_capture",
|
||||
input: { path: await captureFixture(root) },
|
||||
});
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
expect(result.value.result).toMatchObject({
|
||||
coverage: "partial",
|
||||
dns_queries: [
|
||||
{ query: "api.example.com", response_addresses: ["93.184.216.34"] },
|
||||
],
|
||||
});
|
||||
expect(result.value.limitations).toContain(
|
||||
"DNS wire parsing: DNS message declares truncation (TC); captured records are incomplete",
|
||||
);
|
||||
expect(result.value.limitations).toContainEqual(
|
||||
expect.stringContaining("DNS messages declare truncation"),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([0, 1])(
|
||||
"preserves packet-count mismatch facts while accepting an empty capture: capinfos=%s",
|
||||
async (count) => {
|
||||
|
||||
Reference in new issue
Block a user