mirror of
https://github.com/morluto/rea
synced 2026-10-25 14:01:13 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"] };
|
||||
|
||||
@@ -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"] },
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user