1use serde::Serialize;
25
26use super::{FixDisposition, LintRule, Severity};
27
28#[derive(Debug, Clone, Copy, Serialize)]
30pub struct LintRuleInfo {
31 pub name: &'static str,
33 pub default_severity: Severity,
35 pub fix: Option<FixDisposition>,
39 pub description: &'static str,
41}
42
43macro_rules! lint_catalogue {
49 ($($variant:ident => ($sev:expr, $fix:expr, $desc:expr)),+ $(,)?) => {
50 const ALL_LINT_RULES: &[LintRule] = &[$(LintRule::$variant),+];
52
53 fn describe(rule: LintRule) -> (Severity, Option<FixDisposition>, &'static str) {
54 match rule {
55 $(LintRule::$variant => ($sev, $fix, $desc)),+
56 }
57 }
58 };
59}
60
61const SAFE: Option<FixDisposition> = Some(FixDisposition::Safe);
62const NONE: Option<FixDisposition> = None;
63
64lint_catalogue! {
65 YamlParseError => (Severity::Error, NONE, "The document is not valid YAML."),
67 NotAMapping => (Severity::Error, NONE, "The document root is not a YAML mapping."),
68 FileReadError => (Severity::Error, NONE, "The file could not be read from disk."),
69 SchemaViolation => (Severity::Error, NONE, "Reserved for JSON-schema validation failures."),
70
71 MissingTitle => (Severity::Error, NONE, "Required field 'title' is missing."),
73 EmptyTitle => (Severity::Error, NONE, "'title' is present but empty."),
74 TitleTooLong => (Severity::Warning, NONE, "'title' exceeds the 256-character maximum."),
75 MissingDescription => (Severity::Info, NONE, "Recommended field 'description' is missing."),
76 MissingAuthor => (Severity::Info, NONE, "Recommended field 'author' is missing."),
77 InvalidId => (Severity::Warning, NONE, "'id' is not a valid UUID."),
78 InvalidStatus => (Severity::Error, SAFE, "'status' is not one of the allowed values."),
79 MissingLevel => (Severity::Warning, NONE, "Recommended field 'level' is missing."),
80 InvalidLevel => (Severity::Error, SAFE, "'level' is not one of the allowed values."),
81 InvalidDate => (Severity::Error, NONE, "'date' is not a valid YYYY-MM-DD date."),
82 InvalidModified => (Severity::Error, NONE, "'modified' is not a valid YYYY-MM-DD date."),
83 ModifiedBeforeDate => (Severity::Warning, NONE, "'modified' is earlier than 'date'."),
84 DescriptionTooLong => (Severity::Warning, NONE, "'description' exceeds the 65535-character maximum."),
85 NameTooLong => (Severity::Warning, NONE, "'name' exceeds the 256-character maximum."),
86 TaxonomyTooLong => (Severity::Warning, NONE, "'taxonomy' exceeds the 256-character maximum."),
87 NonLowercaseKey => (Severity::Warning, SAFE, "A top-level key is not lowercase."),
88
89 MissingLogsource => (Severity::Error, NONE, "Detection rule is missing 'logsource'."),
91 MissingDetection => (Severity::Error, NONE, "Detection rule is missing 'detection'."),
92 MissingCondition => (Severity::Error, NONE, "'detection' is missing 'condition'."),
93 EmptyDetection => (Severity::Warning, NONE, "'detection' has no named search identifiers."),
94 InvalidRelatedType => (Severity::Error, NONE, "A 'related' entry uses an invalid relation type."),
95 InvalidRelatedId => (Severity::Warning, NONE, "A 'related' entry id is not a valid UUID."),
96 RelatedMissingRequired => (Severity::Error, NONE, "A 'related' entry is missing 'id' or 'type'."),
97 DeprecatedWithoutRelated => (Severity::Warning, NONE, "A deprecated rule has no 'related' replacement link."),
98 InvalidTag => (Severity::Warning, NONE, "A tag does not match the expected pattern."),
99 UnknownTagNamespace => (Severity::Warning, NONE, "A tag uses an unrecognised namespace."),
100 DuplicateTags => (Severity::Warning, SAFE, "The 'tags' list has duplicate entries."),
101 DuplicateReferences => (Severity::Warning, SAFE, "The 'references' list has duplicate entries."),
102 DuplicateFields => (Severity::Warning, SAFE, "The 'fields' list has duplicate entries."),
103 FalsepositiveTooShort => (Severity::Warning, NONE, "A 'falsepositives' entry is too short."),
104 ScopeTooShort => (Severity::Warning, NONE, "A 'scope' entry is too short."),
105 LogsourceValueNotLowercase => (Severity::Warning, SAFE, "A logsource value is not lowercase."),
106 ConditionReferencesUnknown => (Severity::Error, NONE, "The condition references an undefined selection."),
107 DeprecatedAggregationSyntax => (Severity::Warning, NONE, "The condition uses deprecated v1.x aggregation syntax."),
108
109 MissingCorrelation => (Severity::Error, NONE, "Correlation rule is missing 'correlation'."),
111 MissingCorrelationType => (Severity::Error, NONE, "'correlation' is missing 'type'."),
112 InvalidCorrelationType => (Severity::Error, NONE, "'correlation.type' is not a known correlation type."),
113 MissingCorrelationRules => (Severity::Error, NONE, "'correlation' is missing 'rules'."),
114 EmptyCorrelationRules => (Severity::Warning, NONE, "'correlation.rules' is present but empty."),
115 MissingCorrelationTimespan => (Severity::Error, NONE, "'correlation' is missing 'timespan'."),
116 InvalidTimespanFormat => (Severity::Error, NONE, "'correlation.timespan' is not a valid duration."),
117 InvalidWindowMode => (Severity::Error, NONE, "'correlation.window' is not sliding, tumbling, or session."),
118 MissingSessionGap => (Severity::Error, NONE, "window: session requires a 'gap'."),
119 GapWithoutSession => (Severity::Error, NONE, "'gap' is only valid with window: session."),
120 InvalidGapFormat => (Severity::Error, NONE, "'gap' is not a valid duration."),
121 MissingGroupBy => (Severity::Error, NONE, "This correlation type requires 'group-by'."),
122 MissingCorrelationCondition => (Severity::Error, NONE, "This correlation type requires a 'condition'."),
123 MissingConditionField => (Severity::Error, NONE, "The correlation condition requires 'field'."),
124 InvalidConditionOperator => (Severity::Error, NONE, "The correlation condition uses an invalid operator."),
125 ConditionValueNotNumeric => (Severity::Error, NONE, "The correlation condition value must be numeric."),
126 GenerateNotBoolean => (Severity::Error, NONE, "'generate' must be a boolean."),
127
128 MissingFilter => (Severity::Error, NONE, "Filter rule is missing 'filter'."),
130 MissingFilterRules => (Severity::Error, NONE, "'filter.rules' is malformed."),
131 EmptyFilterRules => (Severity::Warning, NONE, "'filter.rules' is present but empty."),
132 MissingFilterSelection => (Severity::Error, NONE, "'filter' is missing 'selection'."),
133 MissingFilterCondition => (Severity::Error, NONE, "'filter' is missing 'condition'."),
134 FilterHasLevel => (Severity::Warning, SAFE, "Filter rules should not have a 'level' field."),
135 FilterHasStatus => (Severity::Warning, SAFE, "Filter rules should not have a 'status' field."),
136 MissingFilterLogsource => (Severity::Error, NONE, "Filter rule is missing 'logsource'."),
137
138 NullInValueList => (Severity::Warning, NONE, "A value list contains a null entry."),
140 SingleValueAllModifier => (Severity::Warning, SAFE, "|all is used with a single value."),
141 AllWithRe => (Severity::Warning, SAFE, "|all combined with |re is redundant."),
142 IncompatibleModifiers => (Severity::Warning, NONE, "A field uses an incompatible combination of modifiers."),
143 EmptyValueList => (Severity::Warning, NONE, "A field has an empty value list."),
144 WildcardOnlyValue => (Severity::Warning, SAFE, "A value is a lone wildcard; consider |exists instead."),
145 FlattenedArrayCorrelation => (Severity::Warning, NONE, "A correlation references a flattened array field."),
146 UnsupportedSigmaVersion => (Severity::Error, NONE, "The declared sigma-version is unsupported."),
147 ArrayMatchingWithoutVersion => (Severity::Warning, NONE, "Array matching is used without declaring sigma-version 3."),
148 SigmaVersionMismatch => (Severity::Warning, NONE, "Cross-referenced rules declare different spec majors."),
149 UnknownRuleReference => (Severity::Warning, NONE, "A reference resolves to no known rule."),
150 UnknownKey => (Severity::Info, SAFE, "An unknown top-level key (likely a typo)."),
151
152 AdsMissingGoal => (Severity::Warning, NONE, "An enforced rule has no ADS goal (description)."),
154 AdsMissingCategorization => (Severity::Warning, NONE, "An enforced rule has no ADS categorization (attack.* tag)."),
155 AdsMissingStrategy => (Severity::Warning, NONE, "An enforced rule has no ADS strategy abstract."),
156 AdsMissingTechnicalContext => (Severity::Warning, NONE, "An enforced rule has no ADS technical context."),
157 AdsMissingBlindSpots => (Severity::Warning, NONE, "An enforced rule states no ADS blind spots or assumptions."),
158 AdsMissingFalsePositives => (Severity::Warning, NONE, "An enforced rule has no ADS false-positive notes (falsepositives)."),
159 AdsMissingValidation => (Severity::Warning, NONE, "An enforced rule has no ADS validation recipe."),
160 AdsMissingPriority => (Severity::Info, NONE, "An enforced rule has no ADS priority rationale."),
161 AdsMissingResponse => (Severity::Warning, NONE, "An enforced rule has no ADS response plan."),
162 AdsEmptySection => (Severity::Info, NONE, "A present rsigma.ads.* section is blank or too short."),
163 AdsUnknownSection => (Severity::Info, SAFE, "An unknown rsigma.ads.* section (likely a typo)."),
164}
165
166pub fn catalogue() -> Vec<LintRuleInfo> {
168 ALL_LINT_RULES
169 .iter()
170 .map(|&rule| {
171 let (default_severity, fix, description) = describe(rule);
172 LintRuleInfo {
173 name: rule_name(rule),
174 default_severity,
175 fix,
176 description,
177 }
178 })
179 .collect()
180}
181
182fn rule_name(rule: LintRule) -> &'static str {
184 LINT_RULE_NAMES
188 .iter()
189 .find(|(r, _)| *r == rule)
190 .map(|(_, name)| *name)
191 .expect("every rule has a name")
192}
193
194const LINT_RULE_NAMES: &[(LintRule, &str)] = &[
198 (LintRule::YamlParseError, "yaml_parse_error"),
199 (LintRule::NotAMapping, "not_a_mapping"),
200 (LintRule::FileReadError, "file_read_error"),
201 (LintRule::SchemaViolation, "schema_violation"),
202 (LintRule::MissingTitle, "missing_title"),
203 (LintRule::EmptyTitle, "empty_title"),
204 (LintRule::TitleTooLong, "title_too_long"),
205 (LintRule::MissingDescription, "missing_description"),
206 (LintRule::MissingAuthor, "missing_author"),
207 (LintRule::InvalidId, "invalid_id"),
208 (LintRule::InvalidStatus, "invalid_status"),
209 (LintRule::MissingLevel, "missing_level"),
210 (LintRule::InvalidLevel, "invalid_level"),
211 (LintRule::InvalidDate, "invalid_date"),
212 (LintRule::InvalidModified, "invalid_modified"),
213 (LintRule::ModifiedBeforeDate, "modified_before_date"),
214 (LintRule::DescriptionTooLong, "description_too_long"),
215 (LintRule::NameTooLong, "name_too_long"),
216 (LintRule::TaxonomyTooLong, "taxonomy_too_long"),
217 (LintRule::NonLowercaseKey, "non_lowercase_key"),
218 (LintRule::MissingLogsource, "missing_logsource"),
219 (LintRule::MissingDetection, "missing_detection"),
220 (LintRule::MissingCondition, "missing_condition"),
221 (LintRule::EmptyDetection, "empty_detection"),
222 (LintRule::InvalidRelatedType, "invalid_related_type"),
223 (LintRule::InvalidRelatedId, "invalid_related_id"),
224 (LintRule::RelatedMissingRequired, "related_missing_required"),
225 (
226 LintRule::DeprecatedWithoutRelated,
227 "deprecated_without_related",
228 ),
229 (LintRule::InvalidTag, "invalid_tag"),
230 (LintRule::UnknownTagNamespace, "unknown_tag_namespace"),
231 (LintRule::DuplicateTags, "duplicate_tags"),
232 (LintRule::DuplicateReferences, "duplicate_references"),
233 (LintRule::DuplicateFields, "duplicate_fields"),
234 (LintRule::FalsepositiveTooShort, "falsepositive_too_short"),
235 (LintRule::ScopeTooShort, "scope_too_short"),
236 (
237 LintRule::LogsourceValueNotLowercase,
238 "logsource_value_not_lowercase",
239 ),
240 (
241 LintRule::ConditionReferencesUnknown,
242 "condition_references_unknown",
243 ),
244 (
245 LintRule::DeprecatedAggregationSyntax,
246 "deprecated_aggregation_syntax",
247 ),
248 (LintRule::MissingCorrelation, "missing_correlation"),
249 (LintRule::MissingCorrelationType, "missing_correlation_type"),
250 (LintRule::InvalidCorrelationType, "invalid_correlation_type"),
251 (
252 LintRule::MissingCorrelationRules,
253 "missing_correlation_rules",
254 ),
255 (LintRule::EmptyCorrelationRules, "empty_correlation_rules"),
256 (
257 LintRule::MissingCorrelationTimespan,
258 "missing_correlation_timespan",
259 ),
260 (LintRule::InvalidTimespanFormat, "invalid_timespan_format"),
261 (LintRule::InvalidWindowMode, "invalid_window_mode"),
262 (LintRule::MissingSessionGap, "missing_session_gap"),
263 (LintRule::GapWithoutSession, "gap_without_session"),
264 (LintRule::InvalidGapFormat, "invalid_gap_format"),
265 (LintRule::MissingGroupBy, "missing_group_by"),
266 (
267 LintRule::MissingCorrelationCondition,
268 "missing_correlation_condition",
269 ),
270 (LintRule::MissingConditionField, "missing_condition_field"),
271 (
272 LintRule::InvalidConditionOperator,
273 "invalid_condition_operator",
274 ),
275 (
276 LintRule::ConditionValueNotNumeric,
277 "condition_value_not_numeric",
278 ),
279 (LintRule::GenerateNotBoolean, "generate_not_boolean"),
280 (LintRule::MissingFilter, "missing_filter"),
281 (LintRule::MissingFilterRules, "missing_filter_rules"),
282 (LintRule::EmptyFilterRules, "empty_filter_rules"),
283 (LintRule::MissingFilterSelection, "missing_filter_selection"),
284 (LintRule::MissingFilterCondition, "missing_filter_condition"),
285 (LintRule::FilterHasLevel, "filter_has_level"),
286 (LintRule::FilterHasStatus, "filter_has_status"),
287 (LintRule::MissingFilterLogsource, "missing_filter_logsource"),
288 (LintRule::NullInValueList, "null_in_value_list"),
289 (
290 LintRule::SingleValueAllModifier,
291 "single_value_all_modifier",
292 ),
293 (LintRule::AllWithRe, "all_with_re"),
294 (LintRule::IncompatibleModifiers, "incompatible_modifiers"),
295 (LintRule::EmptyValueList, "empty_value_list"),
296 (LintRule::WildcardOnlyValue, "wildcard_only_value"),
297 (
298 LintRule::FlattenedArrayCorrelation,
299 "flattened_array_correlation",
300 ),
301 (
302 LintRule::UnsupportedSigmaVersion,
303 "unsupported_sigma_version",
304 ),
305 (
306 LintRule::ArrayMatchingWithoutVersion,
307 "array_matching_without_version",
308 ),
309 (LintRule::SigmaVersionMismatch, "sigma_version_mismatch"),
310 (LintRule::UnknownRuleReference, "unknown_rule_reference"),
311 (LintRule::UnknownKey, "unknown_key"),
312 (LintRule::AdsMissingGoal, "ads_missing_goal"),
313 (
314 LintRule::AdsMissingCategorization,
315 "ads_missing_categorization",
316 ),
317 (LintRule::AdsMissingStrategy, "ads_missing_strategy"),
318 (
319 LintRule::AdsMissingTechnicalContext,
320 "ads_missing_technical_context",
321 ),
322 (LintRule::AdsMissingBlindSpots, "ads_missing_blind_spots"),
323 (
324 LintRule::AdsMissingFalsePositives,
325 "ads_missing_false_positives",
326 ),
327 (LintRule::AdsMissingValidation, "ads_missing_validation"),
328 (LintRule::AdsMissingPriority, "ads_missing_priority"),
329 (LintRule::AdsMissingResponse, "ads_missing_response"),
330 (LintRule::AdsEmptySection, "ads_empty_section"),
331 (LintRule::AdsUnknownSection, "ads_unknown_section"),
332];
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use std::collections::HashSet;
338
339 #[test]
340 fn catalogue_covers_every_rule() {
341 let entries = catalogue();
345 assert_eq!(entries.len(), 86, "expected 86 catalogue entries");
346 assert_eq!(ALL_LINT_RULES.len(), 86);
347 assert_eq!(LINT_RULE_NAMES.len(), 86);
348 }
349
350 #[test]
351 fn names_match_display_and_are_unique() {
352 let mut seen = HashSet::new();
353 for &rule in ALL_LINT_RULES {
354 let name = rule_name(rule);
355 assert_eq!(
356 name,
357 rule.to_string(),
358 "catalogue name must match LintRule Display"
359 );
360 assert!(seen.insert(name), "duplicate catalogue name: {name}");
361 }
362 }
363
364 #[test]
365 fn fixable_rules_are_safe() {
366 for info in catalogue() {
367 if let Some(disposition) = info.fix {
368 assert_eq!(
369 disposition,
370 FixDisposition::Safe,
371 "{} has a non-safe fix in the catalogue",
372 info.name
373 );
374 }
375 }
376 }
377}