1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use clap::{Arg, ArgMatches};

use crate::command::Command;
use crate::commands::files::read_file_content;
use crate::commands::{MIGRATE, OUTPUT, RULES};
use crate::migrate::parser::{parse_rules_file, Clause, Rule, RuleLineType, TypeName};
use crate::rules::errors::Error;
use crate::rules::Result;
use crate::utils::reader::Reader;
use crate::utils::writer::Writer;
use itertools::Itertools;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::str::FromStr;

#[cfg(test)]
#[path = "migrate_tests.rs"]
mod migrate_tests;

#[derive(Clone, Copy, Eq, PartialEq)]
pub struct Migrate {}

#[allow(clippy::new_without_default)]
impl Migrate {
    pub fn new() -> Self {
        Migrate {}
    }
}

impl Command for Migrate {
    fn name(&self) -> &'static str {
        MIGRATE
    }

    fn command(&self) -> clap::Command {
        clap::Command::new(MIGRATE)
            .about("Migrates 1.0 rules to 2.0 compatible rules.")
            .arg(
                Arg::new(RULES.0)
                    .long(RULES.0)
                    .short(RULES.1)
                    .help("Provide a rules file")
                    .required(true),
            )
            .arg(
                Arg::new(OUTPUT.0)
                    .long(OUTPUT.0)
                    .short(OUTPUT.1)
                    .help("Write migrated rules to output file")
                    .required(false),
            )
            .arg_required_else_help(true)
    }

    fn execute(&self, app: &ArgMatches, writer: &mut Writer, _: &mut Reader) -> Result<i32> {
        let file_input = app.get_one::<String>(RULES.0).unwrap(); // safe because this flag is required

        let path = PathBuf::from_str(file_input)?;
        let file_name = path.to_str().unwrap_or("").to_string();
        let file = File::open(file_input)?;

        match read_file_content(file) {
            Err(e) => {
                writer.write_err(format!("Unable read content from file {e}"))?;
                Err(Error::IoError(e))
            }
            Ok(file_content) => match parse_rules_file(&file_content, &file_name) {
                Err(e) => {
                    writer.write_err(format!("Could not parse 1.0 rule file: {file_name}. Please ensure the file is valid with the old version of the tool and try again."))?;
                    Err(e)
                }
                Ok(rules) => {
                    let migrated_rules = migrate_rules(rules)?;
                    let span = crate::rules::parser::Span::new_extra(&migrated_rules, "");
                    match crate::rules::parser::rules_file(span) {
                        Ok(_rules) => {
                            write!(writer, "{migrated_rules}")?;
                            Ok(0_i32)
                        }
                        Err(e) => {
                            writer.write_err(format!(
                                "Could not parse migrated ruleset for file: '{file_name}': {e}"
                            ))?;
                            Err(e)
                        }
                    }
                }
            },
        }
    }
}

#[allow(clippy::uninlined_format_args)]
pub(crate) fn migrated_rules_by_type(
    rules: &[RuleLineType],
    by_type: &HashMap<TypeName, indexmap::IndexSet<&Clause>>,
) -> Result<String> {
    let mut migrated = String::new();
    for rule in rules {
        if let RuleLineType::Assignment(assignment) = rule {
            writeln!(&mut migrated, "{}", assignment)?;
        }
    }

    let mut types = by_type.keys().cloned().collect_vec();
    types.sort();
    for each_type in &types {
        writeln!(
            &mut migrated,
            "let {} = Resources.*[ Type == \"{}\" ]",
            each_type, each_type.type_name
        )?;
        writeln!(
            &mut migrated,
            "rule {name}_checks WHEN %{name} NOT EMPTY {{",
            name = each_type
        )?;
        writeln!(&mut migrated, "    %{} {{", each_type)?;
        for each_clause in by_type.get(each_type).unwrap() {
            writeln!(&mut migrated, "        {}", *each_clause)?;
        }
        writeln!(&mut migrated, "    }}\n}}\n")?;
    }
    Ok(migrated)
}

pub(crate) fn aggregate_by_type(
    rules: &Vec<RuleLineType>,
) -> HashMap<TypeName, indexmap::IndexSet<&Clause>> {
    let mut by_type = HashMap::with_capacity(rules.len());
    for rule in rules {
        if let RuleLineType::Clause(clause) = rule {
            for each in &clause.rules {
                match each {
                    Rule::Basic(br) => {
                        by_type
                            .entry(br.type_name.clone())
                            .or_insert(indexmap::IndexSet::new())
                            .insert(clause);
                    }
                    Rule::Conditional(br) => {
                        by_type
                            .entry(br.type_name.clone())
                            .or_insert(indexmap::IndexSet::new())
                            .insert(clause);
                    }
                }
            }
        }
    }
    by_type
}

#[allow(dead_code)] // not actually dead code, its used in tests
pub(crate) fn get_resource_types_in_ruleset(rules: &Vec<RuleLineType>) -> Result<Vec<TypeName>> {
    let mut resource_types = HashSet::new();
    for rule in rules {
        if let RuleLineType::Clause(clause) = rule.clone() {
            clause.rules.into_iter().for_each(|rule| match rule {
                Rule::Basic(basic_rule) => {
                    resource_types.insert(basic_rule.type_name);
                }
                Rule::Conditional(conditional_rule) => {
                    resource_types.insert(conditional_rule.type_name);
                }
            });
        }
    }
    let mut resource_types_list = resource_types.into_iter().collect::<Vec<_>>();
    resource_types_list.sort();
    Ok(resource_types_list)
}

pub(crate) fn migrate_rules(rules: Vec<RuleLineType>) -> Result<String> {
    let aggr_by_type = aggregate_by_type(&rules);
    let migrated_rules = migrated_rules_by_type(&rules, &aggr_by_type)?;

    Ok(migrated_rules)
}