cfn_guard/commands/
rulegen.rs1use std::fs;
2use std::process;
3
4use crate::commands::Executable;
5use crate::commands::SUCCESS_STATUS_CODE;
6use crate::rules::Result;
7use crate::utils::reader::Reader;
8use crate::utils::writer::Writer;
9use clap::Args;
10use itertools::Itertools;
11use serde_json::Value;
12use std::collections::{HashMap, HashSet};
13use std::io::Write;
14use string_builder::Builder;
15
16const ABOUT: &str = "Autogenerate rules from an existing JSON- or YAML- formatted data. (Currently works with only CloudFormation templates)";
17const TEMPLATE_HELP: &str = "Provide path to a CloudFormation template file in JSON or YAML";
18const OUTPUT_HELP: &str = "Write to output file";
19
20#[derive(Debug, Clone, Eq, PartialEq, Args)]
21#[clap(arg_required_else_help = true)]
22#[clap(about=ABOUT)]
23pub struct Rulegen {
27 #[arg(short, long, help=OUTPUT_HELP)]
31 pub(crate) output: Option<String>,
32 #[arg(short, long, help=TEMPLATE_HELP)]
34 pub(crate) template: String,
35}
36
37impl Executable for Rulegen {
38 fn execute(&self, writer: &mut Writer, _: &mut Reader) -> Result<i32> {
45 let template_contents = fs::read_to_string(&self.template)?;
46
47 let result = parse_template_and_call_gen(&template_contents, writer);
48 print_rules(result, writer)?;
49
50 Ok(SUCCESS_STATUS_CODE)
51 }
52}
53
54pub fn parse_template_and_call_gen(
55 template_contents: &str,
56 writer: &mut Writer,
57) -> HashMap<String, HashMap<String, HashSet<String>>> {
58 let cfn_template: HashMap<String, Value> = match serde_yaml::from_str(template_contents) {
59 Ok(s) => s,
60 Err(e) => {
61 writer
62 .write_err(format!("Parsing error handling template file, Error = {e}"))
63 .expect("failed to write to stderr");
64 process::exit(1);
65 }
66 };
67
68 let cfn_resources_clone = match cfn_template.get("Resources") {
69 Some(y) => y.clone(),
70 None => {
71 writer
72 .write_err(String::from("Template lacks a Resources section"))
73 .expect("failed to write to stderr");
74 process::exit(1);
75 }
76 };
77
78 let cfn_resources: HashMap<String, Value> = match serde_json::from_value(cfn_resources_clone) {
79 Ok(y) => y,
80 Err(e) => {
81 writer
82 .write_err(format!(
83 "Template Resources section has an invalid structure: {e}"
84 ))
85 .expect("failed to write to stderr");
86 process::exit(1);
87 }
88 };
89
90 gen_rules(cfn_resources)
91}
92
93#[allow(clippy::map_entry)]
94fn gen_rules(
95 cfn_resources: HashMap<String, Value>,
96) -> HashMap<String, HashMap<String, HashSet<String>>> {
97 let mut rule_map: HashMap<String, HashMap<String, HashSet<String>>> = HashMap::new();
130 for (_name, cfn_resource) in cfn_resources {
131 let props: HashMap<String, Value> =
132 match serde_json::from_value(cfn_resource["Properties"].clone()) {
133 Ok(s) => s,
134 Err(_) => continue,
135 };
136
137 for (prop_name, prop_val) in props {
138 let stripped_val = match prop_val.as_str() {
139 Some(v) => String::from(v),
140 None => prop_val.to_string(),
141 };
142
143 let mut no_newline_stripped_val = stripped_val.trim().replace('\n', "");
144
145 if prop_val.is_string() {
147 let test_str = format!("{}{}{}", "\"", no_newline_stripped_val, "\"");
148 no_newline_stripped_val = test_str;
149 }
150 let resource_name = (&cfn_resource["Type"].as_str().unwrap()).to_string();
151
152 if !rule_map.contains_key(&resource_name) {
153 let value_set: HashSet<String> =
154 vec![no_newline_stripped_val].into_iter().collect();
155
156 let mut property_map = HashMap::new();
157 property_map.insert(prop_name, value_set);
158 rule_map.insert(resource_name, property_map);
159 } else {
160 let property_map = rule_map.get_mut(&resource_name).unwrap();
161
162 if !property_map.contains_key(&prop_name) {
163 let value_set: HashSet<String> =
164 vec![no_newline_stripped_val].into_iter().collect();
165 property_map.insert(prop_name, value_set);
166 } else {
167 let value_set = property_map.get_mut(&prop_name).unwrap();
168 value_set.insert(no_newline_stripped_val);
169 }
170 };
171 }
172 }
173
174 rule_map
175}
176
177fn print_rules(
188 rule_map: HashMap<String, HashMap<String, HashSet<String>>>,
189 writer: &mut Writer,
190) -> Result<()> {
191 let mut str = Builder::default();
192
193 for (resource, properties) in &rule_map {
194 let resource_name_underscore = resource.replace("::", "_").to_lowercase();
195 let variable_name = format!("{}_resources", resource_name_underscore);
196
197 str.append(format!(
198 "let {} = Resources.*[ Type == '{}' ]\n",
199 variable_name, resource
200 ));
201 str.append(format!(
202 "rule {} when %{} !empty {{\n",
203 resource_name_underscore, variable_name
204 ));
205
206 for (property, values) in properties {
207 if values.len() > 1 {
208 str.append(format!(
209 " %{}.Properties.{} IN [{}]\n",
210 variable_name,
211 property,
212 values.iter().join(", ")
213 ));
214 } else {
215 str.append(format!(
216 " %{}.Properties.{} == {}\n",
217 variable_name,
218 property,
219 values.iter().next().unwrap()
220 ));
221 }
222 }
223
224 str.append("}\n");
225 }
226
227 let generated_rules = str.string().unwrap();
229
230 let span = crate::rules::parser::Span::new_extra(&generated_rules, "");
231 match crate::rules::parser::rules_file(span) {
232 Ok(_rules) => {
233 write!(writer, "{}", generated_rules)?;
237 }
238 Err(e) => {
239 writer.write_err(format!(
240 "Parsing error with generated rules file, Error = {e}"
241 ))?;
242 }
243 }
244 Ok(())
245}
246
247#[cfg(test)]
248#[path = "rulegen_tests.rs"]
249mod rulegen_tests;