33from typing import ClassVar , Dict , List
44
55from sigma .rule import SigmaRule , SigmaLogSource , SigmaRuleBase
6+ from sigma .correlations import SigmaCorrelationRule
67
78from 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+
2533class 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
3864class SigmahqFilenamePrefixIssue (SigmaValidationIssue ):
3965 description : ClassVar [str ] = "The rule filename prefix doesn't match the SigmaHQ convention"
@@ -46,7 +72,36 @@ class SigmahqFilenamePrefixIssue(SigmaValidationIssue):
4672class 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 (
0 commit comments