Skip to content

Commit 76376db

Browse files
committed
click on rule group or category and it will prepopulate on the top
1 parent fc9c57b commit 76376db

1 file changed

Lines changed: 135 additions & 46 deletions

File tree

gui/rule_editor.py

Lines changed: 135 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
QHBoxLayout,
55
QLabel,
66
QComboBox,
7-
QLineEdit,
87
QPushButton,
98
QCheckBox,
9+
QCompleter,
1010
QTreeWidget,
1111
QTreeWidgetItem,
1212
QHeaderView,
@@ -23,6 +23,7 @@ def __init__(self, config: SysmonConfig) -> None:
2323
self.config = config
2424
self.current_event_id: int | None = None
2525
self.current_event_name: str = ""
26+
self.tree_meta_role = int(Qt.ItemDataRole.UserRole) + 1
2627

2728
self.layout = QVBoxLayout()
2829
self.setLayout(self.layout)
@@ -36,20 +37,37 @@ def __init__(self, config: SysmonConfig) -> None:
3637
self.group_box.setEditable(True)
3738
self.group_box.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
3839
self.group_box.setPlaceholderText("Select or type rule group (optional)")
40+
self.group_completer = QCompleter(self.group_box.model(), self.group_box)
41+
self.group_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
42+
self.group_completer.setFilterMode(Qt.MatchFlag.MatchContains)
43+
self.group_completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
44+
self.group_box.setCompleter(self.group_completer)
3945

4046
self.group_relation = QComboBox()
4147
self.group_relation.addItems(["or", "and"])
4248

4349
self.field_box = QComboBox()
50+
self.field_box.setEditable(True)
51+
self.field_box.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
52+
self.field_box.setPlaceholderText("Select or type category...")
53+
self.field_completer = QCompleter(self.field_box.model(), self.field_box)
54+
self.field_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
55+
self.field_completer.setFilterMode(Qt.MatchFlag.MatchContains)
56+
self.field_completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
57+
self.field_box.setCompleter(self.field_completer)
4458

4559
self.condition_box = QComboBox()
4660
self.condition_box.addItems(["is", "contains", "begin with", "end with"])
4761

4862
self.value_preset_box = QComboBox()
49-
self.value_preset_box.setEditable(False)
50-
51-
self.value_input = QLineEdit()
52-
self.value_input.setPlaceholderText("Enter custom value...")
63+
self.value_preset_box.setEditable(True)
64+
self.value_preset_box.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
65+
self.value_preset_box.setPlaceholderText("Select or type value...")
66+
self.value_completer = QCompleter(self.value_preset_box.model(), self.value_preset_box)
67+
self.value_completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
68+
self.value_completer.setFilterMode(Qt.MatchFlag.MatchContains)
69+
self.value_completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
70+
self.value_preset_box.setCompleter(self.value_completer)
5371

5472
self.add_button = QPushButton("Add Rule")
5573
self.remove_button = QPushButton("Remove Selected Rule")
@@ -73,7 +91,6 @@ def __init__(self, config: SysmonConfig) -> None:
7391

7492
self.rule_row_2 = QHBoxLayout()
7593
self.rule_row_2.addWidget(self.value_preset_box)
76-
self.rule_row_2.addWidget(self.value_input)
7794

7895
self.layout.addWidget(self.title)
7996
self.layout.addLayout(self.group_row)
@@ -88,62 +105,62 @@ def __init__(self, config: SysmonConfig) -> None:
88105
self.add_button.clicked.connect(self.add_rule)
89106
self.remove_button.clicked.connect(self.remove_selected_rule)
90107
self.field_box.currentTextChanged.connect(self.load_value_presets_for_field)
91-
self.group_box.currentIndexChanged.connect(self.on_group_selected)
92108
self.new_rules_only_toggle.stateChanged.connect(self.refresh_rules)
109+
self.rule_tree.itemClicked.connect(self.on_rule_tree_item_clicked)
93110

94111
def set_event(self, event_id: int, event_name: str) -> None:
112+
self._set_active_event(event_id, event_name)
113+
self.refresh_rules()
114+
115+
def _set_active_event(self, event_id: int, event_name: str) -> None:
95116
self.current_event_id = event_id
96117
self.current_event_name = event_name
97118
self.title.setText(f"{event_id} - {event_name}")
119+
self.group_box.setEditText("")
98120
self.load_fields_for_event()
99121
self.refresh_group_options()
100-
self.refresh_rules()
101122

102123
def refresh_group_options(self) -> None:
124+
typed_text = self.group_box.currentText()
103125
self.group_box.blockSignals(True)
104126
self.group_box.clear()
105127
self.group_box.addItem("")
106-
self.group_box.setItemData(0, None, Qt.ItemDataRole.UserRole)
107128

108129
if self.current_event_id is None:
130+
self.group_box.setEditText(typed_text)
109131
self.group_box.blockSignals(False)
110132
return
111133

112134
event_config = self.config.events.get(self.current_event_id)
113135
if event_config is None:
136+
self.group_box.setEditText(typed_text)
114137
self.group_box.blockSignals(False)
115138
return
116139

140+
group_labels: list[str] = []
117141
seen_group_ids: set[str] = set()
118142
for rule in event_config.rules:
119143
if not rule.group_id or rule.group_id in seen_group_ids:
120144
continue
121145
seen_group_ids.add(rule.group_id)
122146

123-
group_name = rule.group_name or "Imported Rule"
124-
group_relation = rule.group_relation or "or"
125-
label = f"{group_name} ({group_relation})"
126-
self.group_box.addItem(label)
127-
self.group_box.setItemData(
128-
self.group_box.count() - 1,
129-
{
130-
"group_id": rule.group_id,
131-
"group_name": rule.group_name,
132-
"group_relation": group_relation,
133-
},
134-
Qt.ItemDataRole.UserRole,
135-
)
147+
group_name = (rule.group_name or "").strip()
148+
if group_name:
149+
group_relation = (rule.group_relation or "or").strip()
150+
group_labels.append(f"{group_name} ({group_relation})")
136151

152+
self.group_box.addItems(group_labels)
153+
self.group_box.setEditText(typed_text)
137154
self.group_box.blockSignals(False)
138155

139-
def on_group_selected(self, *_args) -> None:
140-
selected_data = self.group_box.currentData(Qt.ItemDataRole.UserRole)
141-
if not isinstance(selected_data, dict):
142-
return
143-
144-
selected_relation = selected_data.get("group_relation")
145-
if selected_relation in ("or", "and"):
146-
self.group_relation.setCurrentText(selected_relation)
156+
def _split_group_text(self, raw_text: str) -> tuple[str, str | None]:
157+
text = raw_text.strip()
158+
if text.endswith(")") and " (" in text:
159+
name_part, relation_part = text.rsplit(" (", 1)
160+
relation = relation_part[:-1].strip().lower()
161+
if relation in ("or", "and"):
162+
return name_part.strip(), relation
163+
return text, None
147164

148165
def load_fields_for_event(self) -> None:
149166
from data.sysmon_fields import SYS_MON_FIELDS
@@ -167,12 +184,14 @@ def load_fields_for_event(self) -> None:
167184
def load_value_presets_for_field(self, field_name: str) -> None:
168185
from data.sysmon_value_presets import SYS_MON_VALUE_PRESETS
169186

187+
typed_value = self.value_preset_box.currentText()
170188
self.value_preset_box.clear()
171189
self.value_preset_box.addItem("")
172190

173191
presets = SYS_MON_VALUE_PRESETS.get(field_name, [])
174192
if presets:
175193
self.value_preset_box.addItems(presets)
194+
self.value_preset_box.setCurrentText(typed_value)
176195

177196
def refresh_rules(self) -> None:
178197
self.rule_tree.clear()
@@ -212,6 +231,11 @@ def refresh_rules(self) -> None:
212231
)
213232
event_item.setTextAlignment(1, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
214233
event_item.setForeground(1, QColor("#90ee90")) # light green counts
234+
event_item.setData(
235+
0,
236+
self.tree_meta_role,
237+
{"kind": "event", "event_id": event_id},
238+
)
215239
self.rule_tree.addTopLevelItem(event_item)
216240

217241
grouped_parents: dict[str, QTreeWidgetItem] = {}
@@ -226,11 +250,26 @@ def refresh_rules(self) -> None:
226250
grouped_parents[rule.group_id] = QTreeWidgetItem(
227251
[f"Rule: {group_name} ({group_relation})", ""]
228252
)
253+
grouped_parents[rule.group_id].setData(
254+
0,
255+
self.tree_meta_role,
256+
{
257+
"kind": "group",
258+
"event_id": event_id,
259+
"group_name": group_name,
260+
"group_relation": group_relation,
261+
},
262+
)
229263
event_item.addChild(grouped_parents[rule.group_id])
230264
parent_item = grouped_parents[rule.group_id]
231265
else:
232266
if ungrouped_parent is None:
233267
ungrouped_parent = QTreeWidgetItem(["Ungrouped Rules", ""])
268+
ungrouped_parent.setData(
269+
0,
270+
self.tree_meta_role,
271+
{"kind": "ungrouped", "event_id": event_id},
272+
)
234273
event_item.addChild(ungrouped_parent)
235274
parent_item = ungrouped_parent
236275

@@ -243,6 +282,11 @@ def refresh_rules(self) -> None:
243282
)
244283
item = QTreeWidgetItem([rule_text, ""])
245284
item.setData(0, Qt.ItemDataRole.UserRole, (event_id, rule_index))
285+
item.setData(
286+
0,
287+
self.tree_meta_role,
288+
{"kind": "rule", "event_id": event_id, "rule_index": rule_index},
289+
)
246290

247291
if not rule.imported:
248292
item.setBackground(0, QColor("#ffe6cc")) # light orange
@@ -257,14 +301,60 @@ def refresh_rules(self) -> None:
257301
f'Total Exclude <span style="color:#90ee90">({total_exclude})</span>'
258302
)
259303

304+
def on_rule_tree_item_clicked(self, item: QTreeWidgetItem, _column: int) -> None:
305+
meta = item.data(0, self.tree_meta_role)
306+
if not isinstance(meta, dict):
307+
return
308+
309+
kind = meta.get("kind")
310+
event_id = meta.get("event_id")
311+
if not isinstance(event_id, int):
312+
return
313+
314+
event_config = self.config.events.get(event_id)
315+
if event_config is None:
316+
return
317+
318+
self._set_active_event(event_id, event_config.event_name)
319+
320+
if kind == "group":
321+
group_name = (meta.get("group_name") or "").strip()
322+
group_relation = (meta.get("group_relation") or "or").strip().lower()
323+
if group_name:
324+
self.group_box.setEditText(f"{group_name} ({group_relation})")
325+
else:
326+
self.group_box.setEditText("")
327+
if group_relation in ("or", "and"):
328+
self.group_relation.setCurrentText(group_relation)
329+
return
330+
331+
if kind != "rule":
332+
return
333+
334+
rule_index = meta.get("rule_index")
335+
if not isinstance(rule_index, int) or not (0 <= rule_index < len(event_config.rules)):
336+
return
337+
338+
rule = event_config.rules[rule_index]
339+
self.rule_type.setCurrentText(rule.rule_type)
340+
self.field_box.setCurrentText(rule.field_name)
341+
self.condition_box.setCurrentText(rule.condition)
342+
343+
if rule.group_name:
344+
relation = rule.group_relation or "or"
345+
self.group_box.setEditText(f"{rule.group_name} ({relation})")
346+
if relation in ("or", "and"):
347+
self.group_relation.setCurrentText(relation)
348+
else:
349+
self.group_box.setEditText("")
350+
351+
self.value_preset_box.setCurrentText(rule.value)
352+
260353
def add_rule(self) -> None:
261354
if self.current_event_id is None:
262355
return
263356

264-
custom_value = self.value_input.text().strip()
265-
preset_value = self.value_preset_box.currentText().strip()
266-
267-
value = custom_value if custom_value else preset_value
357+
value = self.value_preset_box.currentText().strip()
268358
if not value:
269359
return
270360

@@ -273,30 +363,30 @@ def add_rule(self) -> None:
273363
self.current_event_name,
274364
)
275365

276-
selected_group_data = self.group_box.currentData(Qt.ItemDataRole.UserRole)
277366
selected_group_text = self.group_box.currentText().strip()
367+
selected_group_name, selected_group_relation = self._split_group_text(selected_group_text)
278368

279369
group_id: str | None = None
280370
group_name: str | None = None
281371
group_relation: str | None = None
282372

283-
if isinstance(selected_group_data, dict):
284-
group_id = selected_group_data.get("group_id")
285-
group_name = selected_group_data.get("group_name")
286-
group_relation = selected_group_data.get("group_relation")
287-
elif selected_group_text:
373+
if selected_group_name:
288374
existing_group = None
289375
for existing_rule in event_config.rules:
290376
if not existing_rule.group_id:
291377
continue
292-
if (existing_rule.group_name or "").strip().lower() == selected_group_text.lower():
378+
if (existing_rule.group_name or "").strip().lower() == selected_group_name.lower():
293379
existing_group = existing_rule
294380
break
295381

296382
if existing_group is not None:
297383
group_id = existing_group.group_id
298384
group_name = existing_group.group_name
299-
group_relation = existing_group.group_relation or self.group_relation.currentText()
385+
group_relation = (
386+
existing_group.group_relation
387+
or selected_group_relation
388+
or self.group_relation.currentText()
389+
)
300390
else:
301391
existing_ids = {
302392
existing_rule.group_id
@@ -309,8 +399,8 @@ def add_rule(self) -> None:
309399
next_index += 1
310400
group_id = f"user-group-{next_index}"
311401

312-
group_name = selected_group_text
313-
group_relation = self.group_relation.currentText()
402+
group_name = selected_group_name
403+
group_relation = selected_group_relation or self.group_relation.currentText()
314404

315405
event_config.rules.append(
316406
RuleFilter(
@@ -324,8 +414,7 @@ def add_rule(self) -> None:
324414
)
325415
)
326416

327-
self.value_input.clear()
328-
self.value_preset_box.setCurrentIndex(0)
417+
self.value_preset_box.setEditText("")
329418
self.refresh_group_options()
330419
self.refresh_rules()
331420

0 commit comments

Comments
 (0)