From 6732d8ba6b5360647ffa89bc4277abf3f5a3e5c7 Mon Sep 17 00:00:00 2001 From: dipesh Date: Thu, 19 Feb 2026 16:50:28 -0500 Subject: [PATCH 1/4] fix: prevent decode-as registration growth while preserving defaults Avoid duplicate decode-as default reapplication across module traversal and keep PREF_DECODE_AS_RANGE updates merged with protocol defaults to prevent runaway memory growth on Node 20. --- lib/wiregasm/wiregasm.cpp | 29 ++++++++--------------------- src/index.test.ts | 6 ++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/lib/wiregasm/wiregasm.cpp b/lib/wiregasm/wiregasm.cpp index 54b4a38..b77e842 100644 --- a/lib/wiregasm/wiregasm.cpp +++ b/lib/wiregasm/wiregasm.cpp @@ -43,22 +43,8 @@ static guint wg_apply_decode_as_pref_cb(pref_t *pref, gpointer user_data) { return 0; } -static guint wg_apply_decode_as_module_cb(module_t *module, gpointer user_data); - -static void wg_apply_decode_as_for_module(module_t *module) { - if (module == NULL) { - return; - } - - prefs_pref_foreach(module, wg_apply_decode_as_pref_cb, module); - - if (prefs_module_has_submodules(module)) { - prefs_modules_foreach_submodules(module, wg_apply_decode_as_module_cb, NULL); - } -} - static guint wg_apply_decode_as_module_cb(module_t *module, gpointer user_data _U_) { - wg_apply_decode_as_for_module(module); + prefs_pref_foreach(module, wg_apply_decode_as_pref_cb, module); return 0; } @@ -428,17 +414,18 @@ SetPrefResponse wg_set_pref(string module_name, string pref_name, string value) // handle decode as range ourselves if (type == PREF_DECODE_AS_RANGE) { - // get current range and merge with new value so defaults are preserved - range_t *current_range = prefs_get_range_value_real(p, pref_current); - char *current_range_str = range_convert_range(NULL, current_range); + // merge with default range so builtin ports are preserved, while + // avoiding unbounded growth from repeatedly appending current values. + range_t *default_range = prefs_get_range_value_real(p, pref_default); + char *default_range_str = range_convert_range(NULL, default_range); string merged_str; - if (current_range_str != NULL && strlen(current_range_str) > 0) { - merged_str = string(current_range_str) + "," + value; + if (default_range_str != NULL && strlen(default_range_str) > 0) { + merged_str = string(default_range_str) + "," + value; } else { merged_str = value; } - wmem_free(NULL, current_range_str); + wmem_free(NULL, default_range_str); range_t *merged_range = NULL; convert_ret_t ret = range_convert_str(NULL, &merged_range, merged_str.c_str(), prefs_get_max_value(p)); diff --git a/src/index.test.ts b/src/index.test.ts index 155e070..84a92e6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -794,6 +794,12 @@ describe("Wiregasm Library - Set Preferences", () => { expect(pref2.range_value).toContain("8001"); // defaults should still be present expect(pref2.range_value).toContain("80"); + + // setting it again should still preserve defaults + wg.set_pref("http", "tcp.port", "8002"); + const pref3 = wg.get_pref("http", "tcp.port"); + expect(pref3.range_value).toContain("8002"); + expect(pref3.range_value).toContain("80"); }); test("set preferences works for diameter", async () => { From e4dcfa39192dbe1c3b314d4c8c60f61dbe668c69 Mon Sep 17 00:00:00 2001 From: dipesh Date: Mon, 11 May 2026 13:55:33 -0400 Subject: [PATCH 2/4] feat: add field extraction, present fields, and protocol hierarchy APIs --- lib/wiregasm/bindings.cpp | 58 +++++++ lib/wiregasm/lib.cpp | 344 ++++++++++++++++++++++++++++++++++++++ lib/wiregasm/lib.h | 3 + lib/wiregasm/wiregasm.cpp | 12 ++ lib/wiregasm/wiregasm.h | 47 ++++++ src/index.test.ts | 56 +++++++ src/index.ts | 102 ++++++++++- src/types.ts | 60 +++++++ 8 files changed, 676 insertions(+), 6 deletions(-) diff --git a/lib/wiregasm/bindings.cpp b/lib/wiregasm/bindings.cpp index 1d5923e..a78112b 100644 --- a/lib/wiregasm/bindings.cpp +++ b/lib/wiregasm/bindings.cpp @@ -31,6 +31,9 @@ EMSCRIPTEN_BINDINGS(DissectSession) { .function("tap", &DissectSession::tap) .function("download", &DissectSession::download) .function("iograph", &DissectSession::iograph) + .function("extractFields", &DissectSession::extractFields) + .function("listPresentFields", &DissectSession::listPresentFields) + .function("protocolHierarchy", &DissectSession::protocolHierarchy) .function("follow", &DissectSession::follow); } @@ -193,6 +196,57 @@ EMSCRIPTEN_BINDINGS(IoGraphResult) { register_vector("VectorIoGraph"); } +EMSCRIPTEN_BINDINGS(FieldValue) { + value_object("FieldValue") + .field("field", &FieldValue::field) + .field("value", &FieldValue::value) + .field("raw", &FieldValue::raw); +} + +EMSCRIPTEN_BINDINGS(ExtractedRow) { + value_object("ExtractedRow") + .field("framenum", &ExtractedRow::framenum) + .field("values", &ExtractedRow::values); +} + +EMSCRIPTEN_BINDINGS(ExtractFieldsResponse) { + value_object("ExtractFieldsResponse") + .field("error", &ExtractFieldsResponse::error) + .field("matched", &ExtractFieldsResponse::matched) + .field("total_rows", &ExtractFieldsResponse::total_rows) + .field("truncated", &ExtractFieldsResponse::truncated) + .field("rows", &ExtractFieldsResponse::rows); +} + +EMSCRIPTEN_BINDINGS(PresentField) { + value_object("PresentField") + .field("field", &PresentField::field) + .field("name", &PresentField::name) + .field("type", &PresentField::type) + .field("occurrences", &PresentField::occurrences); +} + +EMSCRIPTEN_BINDINGS(PresentFieldsResponse) { + value_object("PresentFieldsResponse") + .field("error", &PresentFieldsResponse::error) + .field("fields", &PresentFieldsResponse::fields); +} + +EMSCRIPTEN_BINDINGS(ProtocolNode) { + value_object("ProtocolNode") + .field("filter", &ProtocolNode::filter) + .field("name", &ProtocolNode::name) + .field("frames", &ProtocolNode::frames) + .field("bytes", &ProtocolNode::bytes) + .field("children", &ProtocolNode::children); +} + +EMSCRIPTEN_BINDINGS(ProtocolHierarchyResponse) { + value_object("ProtocolHierarchyResponse") + .field("error", &ProtocolHierarchyResponse::error) + .field("protocols", &ProtocolHierarchyResponse::protocols); +} + EMSCRIPTEN_BINDINGS(stl_wrappers) { register_vector("VectorString"); register_vector("VectorFloat"); @@ -203,6 +257,10 @@ EMSCRIPTEN_BINDINGS(stl_wrappers) { register_vector("VectorPrefModule"); register_vector("VectorPrefData"); register_vector("VectorPrefEnum"); + register_vector("VectorFieldValue"); + register_vector("VectorExtractedRow"); + register_vector("VectorPresentField"); + register_vector("VectorProtocolNode"); // Frame::follow is a vector of vectors of strings register_vector>("VectorVectorString"); } diff --git a/lib/wiregasm/lib.cpp b/lib/wiregasm/lib.cpp index e4bf78c..075033c 100644 --- a/lib/wiregasm/lib.cpp +++ b/lib/wiregasm/lib.cpp @@ -1,5 +1,9 @@ #include "lib.h" +#include +#include +#include + static guint32 cum_bytes; static frame_data ref_frame; @@ -1004,6 +1008,346 @@ FramesResponse wg_process_frames(capture_file *cfile, GHashTable *filter_table, return result; } +static void wg_collect_field_values(proto_tree *tree, const map &hfid_to_field, vector *values) { + for (proto_node *node = tree->first_child; node; node = node->next) { + field_info *finfo = PNODE_FINFO(node); + + if (!finfo) { + if (((proto_tree *)node)->first_child) { + wg_collect_field_values((proto_tree *)node, hfid_to_field, values); + } + continue; + } + + if (finfo->hfinfo) { + auto field_it = hfid_to_field.find(finfo->hfinfo->id); + if (field_it != hfid_to_field.end()) { + FieldValue field_value; + field_value.field = field_it->second; + + if (finfo->value) { + char *display = fvalue_to_string_repr(NULL, finfo->value, FTREPR_DISPLAY, finfo->hfinfo->display); + if (display) { + field_value.value = display; + wmem_free(NULL, display); + } + } + + if (field_value.value.empty()) { + char label_str[ITEM_LABEL_LENGTH]; + label_str[0] = '\0'; + proto_item_fill_label(finfo, label_str); + field_value.value = string(label_str); + } + + values->push_back(field_value); + } + } + + if (((proto_tree *)node)->first_child) { + wg_collect_field_values((proto_tree *)node, hfid_to_field, values); + } + } +} + +ExtractFieldsResponse wg_session_process_extract_fields(capture_file *cfile, const vector &fields, const char *filter, guint32 limit) { + ExtractFieldsResponse response; + response.matched = 0; + response.total_rows = 0; + response.truncated = false; + + map hfid_to_field; + for (const auto &field : fields) { + const int hfid = proto_registrar_get_id_byname(field.c_str()); + if (hfid < 0) { + response.error = string("Field not found: ") + field; + return response; + } + hfid_to_field[hfid] = field; + } + + dfilter_t *dfcode = NULL; + if (filter && filter[0]) { + df_error_t *dferr = NULL; + if (!dfilter_compile(filter, &dfcode, &dferr)) { + response.error = dferr ? string(dferr->msg) : "Filter expression invalid"; + if (dferr) + g_free(dferr); + return response; + } + } + + wtap_rec rec; + Buffer rec_buf; + int err = 0; + char *err_info = NULL; + + wtap_rec_init(&rec); + ws_buffer_init(&rec_buf, 1514); + + epan_dissect_t edt; + epan_dissect_init(&edt, cfile->epan, TRUE, FALSE); + + guint32 prev_dis_num = 0; + for (guint32 framenum = 1; framenum <= cfile->count; framenum++) { + frame_data *fdata = wg_get_frame(cfile, framenum); + if (!fdata) + continue; + + if (!wtap_seek_read(cfile->provider.wth, fdata->file_off, &rec, &rec_buf, &err, &err_info)) { + break; + } + + for (const auto &entry : hfid_to_field) { + epan_dissect_prime_with_hfid(&edt, entry.first); + } + + if (dfcode) + epan_dissect_prime_with_dfilter(&edt, dfcode); + + fdata->ref_time = FALSE; + fdata->frame_ref_num = (framenum != 1) ? 1 : 0; + fdata->prev_dis_num = prev_dis_num; + + epan_dissect_run(&edt, cfile->cd_t, &rec, + frame_tvbuff_new_buffer(&cfile->provider, fdata, &rec_buf), + fdata, NULL); + + const bool passed = (dfcode == NULL || dfilter_apply_edt(dfcode, &edt)); + if (passed) { + response.matched++; + prev_dis_num = framenum; + + ExtractedRow row; + row.framenum = framenum; + if (edt.tree) { + wg_collect_field_values(edt.tree, hfid_to_field, &row.values); + } + + if (!row.values.empty()) { + response.rows.push_back(row); + if (limit > 0 && response.rows.size() >= limit) { + response.truncated = true; + wtap_rec_reset(&rec); + epan_dissect_reset(&edt); + break; + } + } + } + + wtap_rec_reset(&rec); + epan_dissect_reset(&edt); + } + + response.total_rows = response.rows.size(); + + epan_dissect_cleanup(&edt); + wtap_rec_cleanup(&rec); + ws_buffer_free(&rec_buf); + + if (dfcode) + dfilter_free(dfcode); + if (err_info) + g_free(err_info); + + return response; +} + +static void wg_collect_present_fields(proto_tree *tree, + set *frame_fields, + map *present_fields) { + for (proto_node *node = tree->first_child; node; node = node->next) { + field_info *finfo = PNODE_FINFO(node); + if (!finfo || !finfo->hfinfo || !finfo->hfinfo->abbrev) { + if (((proto_tree *)node)->first_child) { + wg_collect_present_fields((proto_tree *)node, frame_fields, present_fields); + } + continue; + } + + const string field_name = finfo->hfinfo->abbrev; + if (field_name.empty()) + continue; + + if (frame_fields->insert(field_name).second) { + auto it = present_fields->find(field_name); + if (it == present_fields->end()) { + PresentField present; + present.field = field_name; + present.name = finfo->hfinfo->name ? finfo->hfinfo->name : ""; + present.type = static_cast(finfo->hfinfo->type); + present.occurrences = 1; + (*present_fields)[field_name] = present; + } else { + it->second.occurrences++; + } + } + + if (((proto_tree *)node)->first_child) { + wg_collect_present_fields((proto_tree *)node, frame_fields, present_fields); + } + } +} + +PresentFieldsResponse wg_session_process_present_fields(capture_file *cfile) { + PresentFieldsResponse response; + map fields_map; + + wtap_rec rec; + Buffer rec_buf; + int err = 0; + char *err_info = NULL; + + wtap_rec_init(&rec); + ws_buffer_init(&rec_buf, 1514); + + epan_dissect_t edt; + epan_dissect_init(&edt, cfile->epan, TRUE, FALSE); + + guint32 prev_dis_num = 0; + for (guint32 framenum = 1; framenum <= cfile->count; framenum++) { + frame_data *fdata = wg_get_frame(cfile, framenum); + if (!fdata) + continue; + + if (!wtap_seek_read(cfile->provider.wth, fdata->file_off, &rec, &rec_buf, &err, &err_info)) { + break; + } + + fdata->ref_time = FALSE; + fdata->frame_ref_num = (framenum != 1) ? 1 : 0; + fdata->prev_dis_num = prev_dis_num; + + epan_dissect_run(&edt, cfile->cd_t, &rec, + frame_tvbuff_new_buffer(&cfile->provider, fdata, &rec_buf), + fdata, NULL); + + prev_dis_num = framenum; + + set frame_fields; + if (edt.tree) { + wg_collect_present_fields(edt.tree, &frame_fields, &fields_map); + } + + wtap_rec_reset(&rec); + epan_dissect_reset(&edt); + } + + for (const auto &entry : fields_map) { + response.fields.push_back(entry.second); + } + + sort(response.fields.begin(), response.fields.end(), [](const PresentField &a, const PresentField &b) { + return a.field < b.field; + }); + + epan_dissect_cleanup(&edt); + wtap_rec_cleanup(&rec); + ws_buffer_free(&rec_buf); + + if (err_info) + g_free(err_info); + + return response; +} + +static ProtocolNode *wg_find_or_create_protocol_node(vector *nodes, const string &filter, const string &name) { + for (auto &node : *nodes) { + if (node.filter == filter) + return &node; + } + + ProtocolNode new_node; + new_node.filter = filter; + new_node.name = name; + new_node.frames = 0; + new_node.bytes = 0; + nodes->push_back(new_node); + return &nodes->back(); +} + +static void wg_collect_protocol_hierarchy(proto_tree *tree, + vector *nodes, + unsigned int frame_bytes) { + for (proto_node *node = tree->first_child; node; node = node->next) { + field_info *finfo = PNODE_FINFO(node); + if (!finfo || !finfo->hfinfo) { + if (((proto_tree *)node)->first_child) { + wg_collect_protocol_hierarchy((proto_tree *)node, nodes, frame_bytes); + } + continue; + } + + if (finfo->hfinfo->type == FT_PROTOCOL && finfo->hfinfo->abbrev) { + ProtocolNode *protocol = wg_find_or_create_protocol_node( + nodes, + string(finfo->hfinfo->abbrev), + finfo->hfinfo->name ? string(finfo->hfinfo->name) : ""); + + protocol->frames++; + protocol->bytes += frame_bytes; + + if (((proto_tree *)node)->first_child) { + wg_collect_protocol_hierarchy((proto_tree *)node, &protocol->children, frame_bytes); + } + } else if (((proto_tree *)node)->first_child) { + wg_collect_protocol_hierarchy((proto_tree *)node, nodes, frame_bytes); + } + } +} + +ProtocolHierarchyResponse wg_session_process_protocol_hierarchy(capture_file *cfile) { + ProtocolHierarchyResponse response; + + wtap_rec rec; + Buffer rec_buf; + int err = 0; + char *err_info = NULL; + + wtap_rec_init(&rec); + ws_buffer_init(&rec_buf, 1514); + + epan_dissect_t edt; + epan_dissect_init(&edt, cfile->epan, TRUE, FALSE); + + guint32 prev_dis_num = 0; + for (guint32 framenum = 1; framenum <= cfile->count; framenum++) { + frame_data *fdata = wg_get_frame(cfile, framenum); + if (!fdata) + continue; + + if (!wtap_seek_read(cfile->provider.wth, fdata->file_off, &rec, &rec_buf, &err, &err_info)) { + break; + } + + fdata->ref_time = FALSE; + fdata->frame_ref_num = (framenum != 1) ? 1 : 0; + fdata->prev_dis_num = prev_dis_num; + + epan_dissect_run(&edt, cfile->cd_t, &rec, + frame_tvbuff_new_buffer(&cfile->provider, fdata, &rec_buf), + fdata, NULL); + + prev_dis_num = framenum; + + if (edt.tree) { + wg_collect_protocol_hierarchy(edt.tree, &response.protocols, static_cast(fdata->pkt_len)); + } + + wtap_rec_reset(&rec); + epan_dissect_reset(&edt); + } + + epan_dissect_cleanup(&edt); + wtap_rec_cleanup(&rec); + ws_buffer_free(&rec_buf); + + if (err_info) + g_free(err_info); + + return response; +} + Follow wg_process_follow(capture_file *cfile, const char *follow, const char *filter, char **err_ret) { Follow fdata = wg_session_process_follow(cfile, follow, filter, err_ret); return fdata; diff --git a/lib/wiregasm/lib.h b/lib/wiregasm/lib.h index 796f54e..c51f52a 100644 --- a/lib/wiregasm/lib.h +++ b/lib/wiregasm/lib.h @@ -73,6 +73,9 @@ bool wg_session_eo_retap_listener(capture_file *cfile, const char *tap_type, cha DownloadResponse wg_session_process_download(capture_file *cfile, const char *token); TapResponse wg_session_process_tap(capture_file *cfile, MapInput taps); IoGraphResult wg_session_process_iograph(capture_file *cfile, MapInput input); +ExtractFieldsResponse wg_session_process_extract_fields(capture_file *cfile, const vector &fields, const char *filter, guint32 limit); +PresentFieldsResponse wg_session_process_present_fields(capture_file *cfile); +ProtocolHierarchyResponse wg_session_process_protocol_hierarchy(capture_file *cfile); vector wg_session_process_complete(const char *field); void cf_close(capture_file *cf); diff --git a/lib/wiregasm/wiregasm.cpp b/lib/wiregasm/wiregasm.cpp index 54b4a38..918c211 100644 --- a/lib/wiregasm/wiregasm.cpp +++ b/lib/wiregasm/wiregasm.cpp @@ -608,3 +608,15 @@ IoGraphResult DissectSession::iograph(MapInput args) { } return wg_session_process_iograph(&this->capture_file, args); } + +ExtractFieldsResponse DissectSession::extractFields(vector fields, string filter, int limit) { + return wg_session_process_extract_fields(&this->capture_file, fields, filter.c_str(), limit < 0 ? 0 : static_cast(limit)); +} + +PresentFieldsResponse DissectSession::listPresentFields() { + return wg_session_process_present_fields(&this->capture_file); +} + +ProtocolHierarchyResponse DissectSession::protocolHierarchy() { + return wg_session_process_protocol_hierarchy(&this->capture_file); +} diff --git a/lib/wiregasm/wiregasm.h b/lib/wiregasm/wiregasm.h index f2b1df9..4cb8c1c 100644 --- a/lib/wiregasm/wiregasm.h +++ b/lib/wiregasm/wiregasm.h @@ -103,6 +103,50 @@ struct IoGraphResult { vector iograph; }; +struct FieldValue { + string field; + string value; + string raw; +}; + +struct ExtractedRow { + int framenum; + vector values; +}; + +struct ExtractFieldsResponse { + string error; + unsigned int matched; + unsigned int total_rows; + bool truncated; + vector rows; +}; + +struct PresentField { + string field; + string name; + int type; + int occurrences; +}; + +struct PresentFieldsResponse { + string error; + vector fields; +}; + +struct ProtocolNode { + string filter; + string name; + unsigned int frames; + unsigned int bytes; + vector children; +}; + +struct ProtocolHierarchyResponse { + string error; + vector protocols; +}; + // base struct struct TapValue { string tap; @@ -273,6 +317,9 @@ class DissectSession { Follow follow(string follow, string filter); TapResponse tap(MapInput taps); IoGraphResult iograph(MapInput args); + ExtractFieldsResponse extractFields(vector fields, string filter, int limit); + PresentFieldsResponse listPresentFields(); + ProtocolHierarchyResponse protocolHierarchy(); DownloadResponse download(string token); ~DissectSession(); }; diff --git a/src/index.test.ts b/src/index.test.ts index 155e070..a00b95d 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -168,6 +168,62 @@ describe("Wiregasm Library Wrapper", () => { }); expect(wg.complete_filter("txx").fields.length).toBe(0); }); + + test("extract_fields returns per-frame values", async () => { + const data = await fs.readFile("samples/http.cap"); + const ret = wg.load("http.cap", data); + expect(ret.code).toEqual(0); + + const result = wg.extract_fields(["http.host"], "http.host", 10); + expect(result.error).toBe(""); + expect(result.matched).toBeGreaterThan(0); + expect(result.total_rows).toBeGreaterThan(0); + expect(result.rows[0].values[0].field).toBe("http.host"); + }); + + test("extract_fields supports invalid filter and truncation", async () => { + const data = await fs.readFile("samples/http.cap"); + const ret = wg.load("http.cap", data); + expect(ret.code).toEqual(0); + + const invalid = wg.extract_fields(["http.host"], "http && &&", 0); + expect(invalid.error).not.toBe(""); + + const truncated = wg.extract_fields(["http.host"], "http.host", 1); + expect(truncated.error).toBe(""); + expect(truncated.total_rows).toBe(1); + expect(truncated.truncated).toBe(true); + }); + + test("list_present_fields and protocol_hierarchy work", async () => { + const data = await fs.readFile("samples/http.cap"); + const ret = wg.load("http.cap", data); + expect(ret.code).toEqual(0); + + const fields = wg.list_present_fields("(?i)http"); + expect(fields.length).toBeGreaterThan(0); + expect(fields.some((field) => field.field === "http.host")).toBe(true); + + const hierarchy = wg.protocol_hierarchy(); + expect(hierarchy.length).toBeGreaterThan(0); + + const hasProtocol = ( + nodes: Array<{ filter: string; children: Array }>, + target: string + ): boolean => { + for (const node of nodes) { + if (node.filter === target) { + return true; + } + if (hasProtocol(node.children, target)) { + return true; + } + } + return false; + }; + + expect(hasProtocol(hierarchy, "ip")).toBe(true); + }); }); describe("Wiregasm Library - Export Objects", () => { diff --git a/src/index.ts b/src/index.ts index 2cd94eb..296d227 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,14 +4,17 @@ import { CompleteField, DissectSession, DownloadResponse, + FieldValue, Follow, Frame, FramesResponse, LoadResponse, MapInput, + PresentField, Pref, PrefModule, PrefSetResult, + ProtocolNode, TapConvResponse, TapExportObjectResponse, TapResponse, @@ -94,9 +97,9 @@ export class Wiregasm { set_pref(module: string, key: string, value: string) { const ret = this.lib.setPref(module, key, value); - if (ret.code != PrefSetResult.PREFS_SET_OK) { + if (ret.code !== PrefSetResult.PREFS_SET_OK) { const message = - ret.error != "" ? ret.error : preferenceSetCodeToError(ret.code); + ret.error !== "" ? ret.error : preferenceSetCodeToError(ret.code); throw new Error( `Failed to set preference (${module}.${key}): ${message}` ); @@ -105,7 +108,7 @@ export class Wiregasm { get_pref(module: string, key: string): Pref { const response = this.lib.getPref(module, key); - if (response.code != 0) { + if (response.code !== 0) { throw new Error(`Failed to get preference (${module}.${key})`); } return response.data; @@ -141,13 +144,29 @@ export class Wiregasm { } const args = new this.lib.MapInput(); - Object.entries(taps).forEach(([k, v]) => args.set(k, v)); + Object.entries(taps).forEach(([k, v]) => { + args.set(k, v); + }); const response = this.session.tap(args); return { error: response.error, taps: vectorToArray(response.taps).map((tap) => { - let res; + let res: + | { + proto: string; + tap: string; + type: string; + geoip: boolean; + convs: unknown[]; + hosts: unknown[]; + } + | { + proto: string; + tap: string; + type: string; + objects: unknown[]; + }; if (this.is_conv_tap(tap)) { res = { proto: tap.proto, @@ -192,7 +211,9 @@ export class Wiregasm { } const args = new this.lib.MapInput(); - Object.entries(input).forEach(([k, v]) => args.set(k, v)); + Object.entries(input).forEach(([k, v]) => { + args.set(k, v); + }); const out = this.session.iograph(args); return { @@ -203,6 +224,71 @@ export class Wiregasm { }; } + extract_fields( + fields: string[], + filter = "", + limit = 0 + ): { + error: string; + matched: number; + total_rows: number; + truncated: boolean; + rows: Array<{ framenum: number; values: FieldValue[] }>; + } { + const fieldVec = new this.lib.VectorString(); + fields.forEach((f) => { + fieldVec.push_back(f); + }); + + const response = this.session.extractFields( + fieldVec as unknown as Vector, + filter, + limit + ); + return { + error: response.error, + matched: response.matched, + total_rows: response.total_rows, + truncated: response.truncated, + rows: vectorToArray(response.rows).map((row) => ({ + framenum: row.framenum, + values: vectorToArray(row.values), + })), + }; + } + + list_present_fields(query = ""): PresentField[] { + const response = this.session.listPresentFields(); + if (response.error) { + throw new Error(response.error); + } + + const fields = vectorToArray(response.fields); + if (!query) { + return fields; + } + + const regex = new RegExp(query); + return fields.filter( + (field) => regex.test(field.field) || regex.test(field.name) + ); + } + + protocol_hierarchy(): ProtocolHierarchyNode[] { + const response = this.session.protocolHierarchy(); + if (response.error) { + throw new Error(response.error); + } + + const convert = (nodes: Vector): ProtocolHierarchyNode[] => + vectorToArray(nodes).map((node) => ({ + ...node, + children: convert(node.children), + })); + + return convert(response.protocols); + } + reload_lua_plugins() { this.lib.reloadLuaPlugins(); } @@ -289,5 +375,9 @@ export class Wiregasm { } } +type ProtocolHierarchyNode = Omit & { + children: ProtocolHierarchyNode[]; +}; + export * from "./types"; export * from "./utils"; diff --git a/src/types.ts b/src/types.ts index 3bc3ec6..00d592b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -244,6 +244,50 @@ export interface IoGraph { items: Vector; } +export interface FieldValue { + field: string; + value: string; + raw: string; +} + +export interface ExtractedRow { + framenum: number; + values: Vector; +} + +export interface ExtractFieldsResponse { + error: string; + matched: number; + total_rows: number; + truncated: boolean; + rows: Vector; +} + +export interface PresentField { + field: string; + name: string; + type: number; + occurrences: number; +} + +export interface PresentFieldsResponse { + error: string; + fields: Vector; +} + +export interface ProtocolNode { + filter: string; + name: string; + frames: number; + bytes: number; + children: Vector; +} + +export interface ProtocolHierarchyResponse { + error: string; + protocols: Vector; +} + export interface DissectSession { /** * Free up any memory used by the session @@ -282,6 +326,16 @@ export interface DissectSession { iograph(input: Map): IoGraphResult; + extractFields( + fields: Vector, + filter: string, + limit: number + ): ExtractFieldsResponse; + + listPresentFields(): PresentFieldsResponse; + + protocolHierarchy(): ProtocolHierarchyResponse; + download(token: string): DownloadResponse; } @@ -352,6 +406,12 @@ export interface WiregasmLibOverrides { export interface WiregasmLib extends EmscriptenModule { DissectSession: DissectSessionConstructable; + VectorString: { + new (): { + push_back(value: string): void; + }; + }; + MapInput: MapConstructor; TapExportObject: () => TapExportObjectResponse; TapConvResponse: () => TapConvResponse; From 4e718c58cccb2611d67f43408b5f9d26597777a7 Mon Sep 17 00:00:00 2001 From: dipesh Date: Thu, 14 May 2026 12:04:38 -0400 Subject: [PATCH 3/4] fix: harden field matching and de-duplicate protocol hierarchy counters Avoid compiling untrusted present-field queries as regex by using case-insensitive substring matching with (?i) compatibility, and prevent protocol frames/bytes over-counting when a protocol appears multiple times in a single frame. --- lib/wiregasm/lib.cpp | 12 +++++++++--- src/index.ts | 8 ++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/wiregasm/lib.cpp b/lib/wiregasm/lib.cpp index 075033c..5c20068 100644 --- a/lib/wiregasm/lib.cpp +++ b/lib/wiregasm/lib.cpp @@ -1269,6 +1269,8 @@ static ProtocolNode *wg_find_or_create_protocol_node(vector *nodes static void wg_collect_protocol_hierarchy(proto_tree *tree, vector *nodes, unsigned int frame_bytes) { + set counted_protocols; + for (proto_node *node = tree->first_child; node; node = node->next) { field_info *finfo = PNODE_FINFO(node); if (!finfo || !finfo->hfinfo) { @@ -1279,13 +1281,17 @@ static void wg_collect_protocol_hierarchy(proto_tree *tree, } if (finfo->hfinfo->type == FT_PROTOCOL && finfo->hfinfo->abbrev) { + string protocol_filter(finfo->hfinfo->abbrev); ProtocolNode *protocol = wg_find_or_create_protocol_node( nodes, - string(finfo->hfinfo->abbrev), + protocol_filter, finfo->hfinfo->name ? string(finfo->hfinfo->name) : ""); - protocol->frames++; - protocol->bytes += frame_bytes; + // Count a protocol at most once per frame within the current hierarchy level. + if (counted_protocols.insert(protocol_filter).second) { + protocol->frames++; + protocol->bytes += frame_bytes; + } if (((proto_tree *)node)->first_child) { wg_collect_protocol_hierarchy((proto_tree *)node, &protocol->children, frame_bytes); diff --git a/src/index.ts b/src/index.ts index 296d227..2adb6ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -268,9 +268,13 @@ export class Wiregasm { return fields; } - const regex = new RegExp(query); + // Use safe substring matching instead of compiling user input as RegExp. + // Accept a leading "(?i)" marker for compatibility with Wireshark-style queries. + const normalizedQuery = query.replace(/^\(\?i\)/i, "").toLowerCase(); return fields.filter( - (field) => regex.test(field.field) || regex.test(field.name) + (field) => + field.field.toLowerCase().includes(normalizedQuery) || + field.name.toLowerCase().includes(normalizedQuery) ); } From 97e1c913cedf6413427bf0b1bb6adc9235257288 Mon Sep 17 00:00:00 2001 From: dipesh Date: Thu, 14 May 2026 12:38:02 -0400 Subject: [PATCH 4/4] fix: update zlib wrap hash and add fallback URL for glib build Upstream zlib.net re-published the 1.2.11 tarball with a different hash, breaking the glib subproject download. Update the expected source_hash and add a source_fallback_url pointing to the GitHub release mirror. --- lib/Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Makefile b/lib/Makefile index 7259b30..1bcc24e 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -243,6 +243,8 @@ glib: glib-$(GLIB_MINOR_VERSION).tar.xz .sum-glib $(CLEANUP) tar xJfo $< $(APPLY) $(PATCHES)/glib/emscripten.2.75.0.patch + sed -i 's/source_hash = c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1/source_hash = 03045dcf24b343324e1da0290722725628a403defb87d3bec1b6e2e5f6326ee2/' $(EXTRACT_DIR)/subprojects/zlib.wrap + sed -i '/^source_hash/a source_fallback_url = https://github.com/madler/zlib/releases/download/v1.2.11/zlib-1.2.11.tar.gz' $(EXTRACT_DIR)/subprojects/zlib.wrap $(MOVE) .glib: glib .pcre .ffi crossfile.meson