Skip to content

Commit 2362f5f

Browse files
committed
Add filter autocomplete for matchy.level field
Change matchy.level from FT_STRING to FT_UINT8 with value_string, which enables Wireshark's filter autocomplete to suggest level names. Users can now filter by: - String: matchy.level == "High" - Numeric: matchy.level == 3 - Comparison: matchy.level >= 2 (Medium or higher) The packet details pane shows the text representation (High, Critical, etc.) while the underlying value is numeric, enabling rich filter expressions. Also adds integration tests for numeric and comparison filters.
1 parent 01f58b6 commit 2362f5f

File tree

5 files changed

+98
-22
lines changed

5 files changed

+98
-22
lines changed

src/lib.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ unsafe extern "C" fn proto_register_matchy() {
126126
register_toolbar();
127127
}
128128

129+
/// Value strings for threat level field (enables filter autocomplete)
130+
/// Must be null-terminated array
131+
static THREAT_LEVEL_VALS: [wireshark_ffi::value_string; 6] = [
132+
wireshark_ffi::value_string { value: 4, strptr: c"Critical".as_ptr() },
133+
wireshark_ffi::value_string { value: 3, strptr: c"High".as_ptr() },
134+
wireshark_ffi::value_string { value: 2, strptr: c"Medium".as_ptr() },
135+
wireshark_ffi::value_string { value: 1, strptr: c"Low".as_ptr() },
136+
wireshark_ffi::value_string { value: 0, strptr: c"Unknown".as_ptr() },
137+
// Null terminator
138+
wireshark_ffi::value_string { value: 0, strptr: std::ptr::null() },
139+
];
140+
129141
/// Static storage for header field registration info
130142
/// These MUST be static because Wireshark keeps pointers to them
131143
static mut HF_ARRAY: [wireshark_ffi::hf_register_info; 9] = {
@@ -153,8 +165,10 @@ static mut HF_ARRAY: [wireshark_ffi::hf_register_info; 9] = {
153165
hfinfo: header_field_info {
154166
name: c"Threat Level".as_ptr(),
155167
abbrev: c"matchy.level".as_ptr(),
156-
type_: FT_STRING,
157-
display: BASE_NONE,
168+
type_: FT_UINT8,
169+
display: BASE_DEC,
170+
// Note: strings pointer is set at runtime in proto_register_matchy
171+
// because we can't reference THREAT_LEVEL_VALS in const context
158172
strings: std::ptr::null(),
159173
bitmask: 0,
160174
blurb: c"Severity level of the threat".as_ptr(),
@@ -355,6 +369,10 @@ unsafe fn register_fields() {
355369
HF_ARRAY[7].p_id = std::ptr::addr_of_mut!(HF_THREAT_TLP);
356370
HF_ARRAY[8].p_id = std::ptr::addr_of_mut!(HF_THREAT_LAST_SEEN);
357371

372+
// Set the value_string pointer for threat level field (enables filter autocomplete)
373+
// This must be done at runtime because we can't reference static data in const context
374+
HF_ARRAY[1].hfinfo.strings = THREAT_LEVEL_VALS.as_ptr() as *const libc::c_void;
375+
358376
proto_register_field_array(PROTO_MATCHY, HF_ARRAY.as_mut_ptr(), HF_ARRAY.len() as c_int);
359377

360378
// Register subtree

src/postdissector.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,9 +495,8 @@ unsafe fn add_threat_to_tree(
495495
return;
496496
}
497497

498-
// Add threat details (Wireshark copies string values for FT_STRINGZ)
499-
let level_str = to_c_string(threat.level.display_str());
500-
proto_tree_add_string(subtree, hf.level, tvb, 0, 0, level_str.as_ptr());
498+
// Add threat level as uint8 (value_string provides display text and autocomplete)
499+
proto_tree_add_uint(subtree, hf.level, tvb, 0, 0, threat.level.as_u8() as libc::c_uint);
501500

502501
let category_str = to_c_string(&threat.category);
503502
proto_tree_add_string(subtree, hf.category, tvb, 0, 0, category_str.as_ptr());

src/threats.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
//! Handles IP/domain lookups and visual threat indicators.
44
55
/// Threat level representation
6+
/// Integer values are used for Wireshark field encoding (enables autocomplete)
67
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8+
#[repr(u8)]
79
pub enum ThreatLevel {
8-
Critical,
9-
High,
10-
Medium,
11-
Low,
12-
Unknown,
10+
Critical = 4,
11+
High = 3,
12+
Medium = 2,
13+
Low = 1,
14+
Unknown = 0,
1315
}
1416

1517
impl ThreatLevel {
@@ -37,15 +39,9 @@ impl ThreatLevel {
3739
}
3840
}
3941

40-
/// Get display string
41-
pub fn display_str(&self) -> &'static str {
42-
match self {
43-
ThreatLevel::Critical => "Critical",
44-
ThreatLevel::High => "High",
45-
ThreatLevel::Medium => "Medium",
46-
ThreatLevel::Low => "Low",
47-
ThreatLevel::Unknown => "Unknown",
48-
}
42+
/// Get numeric value for Wireshark field encoding
43+
pub fn as_u8(&self) -> u8 {
44+
*self as u8
4945
}
5046
}
5147

src/wireshark_ffi.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,22 @@ pub const FT_UINT32: c_int = 7;
102102
pub const FT_STRING: c_int = 26;
103103
pub const FT_STRINGZ: c_int = 27;
104104

105+
// ============================================================================
106+
// Value Strings (for enum-like field autocomplete)
107+
// ============================================================================
108+
109+
/// Value string entry - maps integer value to display string
110+
/// Used for Wireshark filter autocomplete
111+
#[repr(C)]
112+
pub struct value_string {
113+
pub value: u32,
114+
pub strptr: *const c_char,
115+
}
116+
117+
// Safety: value_string contains only a u32 and a pointer to static string data.
118+
// The pointer is only read by Wireshark, never written, so this is safe to share.
119+
unsafe impl Sync for value_string {}
120+
105121
// ============================================================================
106122
// Field Display
107123
// ============================================================================

tests/integration.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ fn test_plugin_integration() {
291291
assert_eq!(results.len(), 4, "Expected 4 packets in test pcap");
292292

293293
// Frame 1: dst=192.168.1.1 (exact match) -> high threat, malware
294+
// Note: threat level is now FT_UINT8 with value_string, so raw output shows "3" for High
294295
let pkt1 = &results[0];
295296
assert_eq!(pkt1.frame_number, 1);
296297
assert_eq!(pkt1.dst_ip, "192.168.1.1");
@@ -300,7 +301,7 @@ fn test_plugin_integration() {
300301
);
301302
assert_eq!(
302303
pkt1.threat_level.as_deref(),
303-
Some("High"),
304+
Some("3"), // High = 3
304305
"Frame 1 threat level"
305306
);
306307
assert_eq!(
@@ -319,7 +320,7 @@ fn test_plugin_integration() {
319320
);
320321
assert_eq!(
321322
pkt2.threat_level.as_deref(),
322-
Some("Medium"),
323+
Some("2"), // Medium = 2
323324
"Frame 2 threat level"
324325
);
325326
assert_eq!(
@@ -338,7 +339,7 @@ fn test_plugin_integration() {
338339
);
339340
assert_eq!(
340341
pkt3.threat_level.as_deref(),
341-
Some("High"),
342+
Some("3"), // High = 3
342343
"Frame 3 threat level"
343344
);
344345
assert_eq!(
@@ -500,5 +501,51 @@ fn test_display_filter() {
500501
"Two-pass filter 'matchy.level == \"High\"' should match 2 frames, got: {:?}", frames
501502
);
502503

504+
// Test 6: Numeric filter for threat level (value_string allows both)
505+
// High = 3 in the ThreatLevel enum
506+
let output = Command::new("tshark")
507+
.env("WIRESHARK_PLUGIN_DIR", &plugin_dir)
508+
.args([
509+
"-o", &db_pref,
510+
"-r", pcap_path.to_str().unwrap(),
511+
"-Y", "matchy.level == 3",
512+
"-T", "fields",
513+
"-e", "frame.number",
514+
])
515+
.output()
516+
.expect("Failed to run tshark with numeric filter");
517+
518+
assert!(output.status.success(), "tshark numeric filter command failed");
519+
let stdout = String::from_utf8_lossy(&output.stdout);
520+
let frames: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
521+
522+
assert_eq!(
523+
frames.len(), 2,
524+
"Numeric filter 'matchy.level == 3' should match 2 frames (same as High), got: {:?}", frames
525+
);
526+
527+
// Test 7: Comparison operators (threat level >= Medium)
528+
// Medium = 2, so this should match frames with High (3) and Medium (2)
529+
let output = Command::new("tshark")
530+
.env("WIRESHARK_PLUGIN_DIR", &plugin_dir)
531+
.args([
532+
"-o", &db_pref,
533+
"-r", pcap_path.to_str().unwrap(),
534+
"-Y", "matchy.level >= 2",
535+
"-T", "fields",
536+
"-e", "frame.number",
537+
])
538+
.output()
539+
.expect("Failed to run tshark with comparison filter");
540+
541+
assert!(output.status.success(), "tshark comparison filter command failed");
542+
let stdout = String::from_utf8_lossy(&output.stdout);
543+
let frames: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
544+
545+
assert_eq!(
546+
frames.len(), 3,
547+
"Comparison filter 'matchy.level >= 2' should match 3 frames (High + Medium), got: {:?}", frames
548+
);
549+
503550
eprintln!("All display filter tests passed!");
504551
}

0 commit comments

Comments
 (0)