refactor(pcap): centralize DNS packet facts

This commit is contained in:
morluto committed 2026-10-27 13:10:24 +08:00
1 parent 5aa23c3d17
commit 98e47e37a2
3 files changed
+53 -24

No files matched your search

+25 -9
View File
@@ -1,5 +1,6 @@
type Fields = (name: string) => readonly string[];
interface Attribution {
interface DnsPacketFacts {
readonly questions: readonly string[];
readonly incomplete: boolean;
readonly assigned: readonly (readonly [string, string])[];
readonly unattributed: readonly string[];
@@ -25,11 +26,16 @@ const nameKey = (name: string): string =>
* ambiguous flat inventories instead of reconstructing missing hierarchy.
* Validate that correspondence before joining any owner to an address.
*/
export const attributeDnsAddresses = (field: Fields): Attribution => {
export const parseDnsPacketFields = (field: Fields): DnsPacketFacts => {
const questions = field("dns.qry.name");
const retainedQuestions = [
...new Set(questions.filter((value) => value !== "")),
];
const ipv4 = field("dns.a");
const ipv6 = field("dns.aaaa");
const addresses = [...ipv4, ...ipv6].filter((value) => value !== "");
const unknown = (): Attribution => ({
const unknown = (): DnsPacketFacts => ({
questions: retainedQuestions,
assigned: [],
unattributed: addresses,
incomplete:
@@ -50,8 +56,9 @@ export const attributeDnsAddresses = (field: Fields): Attribution => {
return Number.isInteger(value) && value <= 65535 ? value : null;
};
const response = field("dns.flags.response");
const questions = field("dns.qry.name");
const questionTypes = field("dns.qry.type");
const questionType =
questionTypes.length === 1 ? dnsType(questionTypes[0]) : null;
const answers = count("dns.count.answers");
const authority = count("dns.count.auth_rr");
const additional = count("dns.count.add_rr");
@@ -74,7 +81,12 @@ export const attributeDnsAddresses = (field: Fields): Attribution => {
)
return unknown();
if (addresses.length === 0)
return { assigned: [], unattributed: [], incomplete: false };
return {
questions: retainedQuestions,
assigned: [],
unattributed: [],
incomplete: false,
};
// Only IN establishes Internet address semantics. An ANY-class question
// also permits IN answers; other classes retain the producer values without
// interpreting their class-specific RDATA as a resolved Internet address.
@@ -90,7 +102,7 @@ export const attributeDnsAddresses = (field: Fields): Attribution => {
questions.length !== 1 ||
questions[0] === "" ||
questionTypes.length !== 1 ||
![1, 28, 255].includes(dnsType(questionTypes[0]) ?? -1)
![1, 28, 255].includes(questionType ?? -1)
)
return unknown();
let a = 0,
@@ -140,12 +152,16 @@ export const attributeDnsAddresses = (field: Fields): Attribution => {
record.dnsClass === 1 &&
record.owner !== "" &&
reachable.has(record.owner) &&
(dnsType(questionTypes[0]) === 255 ||
dnsType(questionTypes[0]) === record.type) &&
(questionType === 255 || questionType === record.type) &&
!links.has(record.owner)
)
assigned.push([questions[0]!, record.value!]);
else unattributed.push(record.value!);
}
return { assigned, unattributed, incomplete: unattributed.length > 0 };
return {
questions: retainedQuestions,
assigned,
unattributed,
incomplete: unattributed.length > 0,
};
};
+19
View File
@@ -114,6 +114,25 @@ it.each([
expect(result.partial).toBe(true);
});
it("retains exact nonempty DNS questions when response ownership is unresolved", async () => {
const result = await parseTsharkJsonOutput(
capture([
{
"dns.flags.response": ["1"],
"dns.qry.name": ["", "WWW.Example", "www.example", "WWW.Example"],
"dns.a": ["192.0.2.10"],
},
]),
);
expect(result.dnsQueries).toEqual([
{ query: "WWW.Example", response_addresses: [], response_address_count: 0 },
{ query: "www.example", response_addresses: [], response_address_count: 0 },
]);
expect(result.dnsQueryCount).toBe(2);
expect(result.unattributedDnsAddresses).toEqual(["192.0.2.10"]);
expect(result.partial).toBe(true);
});
it("reports unknown RR coverage when a DNS response omits all count and address fields", async () => {
const result = await parseTsharkJsonOutput(
capture([
+9 -15
View File
@@ -6,7 +6,7 @@ import {
} from "../domain/analysisErrorCore.js";
import { parseRecordedTransportUrl } from "../domain/recordedTransportUrl.js";
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
import { attributeDnsAddresses } from "./PcapDnsOutput.js";
import { parseDnsPacketFields } from "./PcapDnsOutput.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const OPERATION = "inspect_pcap_capture";
@@ -298,24 +298,18 @@ export const parseTsharkJsonOutput = async (
if (!endpoints.has(address)) charge(address);
endpoints.set(address, (endpoints.get(address) ?? 0) + 1);
}
const questions = new Set(
field("dns.qry.name").filter((value) => value !== ""),
);
for (const question of questions)
const dnsFacts = parseDnsPacketFields(field);
for (const question of dnsFacts.questions)
if (!dns.has(question)) {
charge(question);
dns.set(question, new Set());
}
{
const attribution = attributeDnsAddresses(field);
unresolvedDnsOwnership ||= attribution.incomplete;
for (const [question, address] of attribution.assigned)
add(dns.get(question)!, address);
if (attribution.unattributed.length > 0) {
unattributedPackets += 1;
for (const address of attribution.unattributed)
add(unattributed, address);
}
unresolvedDnsOwnership ||= dnsFacts.incomplete;
for (const [question, address] of dnsFacts.assigned)
add(dns.get(question)!, address);
if (dnsFacts.unattributed.length > 0) {
unattributedPackets += 1;
for (const address of dnsFacts.unattributed) add(unattributed, address);
}
for (const value of field("tls.handshake.extensions_server_name"))
if (value !== "") add(sni, value);