1use crate::category::Category;
2
3pub fn is_fatal_rule(rule_id: &str) -> bool {
10 rule_id.starts_with('F')
11}
12
13pub const CUSTOM_RULE_ID_SEPARATORS: [char; 3] = ['_', '.', '-'];
16
17pub fn is_valid_custom_rule_id(rule_id: &str) -> bool {
25 !rule_id.is_empty() && rule_id.chars().all(|c| c.is_ascii_alphanumeric() || CUSTOM_RULE_ID_SEPARATORS.contains(&c))
26}
27
28pub fn rule_number(rule_id: &str) -> u32 {
33 rule_id.get(1..).and_then(|digits| digits.parse::<u32>().ok()).unwrap_or(u32::MAX)
34}
35
36pub fn category_for_rule_id(rule_id: &str) -> Category {
38 if let Some(prefix3) = rule_id.get(..3) {
39 match prefix3 {
40 "E25" | "W25" => return Category::Resource,
41 "E86" | "W86" | "F86" => return Category::Structure,
42 "I25" => return Category::BestPractice,
43 "I35" => return Category::Resource,
44 "W30" => return Category::Schema,
45 "W36" => return Category::Resource,
46 _ => {}
47 }
48 }
49 if let Some(prefix2) = rule_id.get(..2) {
50 match prefix2 {
51 "F0" => return Category::Structure,
52 "F1" => return Category::Intrinsic,
53 "F2" => return Category::Structure,
54 "F3" => return Category::Schema,
55 "F5" => return Category::Structure,
56 "F6" => return Category::Structure,
57 "F7" => return Category::Structure,
58 "F8" => return Category::Structure,
59 "E0" => return Category::Structure,
60 "E1" => return Category::Intrinsic,
61 "E2" => return Category::Parameter,
62 "E3" => return Category::Schema,
63 "E5" => return Category::Structure,
64 "E6" => return Category::Structure,
65 "E8" => return Category::Structure,
66 "W1" => return Category::Intrinsic,
67 "W2" => return Category::Security,
68 "W3" => return Category::Schema,
69 "W7" => return Category::Structure,
70 "W8" => return Category::Structure,
71 "I2" => return Category::Structure,
72 "I3" => return Category::BestPractice,
73 _ => {}
74 }
75 }
76 Category::Resource
77}
78
79pub fn format_rule_for_format(format: &str) -> Option<&'static str> {
82 match format {
83 "AWS::EC2::SecurityGroup.Id" => Some("E1150"),
84 "AWS::EC2::VPC.Id" => Some("E1151"),
85 "AWS::EC2::Image.Id" => Some("E1152"),
86 "AWS::EC2::SecurityGroup.Name" => Some("E1153"),
87 "AWS::EC2::Subnet.Id" => Some("E1154"),
88 "AWS::Logs::LogGroup.Name" => Some("E1155"),
89 "AWS::IAM::Role.Arn" => Some("E1156"),
90 "AWS::IAM::Role.Name" => Some("E1165"),
91 _ => None,
92 }
93}
94
95pub fn section_for_rule_id(rule_id: &str) -> Option<&'static str> {
99 match rule_id {
100 "F0040" | "F6005" | "F6101" | "F6004" | "F6011" | "I6011" | "I6010" | "E6003" => Some("Outputs"),
101 "W8001" | "E8001" => Some("Conditions"),
102 "F0008" | "F0050" | "W7001" | "F0017" | "E7001" | "F7002" | "I7002" | "I7010" => Some("Mappings"),
103 "F0009" => Some("Conditions"),
104 "F0003" | "F0015" | "F0016" | "F2012" | "W2506" | "W2509" | "W2001" | "E2001" | "F2002" | "F2003" | "F2011"
105 | "I2011" | "F2015" | "W2501" | "I2010" => Some("Parameters"),
106 "F0001" | "F0007" | "F0011" | "E0001" | "F0005" | "E1028" => Some("Resources"),
107 "F0002" => Some("AWSTemplateFormatVersion"),
108 "F1004" => Some("Description"),
109 "E1005" => Some("Transform"),
110 "F0004" => Some("Outputs"),
111 "F8600" | "F8601" | "W8602" | "F8603" | "F8604" | "F8605" | "F8606" | "F8607" | "W8608" | "F8609" | "F8610"
112 | "F8611" => Some("Rules"),
113 _ => None,
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::category::Category;
121 use crate::registry::RULE_REGISTRY;
122
123 #[test]
124 fn is_fatal_rule_returns_true_for_f_prefix() {
125 assert!(is_fatal_rule("F3002"));
126 assert!(is_fatal_rule("F0001"));
127 }
128
129 #[test]
130 fn is_fatal_rule_returns_false_for_non_f_prefixes() {
131 assert!(!is_fatal_rule("E3002"));
132 assert!(!is_fatal_rule("W1001"));
133 assert!(!is_fatal_rule("I3011"));
134 }
135
136 #[test]
137 fn is_valid_custom_rule_id_accepts_alphanumeric_and_separators() {
138 assert!(is_valid_custom_rule_id("CUSTOM001"));
140 assert!(is_valid_custom_rule_id("myRule1"));
141 assert!(is_valid_custom_rule_id("check_bucket_encryption"));
142 assert!(is_valid_custom_rule_id("s3.encryption.required"));
143 assert!(is_valid_custom_rule_id("my-rule-1"));
144 assert!(is_valid_custom_rule_id("Fluffy123"));
145 assert!(is_valid_custom_rule_id("_"));
146 }
147
148 #[test]
149 fn is_valid_custom_rule_id_rejects_empty_whitespace_and_punctuation() {
150 assert!(!is_valid_custom_rule_id(""));
151 assert!(!is_valid_custom_rule_id("my rule"));
152 assert!(!is_valid_custom_rule_id("rule/id"));
153 assert!(!is_valid_custom_rule_id("rule:id"));
154 assert!(!is_valid_custom_rule_id("rule#1"));
155 assert!(!is_valid_custom_rule_id("emoji😀"));
156 }
157
158 #[test]
159 fn rule_number_extracts_numeric_suffix_ignoring_prefix() {
160 assert_eq!(rule_number("F0001"), 1);
161 assert_eq!(rule_number("E3012"), 3012);
162 assert_eq!(rule_number("W9012"), 9012);
163 assert_eq!(rule_number("F3012"), rule_number("E3012"));
166 }
167
168 #[test]
169 fn rule_number_returns_max_for_ids_without_a_numeric_suffix() {
170 assert_eq!(rule_number("CUSTOM"), u32::MAX);
171 assert_eq!(rule_number(""), u32::MAX);
172 }
173
174 #[test]
175 fn category_for_rule_id_maps_two_char_prefixes_correctly() {
176 assert_eq!(category_for_rule_id("F0001"), Category::Structure);
177 assert_eq!(category_for_rule_id("F1010"), Category::Intrinsic);
178 assert_eq!(category_for_rule_id("E1040"), Category::Intrinsic);
179 assert_eq!(category_for_rule_id("E2010"), Category::Parameter);
180 assert_eq!(category_for_rule_id("E3012"), Category::Schema);
181 assert_eq!(category_for_rule_id("W2100"), Category::Security);
183 assert_eq!(category_for_rule_id("I3011"), Category::BestPractice);
184 }
185
186 #[test]
187 fn category_for_rule_id_applies_three_char_overrides() {
188 assert_eq!(category_for_rule_id("E2529"), Category::Resource);
189 assert_eq!(category_for_rule_id("E2504"), Category::Resource);
190 assert_eq!(category_for_rule_id("I2530"), Category::BestPractice);
191 assert_eq!(category_for_rule_id("F8600"), Category::Structure);
192 }
193
194 #[test]
195 fn category_for_rule_id_defaults_to_resource_for_unknown_prefix() {
196 assert_eq!(category_for_rule_id("Z9999"), Category::Resource);
197 }
198
199 #[test]
200 fn section_for_rule_id_maps_output_rules_to_outputs() {
201 assert_eq!(section_for_rule_id("F0040"), Some("Outputs"));
202 assert_eq!(section_for_rule_id("F0004"), Some("Outputs"));
203 }
204
205 #[test]
206 fn section_for_rule_id_maps_parameter_rules_to_parameters() {
207 assert_eq!(section_for_rule_id("F0003"), Some("Parameters"));
208 assert_eq!(section_for_rule_id("W2001"), Some("Parameters"));
209 }
210
211 #[test]
212 fn section_for_rule_id_maps_condition_rules_to_conditions() {
213 assert_eq!(section_for_rule_id("F0009"), Some("Conditions"));
214 assert_eq!(section_for_rule_id("W8001"), Some("Conditions"));
215 }
216
217 #[test]
218 fn section_for_rule_id_maps_rules_section_rules() {
219 assert_eq!(section_for_rule_id("F8600"), Some("Rules"));
220 }
221
222 #[test]
223 fn section_for_rule_id_returns_none_for_unmapped_rule() {
224 assert_eq!(section_for_rule_id("Z9999"), None);
225 }
226
227 #[test]
228 fn format_rule_for_format_maps_all_known_formats() {
229 assert_eq!(format_rule_for_format("AWS::EC2::Image.Id"), Some("E1152"));
230 assert_eq!(format_rule_for_format("AWS::EC2::SecurityGroup.Id"), Some("E1150"));
231 assert_eq!(format_rule_for_format("AWS::EC2::VPC.Id"), Some("E1151"));
232 assert_eq!(format_rule_for_format("AWS::EC2::Subnet.Id"), Some("E1154"));
233 assert_eq!(format_rule_for_format("AWS::Logs::LogGroup.Name"), Some("E1155"));
234 assert_eq!(format_rule_for_format("AWS::IAM::Role.Arn"), Some("E1156"));
235 assert_eq!(format_rule_for_format("AWS::IAM::Role.Name"), Some("E1165"));
236 }
237
238 #[test]
239 fn format_rule_for_format_returns_none_for_unknown_format() {
240 assert_eq!(format_rule_for_format("unknown-format"), None);
241 }
242
243 #[test]
244 fn category_heuristic_diverges_from_registry_for_some_rules() {
245 let mut divergences = 0;
246 for rule in RULE_REGISTRY {
247 if category_for_rule_id(rule.id) != rule.category {
248 divergences += 1;
249 }
250 }
251 assert!(divergences > 0, "If all agree, the helper covers everything - great!");
255 }
256}