fix(captures): preserve canonical TShark alias fields

TShark resolves selected field aliases before writing packet JSON. Keep the requested selector in Evidence while accepting the canonical field key produced for it.

Fixes #6595
This commit is contained in:
morluto
2026-10-23 18:43:36 +08:00
parent 25aa298c61
commit d97db6ebaf
5 changed files with 57 additions and 14 deletions
+2
View File
@@ -95,6 +95,8 @@ for field names, display filters, decode-as syntax and pass semantics. Invalid
fields or filters return producer diagnostics. An empty result does not prove
absence of traffic outside the selected recording, filter or read range.
Dissector results are observations, not independently verified device behavior.
TShark may resolve a selected alias to its canonical field name in packet output;
the report preserves the exact requested selection and the producer's output key.
Evidence includes source path/digest/size, exact selections, typed packet fields,
configured and resolved executable paths/digests/full version banner, arguments and stderr. Warnings remain
+3 -4
View File
@@ -362,10 +362,9 @@ export class TsharkCaptureProvider implements PacketCapturePort {
);
phase = "producer";
resourcePath = "path" in input ? input.path : command;
const matchingPackets = parsePacketOutput(
output.stdout.text,
input.fields,
);
// TShark resolves `-e` aliases before emitting canonical field names;
// the command arguments, not output names, own field selection.
const matchingPackets = parsePacketOutput(output.stdout.text);
const start = Math.min(input.match_offset, matchingPackets.length);
const end =
input.match_limit === null
+3 -7
View File
@@ -6,22 +6,18 @@ import { packetFieldsSchema } from "../domain/captures/packetCapture.js";
const producerSchema = z.array(
z.object({ _source: z.object({ layers: packetFieldsSchema }) }),
);
export const parsePacketOutput = (text: string, fields: readonly string[]) => {
export const parsePacketOutput = (text: string) => {
const decoded: unknown = JSON.parse(text);
const packets = producerSchema
.parse(decoded)
.map((packet) => packet._source.layers);
const selected = new Set(["frame.number", ...fields]);
let previous = 0n;
for (const packet of packets) {
const frame = BigInt(packet["frame.number"][0]);
if (
frame <= previous ||
Object.keys(packet).some((name) => !selected.has(name))
)
if (frame <= previous)
throw new AnalysisOutputError(
"inspect_packet_capture",
"TShark returned unordered frames or unselected fields.",
"TShark returned unordered frames.",
);
previous = frame;
}
@@ -34,6 +34,7 @@ else if (${JSON.stringify(mode)} === 'overflow') process.stdout.write('x'.repeat
else if (${JSON.stringify(mode)} === 'wait') { console.log('ready'); setInterval(() => {}, 1000); }
else if (${JSON.stringify(mode)} === 'invalid-json') console.log('not json');
else if (${JSON.stringify(mode)} === 'many') console.log(JSON.stringify(Array.from({length:90},(_,i)=>({_source:{layers:{"frame.number":[String(i+1)],"ip.addr":["192.0.2.1"]}}}))));
else if (${JSON.stringify(mode)} === 'alias-canonical') { if (!process.argv.includes('bootp.type')) throw Error('alias selector missing'); console.log(JSON.stringify([{_source:{layers:{"frame.number":["1"],"dhcp.type":["1"]}}}])); }
else if (process.argv.includes('-G')) { console.log('P\\tUniversal Serial Bus\\tusb\\nF\\tVendor ID\\tusb.idVendor\\tFT_UINT16\\tusb\\tBASE_HEX\\t0x0\\t\\nF\\tOther Vendor\\tusb.idVendor\\tFT_UINT32\\tusb\\tBASE_HEX\\t0x0\\tSecond registration\\nP\\tInternet Protocol\\tip\\nF\\tAddress\\tip.addr\\tFT_IPv4\\tip\\t\\t0x0\\tSource or destination'); console.error('glossary warning'); }
else { if(readFileSync('capture', 'utf8') !== 'recording bytes') throw Error('wrong copy'); console.log(JSON.stringify([{_source:{layers:{"frame.number":["1"],"ip.addr":["192.0.2.1","192.0.2.2"]}}}])); console.error('producer warning'); }
`,
@@ -175,6 +176,40 @@ it("pages matching observations without changing dissection scope or frame ident
}
});
it("preserves TShark canonical output for a caller-selected field alias", async () => {
const f = await fixture("alias-canonical");
const input = { path: f.path, fields: ["bootp.type"] };
const result = await f.service.inspect(input);
if (!result.ok) throw result.error;
const report = packetCaptureSchema.parse(result.value.normalized_result);
expect(report.selection.fields).toEqual(["bootp.type"]);
expect(report.packets).toEqual([
{ "frame.number": ["1"], "dhcp.type": ["1"] },
]);
expect(report.tool.arguments).toContain("bootp.type");
});
it("returns canonical alias output through the public packet-capture CLI", async () => {
const f = await fixture("alias-canonical");
const cli = Cli.create("capture-alias-test", { sync: false });
registerCaptureCommands(cli, silentLogger, {}, f.service);
let stdout = "";
await cli.serve(
[
"inspect-packet-capture",
f.path,
"--fields",
"bootp.type",
"--format",
"json",
],
{ stdout: (text) => (stdout += text), exit: () => {} },
);
const result: unknown = JSON.parse(stdout);
expect(JSON.stringify(result)).toContain('"bootp.type"');
expect(JSON.stringify(result)).toContain('"dhcp.type":["1"]');
});
it("bounds admission without queuing additional snapshots or delaying cancellation", async () => {
const f = await fixture("gated");
const input = { path: f.path, fields: ["ip.addr"] };
+14 -3
View File
@@ -15,7 +15,7 @@ it("preserves repeated field values and missing fields while discarding export i
},
{ _source: { layers: { "frame.number": ["7"] } } },
]);
expect(parsePacketOutput(output, ["ip.addr"])).toEqual([
expect(parsePacketOutput(output)).toEqual([
{ "frame.number": ["2"], "ip.addr": ["192.0.2.1", "192.0.2.2"] },
{ "frame.number": ["7"] },
]);
@@ -27,7 +27,6 @@ it.each(
[{ "frame.number": ["1"], "ip.addr": "192.0.2.1" }],
[{ "frame.number": ["0"] }],
[{ "frame.number": ["1", "2"] }],
[{ "frame.number": ["1"], "unselected.field": ["hidden"] }],
[{ "frame.number": ["2"] }, { "frame.number": ["2"] }],
[{ "frame.number": ["2"] }, { "frame.number": ["1"] }],
].map((layers) => [layers]),
@@ -35,7 +34,19 @@ it.each(
expect(() =>
parsePacketOutput(
JSON.stringify(layers.map((value) => ({ _source: { layers: value } }))),
["ip.addr"],
),
).toThrow();
});
it("preserves canonical field names emitted for a selected alias", () => {
const output = JSON.stringify([
{
_source: {
layers: { "frame.number": ["1"], "dhcp.type": ["1"] },
},
},
]);
expect(parsePacketOutput(output)).toEqual([
{ "frame.number": ["1"], "dhcp.type": ["1"] },
]);
});