@@ -30,6 +30,14 @@ def __init__(self, config: SysmonConfig) -> None:
3030 self .rule_type = QComboBox ()
3131 self .rule_type .addItems (["include" , "exclude" ])
3232
33+ self .group_box = QComboBox ()
34+ self .group_box .setEditable (True )
35+ self .group_box .setInsertPolicy (QComboBox .InsertPolicy .NoInsert )
36+ self .group_box .setPlaceholderText ("Select or type rule group (optional)" )
37+
38+ self .group_relation = QComboBox ()
39+ self .group_relation .addItems (["or" , "and" ])
40+
3341 self .field_box = QComboBox ()
3442
3543 self .condition_box = QComboBox ()
@@ -52,11 +60,16 @@ def __init__(self, config: SysmonConfig) -> None:
5260 self .rule_row_1 .addWidget (self .field_box )
5361 self .rule_row_1 .addWidget (self .condition_box )
5462
63+ self .group_row = QHBoxLayout ()
64+ self .group_row .addWidget (self .group_box )
65+ self .group_row .addWidget (self .group_relation )
66+
5567 self .rule_row_2 = QHBoxLayout ()
5668 self .rule_row_2 .addWidget (self .value_preset_box )
5769 self .rule_row_2 .addWidget (self .value_input )
5870
5971 self .layout .addWidget (self .title )
72+ self .layout .addLayout (self .group_row )
6073 self .layout .addLayout (self .rule_row_1 )
6174 self .layout .addLayout (self .rule_row_2 )
6275 self .layout .addWidget (self .add_button )
@@ -66,14 +79,62 @@ def __init__(self, config: SysmonConfig) -> None:
6679 self .add_button .clicked .connect (self .add_rule )
6780 self .remove_button .clicked .connect (self .remove_selected_rule )
6881 self .field_box .currentTextChanged .connect (self .load_value_presets_for_field )
82+ self .group_box .currentIndexChanged .connect (self .on_group_selected )
6983
7084 def set_event (self , event_id : int , event_name : str ) -> None :
7185 self .current_event_id = event_id
7286 self .current_event_name = event_name
7387 self .title .setText (f"{ event_id } - { event_name } " )
7488 self .load_fields_for_event ()
89+ self .refresh_group_options ()
7590 self .refresh_rules ()
7691
92+ def refresh_group_options (self ) -> None :
93+ self .group_box .blockSignals (True )
94+ self .group_box .clear ()
95+ self .group_box .addItem ("" )
96+ self .group_box .setItemData (0 , None , Qt .ItemDataRole .UserRole )
97+
98+ if self .current_event_id is None :
99+ self .group_box .blockSignals (False )
100+ return
101+
102+ event_config = self .config .events .get (self .current_event_id )
103+ if event_config is None :
104+ self .group_box .blockSignals (False )
105+ return
106+
107+ seen_group_ids : set [str ] = set ()
108+ for rule in event_config .rules :
109+ if not rule .group_id or rule .group_id in seen_group_ids :
110+ continue
111+ seen_group_ids .add (rule .group_id )
112+
113+ group_name = rule .group_name or "Imported Rule"
114+ group_relation = rule .group_relation or "or"
115+ label = f"{ group_name } ({ group_relation } )"
116+ self .group_box .addItem (label )
117+ self .group_box .setItemData (
118+ self .group_box .count () - 1 ,
119+ {
120+ "group_id" : rule .group_id ,
121+ "group_name" : rule .group_name ,
122+ "group_relation" : group_relation ,
123+ },
124+ Qt .ItemDataRole .UserRole ,
125+ )
126+
127+ self .group_box .blockSignals (False )
128+
129+ def on_group_selected (self , * _args ) -> None :
130+ selected_data = self .group_box .currentData (Qt .ItemDataRole .UserRole )
131+ if not isinstance (selected_data , dict ):
132+ return
133+
134+ selected_relation = selected_data .get ("group_relation" )
135+ if selected_relation in ("or" , "and" ):
136+ self .group_relation .setCurrentText (selected_relation )
137+
77138 def load_fields_for_event (self ) -> None :
78139 from data .sysmon_fields import SYS_MON_FIELDS
79140
@@ -169,17 +230,60 @@ def add_rule(self) -> None:
169230 self .current_event_name ,
170231 )
171232
233+ selected_group_data = self .group_box .currentData (Qt .ItemDataRole .UserRole )
234+ selected_group_text = self .group_box .currentText ().strip ()
235+
236+ group_id : str | None = None
237+ group_name : str | None = None
238+ group_relation : str | None = None
239+
240+ if isinstance (selected_group_data , dict ):
241+ group_id = selected_group_data .get ("group_id" )
242+ group_name = selected_group_data .get ("group_name" )
243+ group_relation = selected_group_data .get ("group_relation" )
244+ elif selected_group_text :
245+ existing_group = None
246+ for existing_rule in event_config .rules :
247+ if not existing_rule .group_id :
248+ continue
249+ if (existing_rule .group_name or "" ).strip ().lower () == selected_group_text .lower ():
250+ existing_group = existing_rule
251+ break
252+
253+ if existing_group is not None :
254+ group_id = existing_group .group_id
255+ group_name = existing_group .group_name
256+ group_relation = existing_group .group_relation or self .group_relation .currentText ()
257+ else :
258+ existing_ids = {
259+ existing_rule .group_id
260+ for existing_rule in event_config .rules
261+ if existing_rule .group_id
262+ }
263+ next_index = 1
264+ group_id = f"user-group-{ next_index } "
265+ while group_id in existing_ids :
266+ next_index += 1
267+ group_id = f"user-group-{ next_index } "
268+
269+ group_name = selected_group_text
270+ group_relation = self .group_relation .currentText ()
271+
172272 event_config .rules .append (
173273 RuleFilter (
174274 rule_type = self .rule_type .currentText (),
175275 field_name = self .field_box .currentText (),
176276 condition = self .condition_box .currentText (),
177277 value = value ,
278+ group_id = group_id ,
279+ group_name = group_name ,
280+ group_relation = group_relation ,
178281 )
179282 )
180283
181284 self .value_input .clear ()
182285 self .value_preset_box .setCurrentIndex (0 )
286+ self .refresh_group_options ()
183287 self .refresh_rules ()
184288
185289 def remove_selected_rule (self ) -> None :
@@ -203,4 +307,5 @@ def remove_selected_rule(self) -> None:
203307 if 0 <= rule_index < len (event_config .rules ):
204308 del event_config .rules [rule_index ]
205309
310+ self .refresh_group_options ()
206311 self .refresh_rules ()
0 commit comments