Skip to content

Commit a7bffde

Browse files
nasbenchCopilot
andauthored
Add Correlation Support (#60)
* update related tests * rename folder * apply black formatting * Update pyproject.toml * Update sigma/validators/sigmahq/filename.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/test_correlation.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update test_correlation.py --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 815c4ac commit a7bffde

File tree

12 files changed

+663
-11
lines changed

12 files changed

+663
-11
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pySigma-validators-sigmahq"
3-
version = "0.12.2"
3+
version = "0.13.0"
44
description = "pySigma SigmaHQ validators"
55
authors = ["François Hubaut <frack113@users.noreply.github.com>"]
66
license = "LGPL-2.1-only"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from dataclasses import dataclass
2+
from typing import ClassVar, List
3+
4+
from sigma.correlations import SigmaCorrelationRule, SigmaCorrelationType
5+
from sigma.rule import SigmaRuleBase
6+
from sigma.validators.base import (
7+
SigmaRuleValidator,
8+
SigmaValidationIssue,
9+
SigmaValidationIssueSeverity,
10+
)
11+
12+
13+
@dataclass
14+
class SigmahqCorrelationRulesMinimumIssue(SigmaValidationIssue):
15+
description: ClassVar[str] = (
16+
"Correlation rule must reference at least 2 rules for temporal types"
17+
)
18+
severity: ClassVar[SigmaValidationIssueSeverity] = SigmaValidationIssueSeverity.HIGH
19+
20+
21+
class SigmahqCorrelationRulesMinimumValidator(SigmaRuleValidator):
22+
"""Checks if temporal correlation rules have at least 2 rules."""
23+
24+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
25+
if isinstance(rule, SigmaCorrelationRule):
26+
if rule.type in [SigmaCorrelationType.TEMPORAL, SigmaCorrelationType.TEMPORAL_ORDERED]:
27+
if len(rule.rules) < 2:
28+
return [SigmahqCorrelationRulesMinimumIssue([rule])]
29+
return []
30+
31+
32+
@dataclass
33+
class SigmahqCorrelationGroupByExistenceIssue(SigmaValidationIssue):
34+
description: ClassVar[str] = (
35+
"Correlation rule is missing the group-by field in correlation section"
36+
)
37+
severity: ClassVar[SigmaValidationIssueSeverity] = SigmaValidationIssueSeverity.HIGH
38+
39+
40+
class SigmahqCorrelationGroupByExistenceValidator(SigmaRuleValidator):
41+
"""Checks if a correlation rule has a group-by field for types that require it."""
42+
43+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
44+
if isinstance(rule, SigmaCorrelationRule):
45+
if rule.type in [
46+
SigmaCorrelationType.EVENT_COUNT,
47+
SigmaCorrelationType.VALUE_COUNT,
48+
SigmaCorrelationType.TEMPORAL,
49+
SigmaCorrelationType.TEMPORAL_ORDERED,
50+
]:
51+
if rule.group_by is None or len(rule.group_by) == 0:
52+
return [SigmahqCorrelationGroupByExistenceIssue([rule])]
53+
return []

sigma/validators/sigmahq/detection.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from sigma.rule import (
55
SigmaRule,
66
SigmaDetectionItem,
7+
SigmaRuleBase,
78
)
89

910
from sigma.validators.base import (
1011
SigmaValidationIssue,
11-
SigmaRuleValidator,
1212
SigmaValidationIssueSeverity,
1313
SigmaDetectionItemValidator,
1414
SigmaDetectionItem,
@@ -32,7 +32,11 @@ class SigmahqCategoryEventIdIssue(SigmaValidationIssue):
3232
class SigmahqCategoryEventIdValidator(SigmaDetectionItemValidator):
3333
"""Checks if a rule uses an EventID field with a windows category logsource that doesn't require it."""
3434

35-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
35+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
36+
# Only validate SigmaRule (detection rules), not correlation rules
37+
if not isinstance(rule, SigmaRule):
38+
return []
39+
3640
if (
3741
rule.logsource.product == "windows"
3842
and rule.logsource.category in config.windows_no_eventid
@@ -61,7 +65,11 @@ class SigmahqCategoryWindowsProviderNameIssue(SigmaValidationIssue):
6165
class SigmahqCategoryWindowsProviderNameValidator(SigmaDetectionItemValidator):
6266
"""Checks if a rule uses a Provider_Name field with a windows category logsource that doesn't require it."""
6367

64-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
68+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
69+
# Only validate SigmaRule (detection rules), not correlation rules
70+
if not isinstance(rule, SigmaRule):
71+
return []
72+
6573
if rule.logsource in config.windows_provider_name:
6674
self.list_provider = config.windows_provider_name[rule.logsource]
6775
return super().validate(rule)

sigma/validators/sigmahq/field.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import ClassVar, Dict, List, Tuple
44
import re
55

6-
from sigma.rule import SigmaRule, SigmaLogSource
6+
from sigma.rule import SigmaRule, SigmaLogSource, SigmaRuleBase
77
from sigma.types import SigmaString
88
from sigma.validators.base import (
99
SigmaValidationIssue,
@@ -57,7 +57,11 @@ class SigmahqFieldnameCastIssue(SigmaValidationIssue):
5757
class SigmahqFieldnameCastValidator(SigmaDetectionItemValidator):
5858
"""Check field name have a cast error."""
5959

60-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
60+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
61+
# Only validate SigmaRule (detection rules), not correlation rules
62+
if not isinstance(rule, SigmaRule):
63+
return []
64+
6165
core_logsource = SigmaLogSource(
6266
category=rule.logsource.category,
6367
product=rule.logsource.product,
@@ -95,7 +99,11 @@ class SigmahqInvalidFieldnameIssue(SigmaValidationIssue):
9599
class SigmahqInvalidFieldnameValidator(SigmaDetectionItemValidator):
96100
"""Check field name do not exist in the logsource."""
97101

98-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
102+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
103+
# Only validate SigmaRule (detection rules), not correlation rules
104+
if not isinstance(rule, SigmaRule):
105+
return []
106+
99107
core_logsource = SigmaLogSource(
100108
category=rule.logsource.category,
101109
product=rule.logsource.product,
@@ -288,7 +296,11 @@ class SigmahqRedundantFieldIssue(SigmaValidationIssue):
288296
class SigmahqRedundantFieldValidator(SigmaDetectionItemValidator):
289297
"""Check if a field name is already covered by the logsource."""
290298

291-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
299+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
300+
# Only validate SigmaRule (detection rules), not correlation rules
301+
if not isinstance(rule, SigmaRule):
302+
return []
303+
292304
core_logsource = SigmaLogSource(
293305
category=rule.logsource.category,
294306
product=rule.logsource.product,

sigma/validators/sigmahq/filename.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import ClassVar, Dict, List
44

55
from sigma.rule import SigmaRule, SigmaLogSource, SigmaRuleBase
6+
from sigma.correlations import SigmaCorrelationRule
67

78
from sigma.validators.base import (
89
SigmaRuleValidator,
@@ -22,6 +23,13 @@ class SigmahqFilenameConventionIssue(SigmaValidationIssue):
2223
filename: str
2324

2425

26+
@dataclass
27+
class SigmahqCorrelationFilenamePrefixIssue(SigmaValidationIssue):
28+
description: ClassVar[str] = "Correlation rule filename must start with 'correlation_'"
29+
severity: ClassVar[SigmaValidationIssueSeverity] = SigmaValidationIssueSeverity.MEDIUM
30+
filename: str
31+
32+
2533
class SigmahqFilenameConventionValidator(SigmaRuleValidator):
2634
"""Check a rule filename against SigmaHQ filename convention."""
2735

@@ -34,6 +42,24 @@ def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
3442
return []
3543

3644

45+
class SigmahqCorrelationFilenamePrefixValidator(SigmaRuleValidator):
46+
"""Check that correlation rule filenames start with 'correlation_'."""
47+
48+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
49+
# Only validate correlation rules
50+
if not isinstance(rule, SigmaCorrelationRule):
51+
return []
52+
53+
if rule.source is not None:
54+
filename = rule.source.path.name
55+
56+
# All correlation files (pure or combined) must start with 'correlation_'
57+
if not filename.startswith("correlation_"):
58+
return [SigmahqCorrelationFilenamePrefixIssue([rule], filename)]
59+
60+
return []
61+
62+
3763
@dataclass
3864
class SigmahqFilenamePrefixIssue(SigmaValidationIssue):
3965
description: ClassVar[str] = "The rule filename prefix doesn't match the SigmaHQ convention"
@@ -46,7 +72,36 @@ class SigmahqFilenamePrefixIssue(SigmaValidationIssue):
4672
class SigmahqFilenamePrefixValidator(SigmaRuleValidator):
4773
"""Check a rule filename against SigmaHQ filename prefix convention."""
4874

49-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
75+
def _is_combined_file(self, rule: SigmaRuleBase) -> bool:
76+
"""
77+
Check if the file contains a combined format (both detection(s) and correlation rules).
78+
This is determined by reading the file and checking for YAML document separator.
79+
"""
80+
if rule.source is None:
81+
return False
82+
83+
try:
84+
with open(rule.source.path, "r", encoding="utf-8") as f:
85+
content = f.read()
86+
# Check if file contains both correlation and detection/logsource sections
87+
has_separator = "\n---\n" in content or content.startswith("---\n")
88+
has_correlation = "correlation:" in content
89+
has_logsource = "logsource:" in content
90+
91+
# Combined if it has separator and both correlation and logsource
92+
return has_separator and has_correlation and has_logsource
93+
except:
94+
return False
95+
96+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
97+
# Only validate SigmaRule (detection rules), not correlation rules
98+
if not isinstance(rule, SigmaRule):
99+
return []
100+
101+
# Skip validation for combined format files (they can have multiple logsources)
102+
if self._is_combined_file(rule):
103+
return []
104+
50105
if rule.source is not None:
51106
filename = rule.source.path.name
52107
logsource = SigmaLogSource(

sigma/validators/sigmahq/logsource.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class SigmahqLogsourceUnknownIssue(SigmaValidationIssue):
2323
class SigmahqLogsourceUnknownValidator(SigmaRuleValidator):
2424
"""Checks if a rule uses an unknown logsource."""
2525

26-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
26+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
2727
# Ensure rule is a SigmaRule instance to access logsource
2828
logsource = getattr(rule, "logsource", None)
2929
if logsource is not None:
@@ -51,7 +51,11 @@ class SigmahqSysmonMissingEventidIssue(SigmaValidationIssue):
5151
class SigmahqSysmonMissingEventidValidator(SigmaRuleValidator):
5252
"""Checks if a rule uses the windows sysmon service logsource without the EventID field."""
5353

54-
def validate(self, rule: SigmaRule) -> List[SigmaValidationIssue]:
54+
def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
55+
# Only validate SigmaRule (detection rules), not correlation rules
56+
if not isinstance(rule, SigmaRule):
57+
return []
58+
5559
if rule.logsource.service == "sysmon":
5660
find = False
5761
for selection in rule.detection.detections.values():

sigma/validators/sigmahq/metadata.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import re
55

66
from sigma.rule import SigmaRuleBase, SigmaStatus
7+
from sigma.correlations import SigmaCorrelationRule
78
from sigma.validators.base import (
89
SigmaRuleValidator,
910
SigmaValidationIssue,
@@ -275,6 +276,10 @@ def validate(self, rule: SigmaRuleBase) -> List[SigmaValidationIssue]:
275276
custom_keys = list(rule.custom_attributes.keys())
276277
allowed_fields = {"regression_tests_path", "simulation"}
277278

279+
# For correlation rules, the 'correlation' field is standard, not custom
280+
if isinstance(rule, SigmaCorrelationRule):
281+
allowed_fields.add("correlation")
282+
278283
# Find any custom attributes that are not in the allowed list
279284
unknown_fields = [key for key in custom_keys if key not in allowed_fields]
280285

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
title: Test Detection Rule in Combined File
2+
id: aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
3+
status: test
4+
description: This is a test detection rule in a combined format file
5+
author: Test Author
6+
date: 2024-01-01
7+
level: medium
8+
logsource:
9+
category: process_creation
10+
product: windows
11+
detection:
12+
selection:
13+
Image|endswith: '\test.exe'
14+
condition: selection
15+
falsepositives:
16+
- Unknown
17+
---
18+
title: Test Correlation Rule in Combined File
19+
id: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb
20+
status: test
21+
description: This is a test correlation rule in a combined format file
22+
author: Test Author
23+
date: 2024-01-01
24+
level: high
25+
correlation:
26+
type: event_count
27+
rules:
28+
- aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
29+
group-by:
30+
- Computer
31+
timespan: 5m
32+
condition:
33+
gte: 10
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
title: Test Correlation Rule With Correct Filename
2+
id: 12345678-1234-1234-1234-123456789012
3+
status: test
4+
description: This is a test correlation rule with a correctly named file
5+
author: Test Author
6+
date: 2024-01-01
7+
level: high
8+
correlation:
9+
type: event_count
10+
rules:
11+
- 87654321-4321-4321-4321-210987654321
12+
group-by:
13+
- Computer
14+
timespan: 5m
15+
condition:
16+
gte: 10
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
title: Test Correlation Rule With Incorrect Filename
2+
id: 98765432-9876-9876-9876-987654321098
3+
status: test
4+
description: This is a test correlation rule with an incorrectly named file
5+
author: Test Author
6+
date: 2024-01-01
7+
level: high
8+
correlation:
9+
type: event_count
10+
rules:
11+
- 87654321-4321-4321-4321-210987654321
12+
group-by:
13+
- Computer
14+
timespan: 5m
15+
condition:
16+
gte: 10

0 commit comments

Comments
 (0)