cfn_guard/
util.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{enums, structs};
5use lazy_static::lazy_static;
6use log::{self, error, trace};
7use regex::{Captures, Regex};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::process;
11
12// This sets it up so the regex only gets compiled once
13// See: https://docs.rs/regex/1.3.9/regex/#example-avoid-compiling-the-same-regex-in-a-loop
14lazy_static! {
15    static ref STRINGIFIED_BOOLS: Regex =
16        Regex::new(r"[:=]\s*([fF]alse|[tT]rue)\s*([,}]+|$)").unwrap();
17}
18pub fn fix_stringified_bools(fstr: &str) -> String {
19    let after = STRINGIFIED_BOOLS.replace_all(fstr, |caps: &Captures| caps[0].to_lowercase());
20    after.to_string()
21}
22
23pub fn format_value(v: &Value) -> String {
24    let formatted_value = if v.is_string() {
25        //This is necessary to remove extraneous quotes when converting a string
26        strip_ws_nl(String::from(v.as_str().unwrap()))
27    } else {
28        //Quotes not added for non-String SerDe values
29        strip_ws_nl(v.to_string())
30    };
31    trace!("formatted_value is '{}'", formatted_value);
32    formatted_value
33}
34
35pub fn strip_ws_nl(v: String) -> String {
36    trace!("Removing spaces and newline characters from '{}'", &v);
37    v.replace("\n", "").replace(" ", "")
38}
39
40pub fn convert_list_var_to_vec(rule_val: &str) -> Vec<String> {
41    let mut value_vec: Vec<String> = vec![];
42    match serde_json::from_str(rule_val) {
43        Ok(v) => {
44            trace!("List {} is a json list", rule_val);
45            let val: Value = v;
46            match val.as_array() {
47                Some(vv) => {
48                    for vvv in vv {
49                        let val_string = vvv.to_string();
50                        let list_val = val_string.trim_start_matches('"').trim_end_matches('"');
51                        value_vec.push(String::from(list_val))
52                    }
53                }
54                None => value_vec.push(val.to_string()),
55            }
56        }
57        Err(_) => {
58            trace!("List {} is not a json list", rule_val);
59            let value_string: String = rule_val
60                .trim_start_matches('[')
61                .trim_end_matches(']')
62                .replace(" ", "");
63
64            for vs in value_string.split(',') {
65                value_vec.push(String::from(vs));
66            }
67        }
68    };
69
70    trace!("Rule value_vec is {:?}", &value_vec);
71    value_vec
72}
73
74fn match_props<'a>(props: &'a Value, n: &'a dyn serde_json::value::Index) -> Result<&'a Value, ()> {
75    trace!("props are {:#?}", props);
76
77    match props.get(n) {
78        Some(v) => Ok(v),
79        None => Err(()),
80    }
81}
82
83pub fn get_resource_prop_value(props: &Value, field: &[&str]) -> Result<Value, String> {
84    trace!("Getting {:?} from {}", &field, &props);
85    let mut field_list = field.to_owned();
86    trace!("field_list is {:?}", field_list);
87    if field_list.is_empty() {
88        return Ok(props.clone());
89    }
90    let next_field = field_list.remove(0);
91    match next_field {
92        "" => return Ok(props.clone()),
93        "." => get_resource_prop_value(&props, &field_list),
94        _ => match next_field.parse::<usize>() {
95            Ok(n) => {
96                trace!(
97                    "next_field is {:?} and field_list is now {:?}",
98                    &n,
99                    &field_list
100                );
101
102                match match_props(props, &n) {
103                    Ok(v) => {
104                        if !field_list.is_empty() {
105                            get_resource_prop_value(&v, &field_list)
106                        } else {
107                            Ok(v.clone())
108                        }
109                    }
110                    Err(_) => destringify_json(props, &n, &mut field_list),
111                }
112            }
113            Err(_) => {
114                trace!(
115                    "next_field is {:?} and field_list is now {:?}",
116                    &next_field,
117                    &field_list
118                );
119                match match_props(props, &next_field) {
120                    Ok(v) => {
121                        if !field_list.is_empty() {
122                            get_resource_prop_value(&v, &field_list)
123                        } else {
124                            Ok(v.clone())
125                        }
126                    }
127                    Err(_) => destringify_json(props, &next_field, &mut field_list),
128                }
129            }
130        },
131    }
132}
133
134fn destringify_json<'a>(
135    props: &'a Value,
136    field: &'a dyn serde_json::value::Index,
137    field_list: &'a mut Vec<&str>,
138) -> Result<Value, String> {
139    match props.as_str() {
140        Some(p) => match serde_json::from_str::<Value>(p) {
141            Ok(s) => {
142                trace!("sub structure is {:#?}", s);
143                // trace!("next_field is {}", field);
144                match match_props(&s, field) {
145                    Ok(v) => {
146                        trace!("next_props is {:#?}", v);
147                        get_resource_prop_value(&v, &field_list)
148                    }
149                    Err(_) => return Err(format!("Invalid address")),
150                }
151            }
152            Err(e) => return Err(e.to_string()),
153        },
154        None => return Err(format!("Could not convert properties to string")),
155    }
156}
157
158pub fn filter_for_env_vars(vars: &HashMap<String, String>) -> HashMap<String, String> {
159    let mut filtered_map: HashMap<String, String> = HashMap::new();
160    for (k, v) in vars.iter() {
161        if !k.starts_with("ENV") {
162            filtered_map.insert(k.to_string(), v.to_string());
163        } else {
164            filtered_map.insert(k.to_string(), format!("********"));
165        }
166    }
167    filtered_map
168}
169
170pub fn deref_rule_value<'a>(
171    rule: &'a structs::Rule,
172    vars: &'a HashMap<String, String>,
173) -> Result<&'a str, String> {
174    let filtered_env_vars = filter_for_env_vars(vars);
175    trace!(
176        "Entered dereference_rule_value() with '{:#?}' and Variables '{:#?}'",
177        rule,
178        filtered_env_vars
179    );
180    match rule.rule_vtype {
181        enums::RValueType::Variable => {
182            let target_value: &str = rule.value.split('%').collect::<Vec<&str>>()[1];
183            let first_char = target_value.chars().collect::<Vec<char>>()[0];
184            let final_target = match first_char {
185                // Environment variable lookup
186                '{' => format!(
187                    "ENV_{}",
188                    target_value.trim_start_matches('{').trim_end_matches('}')
189                ),
190                _ => target_value.to_string(),
191            };
192            trace!(
193                "Dereferencing variable {:?} in '{:#?}'",
194                final_target,
195                filtered_env_vars
196            );
197            match &vars.get(&final_target) {
198                Some(v) => Ok(v),
199                None => {
200                    error!(
201                        "Undefined Variable: [{}] does not exist in {:#?}",
202                        final_target, &filtered_env_vars
203                    );
204                    Err(format!(
205                        "[{}] does not exist in {:#?}",
206                        rule.value, &filtered_env_vars
207                    ))
208                }
209            }
210        }
211        _ => Ok(&rule.value),
212    }
213}
214
215pub fn expand_wildcard_props(
216    props: &Value,
217    address: String,
218    accumulator: String,
219) -> Option<Vec<String>> {
220    trace!(
221        "Entering expand_wildcard_props() with props: {:#?} , address: {:#?} , accumulator: {:#?}",
222        &props,
223        &address,
224        &accumulator
225    );
226    let mut segments = address.split('*').collect::<Vec<&str>>();
227    trace!("Segments are {:#?}", &segments);
228    let segment = segments.remove(0);
229    trace!("Processing segment {:#?}", &segment);
230    if segments.len() > 0 {
231        let mut expanded_props: Vec<String> = vec![];
232        let s = segment.trim_end_matches('.').trim_start_matches('.');
233        let steps = s.split('.').collect::<Vec<&str>>();
234        match get_resource_prop_value(props, &steps) {
235            Ok(v) => match v.as_array() {
236                Some(result_array) => {
237                    trace!("Value is an array");
238                    for (counter, r) in result_array.iter().enumerate() {
239                        trace!("Counter is {:#?}", counter);
240                        let next_segment = segments.join("*");
241                        trace!("next_segment is '{:#?}'", &next_segment);
242                        let temp_address = format!("{}{}{}", accumulator, segment, counter);
243                        trace!("temp_address is {:#?}", &temp_address);
244                        match expand_wildcard_props(&r, next_segment, temp_address) {
245                            Some(result) => expanded_props.append(&mut result.clone()),
246                            None => return None,
247                        }
248                    }
249                }
250                None => match v.as_object() {
251                    Some(result_object) => {
252                        trace!("Value is an object");
253                        for (k, v) in result_object.iter() {
254                            trace!("Key is '{}'", k);
255                            let next_segment = segments.join("*");
256                            trace!("next_segment is {:#?}", next_segment);
257                            let temp_address = format!("{}{}{}", accumulator, segment, k);
258                            trace!("temp_address is {:#?}", &temp_address);
259                            match expand_wildcard_props(&v, next_segment, temp_address) {
260                                Some(result) => expanded_props.append(&mut result.clone()),
261                                None => return None,
262                            }
263                        }
264                    }
265                    None => expanded_props.push(format!("{}{}", accumulator, segment)),
266                },
267            },
268            Err(_) => return None,
269        }
270        Some(expanded_props)
271    } else {
272        trace!("Final segment");
273        let accumulated_address = format!("{}{}", accumulator, segment);
274        trace!("Accumulated address: {}", accumulated_address);
275        Some(vec![accumulated_address])
276    }
277}
278
279// TODO: Move all in-proc exits to clean_exit()
280// pub fn clean_exit(msg_string: String) {
281//     error!("{}", &msg_string);
282//     process::exit(1)
283// }
284
285// TODO: Collapse the format_val.. fn's to a generic
286pub fn parse_value_as_float(val: &Value) -> f32 {
287    trace!("Formatting {} as float", val);
288    match format_value(val).parse::<f32>() {
289        Ok(s) => s,
290        Err(_) => {
291            let msg_string = format!("Value cannot be parsed as a float: {}", val);
292            error!("{}", &msg_string);
293            process::exit(1)
294        }
295    }
296}
297pub fn parse_str_as_float(val: &str) -> f32 {
298    trace!("Formatting {} as float", val);
299    match strip_ws_nl(val.to_string()).parse::<f32>() {
300        Ok(s) => s,
301        Err(_) => {
302            let msg_string = format!("String cannot be parsed as a float: {}", val);
303            error!("{}", &msg_string);
304            process::exit(1)
305        }
306    }
307}
308mod tests {
309    #[cfg(test)]
310    use crate::util::expand_wildcard_props;
311    #[cfg(test)]
312    use std::collections::HashMap;
313
314    #[test]
315    fn test_wildcard_expansion() {
316        let iam_template: &'static str = r#"
317Resources:
318  LambdaRoleHelper:
319    Type: 'AWS::IAM::Role'
320    Properties:
321      AssumeRolePolicyDocument:
322        Version: 2012-10-17
323        Statement:
324          - Effect: Allow
325            Principal:
326              Service:
327                - ec2.amazonaws.com
328                - lambda.amazonaws.com
329            Action:
330              - 'sts:AssumeRole'
331          - Effect: Allow
332            Principal:
333              Service:
334                - lambda.amazonaws.com
335                - ec2.amazonaws.com
336            Action:
337              - 'sts:AssumeRole'
338          - Effect: Allow
339            Principal:
340              Service:
341                - lambda.amazonaws.com
342                - ec2.amazonaws.com
343            Action:
344              - 'sts:AssumeRole'
345"#;
346        let cfn_template: HashMap<String, serde_json::Value> =
347            serde_yaml::from_str(&iam_template).unwrap();
348        let mut wildcard = String::from("AssumeRolePolicyDocument.Statement.*.Effect");
349        let root = &cfn_template["Resources"]["LambdaRoleHelper"]["Properties"];
350        let mut expanded_wildcards =
351            expand_wildcard_props(&root, wildcard, String::from("")).unwrap();
352        assert_eq!(
353            expanded_wildcards,
354            vec![
355                String::from("AssumeRolePolicyDocument.Statement.0.Effect"),
356                String::from("AssumeRolePolicyDocument.Statement.1.Effect"),
357                String::from("AssumeRolePolicyDocument.Statement.2.Effect"),
358            ]
359        );
360        wildcard = String::from("AssumeRolePolicyDocument.Statement.*.Action.*");
361        expanded_wildcards = expand_wildcard_props(&root, wildcard, String::from("")).unwrap();
362        assert_eq!(
363            expanded_wildcards,
364            vec![
365                String::from("AssumeRolePolicyDocument.Statement.0.Action.0"),
366                String::from("AssumeRolePolicyDocument.Statement.1.Action.0"),
367                String::from("AssumeRolePolicyDocument.Statement.2.Action.0"),
368            ]
369        );
370    }
371}