use serde::Serialize;
use super::{FixDisposition, LintRule, Severity};
#[derive(Debug, Clone, Copy, Serialize)]
pub struct LintRuleInfo {
pub name: &'static str,
pub default_severity: Severity,
pub fix: Option<FixDisposition>,
pub description: &'static str,
}
macro_rules! lint_catalogue {
($($variant:ident => ($sev:expr, $fix:expr, $desc:expr)),+ $(,)?) => {
const ALL_LINT_RULES: &[LintRule] = &[$(LintRule::$variant),+];
fn describe(rule: LintRule) -> (Severity, Option<FixDisposition>, &'static str) {
match rule {
$(LintRule::$variant => ($sev, $fix, $desc)),+
}
}
};
}
const SAFE: Option<FixDisposition> = Some(FixDisposition::Safe);
const NONE: Option<FixDisposition> = None;
lint_catalogue! {
YamlParseError => (Severity::Error, NONE, "The document is not valid YAML."),
NotAMapping => (Severity::Error, NONE, "The document root is not a YAML mapping."),
FileReadError => (Severity::Error, NONE, "The file could not be read from disk."),
SchemaViolation => (Severity::Error, NONE, "Reserved for JSON-schema validation failures."),
MissingTitle => (Severity::Error, NONE, "Required field 'title' is missing."),
EmptyTitle => (Severity::Error, NONE, "'title' is present but empty."),
TitleTooLong => (Severity::Warning, NONE, "'title' exceeds the 256-character maximum."),
MissingDescription => (Severity::Info, NONE, "Recommended field 'description' is missing."),
MissingAuthor => (Severity::Info, NONE, "Recommended field 'author' is missing."),
InvalidId => (Severity::Warning, NONE, "'id' is not a valid UUID."),
InvalidStatus => (Severity::Error, SAFE, "'status' is not one of the allowed values."),
MissingLevel => (Severity::Warning, NONE, "Recommended field 'level' is missing."),
InvalidLevel => (Severity::Error, SAFE, "'level' is not one of the allowed values."),
InvalidDate => (Severity::Error, NONE, "'date' is not a valid YYYY-MM-DD date."),
InvalidModified => (Severity::Error, NONE, "'modified' is not a valid YYYY-MM-DD date."),
ModifiedBeforeDate => (Severity::Warning, NONE, "'modified' is earlier than 'date'."),
DescriptionTooLong => (Severity::Warning, NONE, "'description' exceeds the 65535-character maximum."),
NameTooLong => (Severity::Warning, NONE, "'name' exceeds the 256-character maximum."),
TaxonomyTooLong => (Severity::Warning, NONE, "'taxonomy' exceeds the 256-character maximum."),
NonLowercaseKey => (Severity::Warning, SAFE, "A top-level key is not lowercase."),
MissingLogsource => (Severity::Error, NONE, "Detection rule is missing 'logsource'."),
MissingDetection => (Severity::Error, NONE, "Detection rule is missing 'detection'."),
MissingCondition => (Severity::Error, NONE, "'detection' is missing 'condition'."),
EmptyDetection => (Severity::Warning, NONE, "'detection' has no named search identifiers."),
InvalidRelatedType => (Severity::Error, NONE, "A 'related' entry uses an invalid relation type."),
InvalidRelatedId => (Severity::Warning, NONE, "A 'related' entry id is not a valid UUID."),
RelatedMissingRequired => (Severity::Error, NONE, "A 'related' entry is missing 'id' or 'type'."),
DeprecatedWithoutRelated => (Severity::Warning, NONE, "A deprecated rule has no 'related' replacement link."),
InvalidTag => (Severity::Warning, NONE, "A tag does not match the expected pattern."),
UnknownTagNamespace => (Severity::Warning, NONE, "A tag uses an unrecognised namespace."),
DuplicateTags => (Severity::Warning, SAFE, "The 'tags' list has duplicate entries."),
DuplicateReferences => (Severity::Warning, SAFE, "The 'references' list has duplicate entries."),
DuplicateFields => (Severity::Warning, SAFE, "The 'fields' list has duplicate entries."),
FalsepositiveTooShort => (Severity::Warning, NONE, "A 'falsepositives' entry is too short."),
ScopeTooShort => (Severity::Warning, NONE, "A 'scope' entry is too short."),
LogsourceValueNotLowercase => (Severity::Warning, SAFE, "A logsource value is not lowercase."),
ConditionReferencesUnknown => (Severity::Error, NONE, "The condition references an undefined selection."),
DeprecatedAggregationSyntax => (Severity::Warning, NONE, "The condition uses deprecated v1.x aggregation syntax."),
MissingCorrelation => (Severity::Error, NONE, "Correlation rule is missing 'correlation'."),
MissingCorrelationType => (Severity::Error, NONE, "'correlation' is missing 'type'."),
InvalidCorrelationType => (Severity::Error, NONE, "'correlation.type' is not a known correlation type."),
MissingCorrelationRules => (Severity::Error, NONE, "'correlation' is missing 'rules'."),
EmptyCorrelationRules => (Severity::Warning, NONE, "'correlation.rules' is present but empty."),
MissingCorrelationTimespan => (Severity::Error, NONE, "'correlation' is missing 'timespan'."),
InvalidTimespanFormat => (Severity::Error, NONE, "'correlation.timespan' is not a valid duration."),
InvalidWindowMode => (Severity::Error, NONE, "'correlation.window' is not sliding, tumbling, or session."),
MissingSessionGap => (Severity::Error, NONE, "window: session requires a 'gap'."),
GapWithoutSession => (Severity::Error, NONE, "'gap' is only valid with window: session."),
InvalidGapFormat => (Severity::Error, NONE, "'gap' is not a valid duration."),
MissingGroupBy => (Severity::Error, NONE, "This correlation type requires 'group-by'."),
MissingCorrelationCondition => (Severity::Error, NONE, "This correlation type requires a 'condition'."),
MissingConditionField => (Severity::Error, NONE, "The correlation condition requires 'field'."),
InvalidConditionOperator => (Severity::Error, NONE, "The correlation condition uses an invalid operator."),
ConditionValueNotNumeric => (Severity::Error, NONE, "The correlation condition value must be numeric."),
GenerateNotBoolean => (Severity::Error, NONE, "'generate' must be a boolean."),
MissingFilter => (Severity::Error, NONE, "Filter rule is missing 'filter'."),
MissingFilterRules => (Severity::Error, NONE, "'filter.rules' is malformed."),
EmptyFilterRules => (Severity::Warning, NONE, "'filter.rules' is present but empty."),
MissingFilterSelection => (Severity::Error, NONE, "'filter' is missing 'selection'."),
MissingFilterCondition => (Severity::Error, NONE, "'filter' is missing 'condition'."),
FilterHasLevel => (Severity::Warning, SAFE, "Filter rules should not have a 'level' field."),
FilterHasStatus => (Severity::Warning, SAFE, "Filter rules should not have a 'status' field."),
MissingFilterLogsource => (Severity::Error, NONE, "Filter rule is missing 'logsource'."),
NullInValueList => (Severity::Warning, NONE, "A value list contains a null entry."),
SingleValueAllModifier => (Severity::Warning, SAFE, "|all is used with a single value."),
AllWithRe => (Severity::Warning, SAFE, "|all combined with |re is redundant."),
IncompatibleModifiers => (Severity::Warning, NONE, "A field uses an incompatible combination of modifiers."),
EmptyValueList => (Severity::Warning, NONE, "A field has an empty value list."),
WildcardOnlyValue => (Severity::Warning, SAFE, "A value is a lone wildcard; consider |exists instead."),
FlattenedArrayCorrelation => (Severity::Warning, NONE, "A correlation references a flattened array field."),
UnsupportedSigmaVersion => (Severity::Error, NONE, "The declared sigma-version is unsupported."),
ArrayMatchingWithoutVersion => (Severity::Warning, NONE, "Array matching is used without declaring sigma-version 3."),
SigmaVersionMismatch => (Severity::Warning, NONE, "Cross-referenced rules declare different spec majors."),
UnknownRuleReference => (Severity::Warning, NONE, "A reference resolves to no known rule."),
UnknownKey => (Severity::Info, SAFE, "An unknown top-level key (likely a typo)."),
AdsMissingGoal => (Severity::Warning, NONE, "An enforced rule has no ADS goal (description)."),
AdsMissingCategorization => (Severity::Warning, NONE, "An enforced rule has no ADS categorization (attack.* tag)."),
AdsMissingStrategy => (Severity::Warning, NONE, "An enforced rule has no ADS strategy abstract."),
AdsMissingTechnicalContext => (Severity::Warning, NONE, "An enforced rule has no ADS technical context."),
AdsMissingBlindSpots => (Severity::Warning, NONE, "An enforced rule states no ADS blind spots or assumptions."),
AdsMissingFalsePositives => (Severity::Warning, NONE, "An enforced rule has no ADS false-positive notes (falsepositives)."),
AdsMissingValidation => (Severity::Warning, NONE, "An enforced rule has no ADS validation recipe."),
AdsMissingPriority => (Severity::Info, NONE, "An enforced rule has no ADS priority rationale."),
AdsMissingResponse => (Severity::Warning, NONE, "An enforced rule has no ADS response plan."),
AdsEmptySection => (Severity::Info, NONE, "A present rsigma.ads.* section is blank or too short."),
AdsUnknownSection => (Severity::Info, SAFE, "An unknown rsigma.ads.* section (likely a typo)."),
}
pub fn catalogue() -> Vec<LintRuleInfo> {
ALL_LINT_RULES
.iter()
.map(|&rule| {
let (default_severity, fix, description) = describe(rule);
LintRuleInfo {
name: rule_name(rule),
default_severity,
fix,
description,
}
})
.collect()
}
fn rule_name(rule: LintRule) -> &'static str {
LINT_RULE_NAMES
.iter()
.find(|(r, _)| *r == rule)
.map(|(_, name)| *name)
.expect("every rule has a name")
}
const LINT_RULE_NAMES: &[(LintRule, &str)] = &[
(LintRule::YamlParseError, "yaml_parse_error"),
(LintRule::NotAMapping, "not_a_mapping"),
(LintRule::FileReadError, "file_read_error"),
(LintRule::SchemaViolation, "schema_violation"),
(LintRule::MissingTitle, "missing_title"),
(LintRule::EmptyTitle, "empty_title"),
(LintRule::TitleTooLong, "title_too_long"),
(LintRule::MissingDescription, "missing_description"),
(LintRule::MissingAuthor, "missing_author"),
(LintRule::InvalidId, "invalid_id"),
(LintRule::InvalidStatus, "invalid_status"),
(LintRule::MissingLevel, "missing_level"),
(LintRule::InvalidLevel, "invalid_level"),
(LintRule::InvalidDate, "invalid_date"),
(LintRule::InvalidModified, "invalid_modified"),
(LintRule::ModifiedBeforeDate, "modified_before_date"),
(LintRule::DescriptionTooLong, "description_too_long"),
(LintRule::NameTooLong, "name_too_long"),
(LintRule::TaxonomyTooLong, "taxonomy_too_long"),
(LintRule::NonLowercaseKey, "non_lowercase_key"),
(LintRule::MissingLogsource, "missing_logsource"),
(LintRule::MissingDetection, "missing_detection"),
(LintRule::MissingCondition, "missing_condition"),
(LintRule::EmptyDetection, "empty_detection"),
(LintRule::InvalidRelatedType, "invalid_related_type"),
(LintRule::InvalidRelatedId, "invalid_related_id"),
(LintRule::RelatedMissingRequired, "related_missing_required"),
(
LintRule::DeprecatedWithoutRelated,
"deprecated_without_related",
),
(LintRule::InvalidTag, "invalid_tag"),
(LintRule::UnknownTagNamespace, "unknown_tag_namespace"),
(LintRule::DuplicateTags, "duplicate_tags"),
(LintRule::DuplicateReferences, "duplicate_references"),
(LintRule::DuplicateFields, "duplicate_fields"),
(LintRule::FalsepositiveTooShort, "falsepositive_too_short"),
(LintRule::ScopeTooShort, "scope_too_short"),
(
LintRule::LogsourceValueNotLowercase,
"logsource_value_not_lowercase",
),
(
LintRule::ConditionReferencesUnknown,
"condition_references_unknown",
),
(
LintRule::DeprecatedAggregationSyntax,
"deprecated_aggregation_syntax",
),
(LintRule::MissingCorrelation, "missing_correlation"),
(LintRule::MissingCorrelationType, "missing_correlation_type"),
(LintRule::InvalidCorrelationType, "invalid_correlation_type"),
(
LintRule::MissingCorrelationRules,
"missing_correlation_rules",
),
(LintRule::EmptyCorrelationRules, "empty_correlation_rules"),
(
LintRule::MissingCorrelationTimespan,
"missing_correlation_timespan",
),
(LintRule::InvalidTimespanFormat, "invalid_timespan_format"),
(LintRule::InvalidWindowMode, "invalid_window_mode"),
(LintRule::MissingSessionGap, "missing_session_gap"),
(LintRule::GapWithoutSession, "gap_without_session"),
(LintRule::InvalidGapFormat, "invalid_gap_format"),
(LintRule::MissingGroupBy, "missing_group_by"),
(
LintRule::MissingCorrelationCondition,
"missing_correlation_condition",
),
(LintRule::MissingConditionField, "missing_condition_field"),
(
LintRule::InvalidConditionOperator,
"invalid_condition_operator",
),
(
LintRule::ConditionValueNotNumeric,
"condition_value_not_numeric",
),
(LintRule::GenerateNotBoolean, "generate_not_boolean"),
(LintRule::MissingFilter, "missing_filter"),
(LintRule::MissingFilterRules, "missing_filter_rules"),
(LintRule::EmptyFilterRules, "empty_filter_rules"),
(LintRule::MissingFilterSelection, "missing_filter_selection"),
(LintRule::MissingFilterCondition, "missing_filter_condition"),
(LintRule::FilterHasLevel, "filter_has_level"),
(LintRule::FilterHasStatus, "filter_has_status"),
(LintRule::MissingFilterLogsource, "missing_filter_logsource"),
(LintRule::NullInValueList, "null_in_value_list"),
(
LintRule::SingleValueAllModifier,
"single_value_all_modifier",
),
(LintRule::AllWithRe, "all_with_re"),
(LintRule::IncompatibleModifiers, "incompatible_modifiers"),
(LintRule::EmptyValueList, "empty_value_list"),
(LintRule::WildcardOnlyValue, "wildcard_only_value"),
(
LintRule::FlattenedArrayCorrelation,
"flattened_array_correlation",
),
(
LintRule::UnsupportedSigmaVersion,
"unsupported_sigma_version",
),
(
LintRule::ArrayMatchingWithoutVersion,
"array_matching_without_version",
),
(LintRule::SigmaVersionMismatch, "sigma_version_mismatch"),
(LintRule::UnknownRuleReference, "unknown_rule_reference"),
(LintRule::UnknownKey, "unknown_key"),
(LintRule::AdsMissingGoal, "ads_missing_goal"),
(
LintRule::AdsMissingCategorization,
"ads_missing_categorization",
),
(LintRule::AdsMissingStrategy, "ads_missing_strategy"),
(
LintRule::AdsMissingTechnicalContext,
"ads_missing_technical_context",
),
(LintRule::AdsMissingBlindSpots, "ads_missing_blind_spots"),
(
LintRule::AdsMissingFalsePositives,
"ads_missing_false_positives",
),
(LintRule::AdsMissingValidation, "ads_missing_validation"),
(LintRule::AdsMissingPriority, "ads_missing_priority"),
(LintRule::AdsMissingResponse, "ads_missing_response"),
(LintRule::AdsEmptySection, "ads_empty_section"),
(LintRule::AdsUnknownSection, "ads_unknown_section"),
];
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn catalogue_covers_every_rule() {
let entries = catalogue();
assert_eq!(entries.len(), 86, "expected 86 catalogue entries");
assert_eq!(ALL_LINT_RULES.len(), 86);
assert_eq!(LINT_RULE_NAMES.len(), 86);
}
#[test]
fn names_match_display_and_are_unique() {
let mut seen = HashSet::new();
for &rule in ALL_LINT_RULES {
let name = rule_name(rule);
assert_eq!(
name,
rule.to_string(),
"catalogue name must match LintRule Display"
);
assert!(seen.insert(name), "duplicate catalogue name: {name}");
}
}
#[test]
fn fixable_rules_are_safe() {
for info in catalogue() {
if let Some(disposition) = info.fix {
assert_eq!(
disposition,
FixDisposition::Safe,
"{} has a non-safe fix in the catalogue",
info.name
);
}
}
}
}