Skip to content

Commit aaed5dd

Browse files
committed
add sorting by rulename
1 parent 49a38d4 commit aaed5dd

5 files changed

Lines changed: 172 additions & 52 deletions

File tree

data/sysmon_events.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
SYS_MON_EVENTS: dict[int, str] = {
22
1: "Process Create",
3-
2: "File Creation Time Changed",
4-
3: "Network Connection",
5-
4: "Sysmon Service State Changed",
6-
5: "Process Terminated",
7-
6: "Driver Loaded",
8-
7: "Image Loaded",
3+
2: "File Create Time",
4+
3: "Network Connect",
5+
4: "Sysmon State Change",
6+
5: "Process Terminate",
7+
6: "Driver Load",
8+
7: "Image Load",
99
8: "CreateRemoteThread",
1010
9: "RawAccessRead",
1111
10: "Process Access",
1212
11: "File Create",
13-
12: "Registry Object Added or Deleted",
14-
13: "Registry Value Set",
15-
14: "Registry Key or Value Renamed",
13+
12: "Registry Event",
14+
13: "Registry Event",
15+
14: "Registry Event",
1616
15: "File Create Stream Hash",
1717
16: "Sysmon Config State Changed",
18-
17: "Pipe Created",
19-
18: "Pipe Connected",
20-
19: "WMI Event Filter",
21-
20: "WMI Event Consumer",
22-
21: "WMI Event Consumer To Filter",
18+
17: "Pipe Event",
19+
18: "Pipe Event",
20+
19: "Wmi Event",
21+
20: "Wmi Event",
22+
21: "Wmi Event",
2323
22: "DNS Query",
2424
23: "File Delete",
2525
24: "Clipboard Change",
@@ -31,17 +31,20 @@
3131
30: "File Blocked",
3232
}
3333

34+
def _normalize(tag: str) -> str:
35+
return tag.replace(" ", "").lower()
36+
3437

3538
def get_event_xml_tag(event_id: int) -> str:
3639
name = SYS_MON_EVENTS.get(event_id, f"Event{event_id}")
3740
return name.replace(" ", "")
3841

3942

4043
def get_event_id_from_xml_tag(xml_tag: str) -> int | None:
41-
normalized = xml_tag.replace(" ", "").lower()
44+
normalized = _normalize(xml_tag)
4245

4346
for event_id in SYS_MON_EVENTS:
44-
if get_event_xml_tag(event_id).lower() == normalized:
47+
if _normalize(get_event_xml_tag(event_id)) == normalized:
4548
return event_id
4649

47-
return None
50+
return None

exporters/xml_exporter.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,46 @@ def export_config(config: SysmonConfig, output_path: str) -> None:
4646
attrib={"onmatch": rule_type},
4747
)
4848

49+
emitted_groups: set[str] = set()
50+
grouped_rule_members: dict[str, list] = {}
51+
52+
for rule in rules:
53+
if rule.group_id:
54+
grouped_rule_members.setdefault(rule.group_id, []).append(rule)
55+
4956
for rule in rules:
50-
field_element = ET.SubElement(
51-
event_element,
52-
rule.field_name,
53-
attrib={"condition": rule.condition},
54-
)
55-
field_element.text = rule.value
57+
if not rule.group_id:
58+
field_element = ET.SubElement(
59+
event_element,
60+
rule.field_name,
61+
attrib={"condition": rule.condition},
62+
)
63+
field_element.text = rule.value
64+
continue
65+
66+
if rule.group_id in emitted_groups:
67+
continue
68+
69+
group_rules = grouped_rule_members.get(rule.group_id, [rule])
70+
group_attrs: dict[str, str] = {}
71+
if rule.group_name:
72+
group_attrs["name"] = rule.group_name
73+
if rule.group_relation:
74+
group_attrs["groupRelation"] = rule.group_relation
75+
76+
rule_group_element = ET.SubElement(event_element, "Rule", attrib=group_attrs)
77+
78+
for grouped_rule in group_rules:
79+
field_element = ET.SubElement(
80+
rule_group_element,
81+
grouped_rule.field_name,
82+
attrib={"condition": grouped_rule.condition},
83+
)
84+
field_element.text = grouped_rule.value
85+
86+
emitted_groups.add(rule.group_id)
5687

5788
xml_output: str = prettify_xml(root)
5889

5990
output_file = Path(output_path)
60-
output_file.write_text(xml_output, encoding="utf-8")
91+
output_file.write_text(xml_output, encoding="utf-8")

gui/rule_editor.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
QComboBox,
77
QLineEdit,
88
QPushButton,
9-
QListWidget,
10-
QListWidgetItem,
9+
QTreeWidget,
10+
QTreeWidgetItem,
1111
)
1212
from models.sysmon_config import RuleFilter, SysmonConfig
1313
from PySide6.QtGui import QColor
14+
from PySide6.QtCore import Qt
1415

1516

1617
class RuleEditor(QWidget):
@@ -20,7 +21,6 @@ def __init__(self, config: SysmonConfig) -> None:
2021
self.config = config
2122
self.current_event_id: int | None = None
2223
self.current_event_name: str = ""
23-
self.displayed_rules: list[tuple[int, int]] = []
2424

2525
self.layout = QVBoxLayout()
2626
self.setLayout(self.layout)
@@ -44,7 +44,8 @@ def __init__(self, config: SysmonConfig) -> None:
4444
self.add_button = QPushButton("Add Rule")
4545
self.remove_button = QPushButton("Remove Selected Rule")
4646

47-
self.rule_list = QListWidget()
47+
self.rule_tree = QTreeWidget()
48+
self.rule_tree.setHeaderHidden(True)
4849

4950
self.rule_row_1 = QHBoxLayout()
5051
self.rule_row_1.addWidget(self.rule_type)
@@ -60,7 +61,7 @@ def __init__(self, config: SysmonConfig) -> None:
6061
self.layout.addLayout(self.rule_row_2)
6162
self.layout.addWidget(self.add_button)
6263
self.layout.addWidget(self.remove_button)
63-
self.layout.addWidget(self.rule_list)
64+
self.layout.addWidget(self.rule_tree)
6465

6566
self.add_button.clicked.connect(self.add_rule)
6667
self.remove_button.clicked.connect(self.remove_selected_rule)
@@ -103,25 +104,54 @@ def load_value_presets_for_field(self, field_name: str) -> None:
103104
self.value_preset_box.addItems(presets)
104105

105106
def refresh_rules(self) -> None:
106-
self.rule_list.clear()
107-
self.displayed_rules.clear()
107+
self.rule_tree.clear()
108108

109109
for event_id, event_config in sorted(self.config.events.items()):
110+
if not event_config.rules:
111+
continue
112+
113+
event_item = QTreeWidgetItem(
114+
[f"{event_id} - {event_config.event_name}"]
115+
)
116+
self.rule_tree.addTopLevelItem(event_item)
117+
118+
grouped_parents: dict[str, QTreeWidgetItem] = {}
119+
ungrouped_parent: QTreeWidgetItem | None = None
120+
110121
for rule_index, rule in enumerate(event_config.rules):
122+
if rule.group_id:
123+
if rule.group_id not in grouped_parents:
124+
group_name = rule.group_name or "Imported Rule"
125+
group_relation = rule.group_relation or "or"
126+
grouped_parents[rule.group_id] = QTreeWidgetItem(
127+
[f"Rule: {group_name} ({group_relation})"]
128+
)
129+
event_item.addChild(grouped_parents[rule.group_id])
130+
parent_item = grouped_parents[rule.group_id]
131+
else:
132+
if ungrouped_parent is None:
133+
ungrouped_parent = QTreeWidgetItem(["Ungrouped Rules"])
134+
event_item.addChild(ungrouped_parent)
135+
parent_item = ungrouped_parent
136+
111137
rule_text = (
112138
f"{event_id} | "
113139
f"{rule.rule_type} | "
114140
f"{rule.field_name} | "
115141
f"{rule.condition} | "
116142
f"{rule.value}"
117143
)
118-
item = QListWidgetItem(rule_text)
144+
item = QTreeWidgetItem([rule_text])
145+
item.setData(0, Qt.ItemDataRole.UserRole, (event_id, rule_index))
119146

120147
if not rule.imported:
121-
item.setBackground(QColor("#ffe6cc")) # light orange
148+
item.setBackground(0, QColor("#ffe6cc")) # light orange
149+
150+
parent_item.addChild(item)
122151

123-
self.rule_list.addItem(item)
124-
self.displayed_rules.append((event_id, rule_index))
152+
event_item.setExpanded(True)
153+
154+
self.rule_tree.expandAll()
125155

126156
def add_rule(self) -> None:
127157
if self.current_event_id is None:
@@ -156,14 +186,15 @@ def remove_selected_rule(self) -> None:
156186
if self.current_event_id is None:
157187
return
158188

159-
selected_row = self.rule_list.currentRow()
160-
if selected_row < 0:
189+
selected_item = self.rule_tree.currentItem()
190+
if selected_item is None:
161191
return
162192

163-
if selected_row >= len(self.displayed_rules):
193+
rule_key = selected_item.data(0, Qt.ItemDataRole.UserRole)
194+
if not isinstance(rule_key, tuple) or len(rule_key) != 2:
164195
return
165196

166-
event_id, rule_index = self.displayed_rules[selected_row]
197+
event_id, rule_index = rule_key
167198
event_config = self.config.events.get(event_id)
168199

169200
if event_config is None:
@@ -172,4 +203,4 @@ def remove_selected_rule(self) -> None:
172203
if 0 <= rule_index < len(event_config.rules):
173204
del event_config.rules[rule_index]
174205

175-
self.refresh_rules()
206+
self.refresh_rules()

importers/xml_importer.py

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,57 @@
44
from models.sysmon_config import RuleFilter, SysmonConfig
55

66

7+
def strip_namespace(tag: str) -> str:
8+
if "}" in tag:
9+
return tag.split("}", 1)[1]
10+
return tag
11+
12+
713
def extract_rules_from_node(
814
node: ET.Element,
915
event_config,
1016
rule_type: str,
17+
group_id: str | None = None,
18+
group_relation: str | None = None,
19+
group_name: str | None = None,
20+
group_counter: list[int] | None = None,
1121
) -> None:
1222
for child in node:
13-
if child.tag == "Rule":
14-
extract_rules_from_node(child, event_config, rule_type)
23+
child_tag = strip_namespace(child.tag)
24+
25+
if child_tag == "Rule":
26+
next_group_id = group_id
27+
if next_group_id is None and group_counter is not None:
28+
next_group_id = f"imported-group-{group_counter[0]}"
29+
group_counter[0] += 1
30+
31+
next_group_relation = child.attrib.get("groupRelation", group_relation)
32+
next_group_name = child.attrib.get("name", group_name)
33+
34+
extract_rules_from_node(
35+
child,
36+
event_config,
37+
rule_type,
38+
next_group_id,
39+
next_group_relation,
40+
next_group_name,
41+
group_counter,
42+
)
1543
continue
1644

1745
if len(child) > 0:
18-
extract_rules_from_node(child, event_config, rule_type)
46+
extract_rules_from_node(
47+
child,
48+
event_config,
49+
rule_type,
50+
group_id,
51+
group_relation,
52+
group_name,
53+
group_counter,
54+
)
1955
continue
2056

21-
field_name = child.tag
57+
field_name = child_tag
2258
condition = child.attrib.get("condition", "is")
2359
value = (child.text or "").strip()
2460

@@ -32,6 +68,9 @@ def extract_rules_from_node(
3268
condition=condition,
3369
value=value,
3470
imported=True,
71+
group_id=group_id,
72+
group_relation=group_relation,
73+
group_name=group_name,
3574
)
3675
)
3776

@@ -43,15 +82,23 @@ def import_config(input_path: str) -> SysmonConfig:
4382
config = SysmonConfig()
4483

4584
event_filtering = root.find("EventFiltering")
85+
if event_filtering is None:
86+
for child in root:
87+
if strip_namespace(child.tag) == "EventFiltering":
88+
event_filtering = child
89+
break
90+
4691
if event_filtering is None:
4792
return config
4893

49-
for rule_group in event_filtering:
50-
if rule_group.tag != "RuleGroup":
51-
continue
94+
group_counter = [1]
5295

53-
for event_element in rule_group:
54-
event_tag = event_element.tag
96+
for child in event_filtering:
97+
child_tag = strip_namespace(child.tag)
98+
event_elements = child if child_tag == "RuleGroup" else [child]
99+
100+
for event_element in event_elements:
101+
event_tag = strip_namespace(event_element.tag)
55102
event_id = get_event_id_from_xml_tag(event_tag)
56103

57104
if event_id is None:
@@ -61,6 +108,11 @@ def import_config(input_path: str) -> SysmonConfig:
61108
rule_type = event_element.attrib.get("onmatch", "include")
62109

63110
event_config = config.get_or_create_event(event_id, event_name)
64-
extract_rules_from_node(event_element, event_config, rule_type)
111+
extract_rules_from_node(
112+
event_element,
113+
event_config,
114+
rule_type,
115+
group_counter=group_counter,
116+
)
65117

66-
return config
118+
return config

models/sysmon_config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class RuleFilter:
99
condition: str
1010
value: str
1111
imported: bool = False
12+
group_id: str | None = None
13+
group_relation: str | None = None
14+
group_name: str | None = None
1215

1316

1417
@dataclass
@@ -28,4 +31,4 @@ def get_or_create_event(self, event_id: int, event_name: str) -> EventConfig:
2831
event_id=event_id,
2932
event_name=event_name,
3033
)
31-
return self.events[event_id]
34+
return self.events[event_id]

0 commit comments

Comments
 (0)