use super::condition::RuleCondition;
use crate::findings::{RecommendedAction, Severity, ThreatCategory};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ShieldHint {
pub scope: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Rule {
pub id: String,
pub category: ThreatCategory,
pub severity: Severity,
#[serde(default = "default_confidence")]
pub confidence: f32,
#[serde(rename = "when")]
pub condition: RuleCondition,
pub action: RecommendedAction,
pub reason: String,
#[serde(default)]
pub shield: Option<ShieldHint>,
#[serde(default = "default_enabled")]
pub enabled: bool,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RulePackKind {
Official,
Community,
IocFeed,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct RulePackMetadata {
#[serde(default)]
pub name: String,
#[serde(default)]
pub kind: Option<RulePackKind>,
#[serde(default)]
pub compatibility: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RulePackFile {
pub schema_version: String,
#[serde(default)]
pub metadata: RulePackMetadata,
#[serde(default)]
pub rules: Vec<Rule>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct IocFeedFile {
pub schema_version: String,
#[serde(default)]
pub metadata: RulePackMetadata,
#[serde(default)]
pub domains: Vec<String>,
#[serde(default)]
pub filenames: Vec<String>,
#[serde(default)]
pub ips: Vec<String>,
}
fn default_confidence() -> f32 {
super::DEFAULT_RULE_CONFIDENCE
}
fn default_enabled() -> bool {
true
}
#[cfg(test)]
mod deny_unknown_fields_tests {
use super::*;
#[test]
fn rule_rejects_unknown_fields() {
let yaml = r#"
id: TEST_RULE
category: RemoteExec
severity: High
when:
regex:
pattern: "curl"
action: Block
reason: test
confedence: 0.5
"#;
let result: Result<Rule, _> = serde_yaml::from_str(yaml);
assert!(
result.is_err(),
"Rule MUST reject unknown field 'confedence'; \
pre-fix, this was silently accepted and confidence defaulted to 0.9"
);
}
#[test]
fn shield_hint_rejects_unknown_fields() {
let yaml = "scop: package\n";
let result: Result<ShieldHint, _> = serde_yaml::from_str(yaml);
assert!(
result.is_err(),
"ShieldHint MUST reject unknown field 'scop'; \
pre-fix, this was silently accepted and scope was missing"
);
}
#[test]
fn rule_pack_file_rejects_unknown_fields() {
let yaml = "schema_version: \"1\"\nruels: []\n";
let result: Result<RulePackFile, _> = serde_yaml::from_str(yaml);
assert!(
result.is_err(),
"RulePackFile MUST reject unknown field 'ruels'; \
pre-fix, this was silently accepted and rules defaulted to empty"
);
}
#[test]
fn ioc_feed_file_rejects_unknown_fields() {
let yaml = "schema_version: \"1\"\ndomians: []\n";
let result: Result<IocFeedFile, _> = serde_yaml::from_str(yaml);
assert!(
result.is_err(),
"IocFeedFile MUST reject unknown field 'domians'; \
pre-fix, this was silently accepted and domains defaulted to empty"
);
}
#[test]
fn rule_pack_metadata_rejects_unknown_fields() {
let yaml = "compatability: [\"1\"]\n";
let result: Result<RulePackMetadata, _> = serde_yaml::from_str(yaml);
assert!(
result.is_err(),
"RulePackMetadata MUST reject unknown field 'compatability'; \
pre-fix, this was silently accepted and compatibility defaulted to empty"
);
}
}