use super::super::machine::Machine;
use super::super::problem::{Code, FieldPath, Problem, Problems};
use super::super::step::{FirewallPort, Step, has_extension};
use super::port_reference;
pub(super) fn check(at: &FieldPath, m: &Machine, step: &Step, out: &mut Problems) {
let windows = m.os.is_some_and(super::super::os::OsRef::is_windows);
if windows {
windows_step(at, step, out);
} else {
linux_step(at, m, step, out);
}
if let Step::Firewall(f) = step {
for (i, port) in f.allow.iter().enumerate() {
if let FirewallPort::Named(name) = port
&& !m.ports.contains_key(name)
{
out.push(port_reference(&at.key("firewall").key("allow").index(i), name.as_str(), m));
}
}
}
}
fn windows_step(at: &FieldPath, step: &Step, out: &mut Problems) {
match step {
Step::Run { script, .. } if !has_extension(script.as_str(), "ps1") => {
out.push(Problem::new(
Code::InvalidValue,
at.key("run"),
format!("`{script}`: a Windows machine runs PowerShell scripts (`.ps1`)"),
));
}
Step::Playbook(p) => {
out.push(
Problem::new(
Code::InvalidValue,
at.key("playbook"),
format!("`{p}`: a playbook runs on a Linux machine; a Windows machine's steps are PowerShell scripts"),
)
.fix("`{ run: <script>.ps1 }`"),
);
}
Step::Reboot => {
out.push(Problem::new(
Code::InvalidValue,
at.clone(),
"a Windows machine restarts from its own script, not a `reboot` step",
));
}
_ => {}
}
}
fn linux_step(at: &FieldPath, m: &Machine, step: &Step, out: &mut Problems) {
match step {
Step::Run { script, .. } if !has_extension(script.as_str(), "sh") => {
out.push(
Problem::new(Code::InvalidValue, at.key("run"), format!("`{script}`: a step runs a shell script (`.sh`)"))
.fix("a playbook is `{ playbook: <file>.yml }`"),
);
}
Step::Playbook(p) if !has_extension(p.as_str(), "yml") && !has_extension(p.as_str(), "yaml") => {
out.push(Problem::new(
Code::InvalidValue,
at.key("playbook"),
format!("`{p}`: a playbook is a `.yml` file"),
));
}
Step::Repository(r) if !apt(m) => {
let field = match (&r.suite, &r.components) {
(Some(_), _) => Some("suite"),
(None, Some(_)) => Some("components"),
(None, None) => None,
};
if let Some(field) = field {
let os = m.os.map(|o| o.to_string()).unwrap_or_default();
out.push(
Problem::new(
Code::InvalidValue,
at.key("repository").key(field),
format!("`{field}` is apt's, and `{os}` doesn't install with apt"),
)
.fix(format!("leave `{field}` out")),
);
}
}
Step::User(u) => account(&at.key("user").key("name"), u.name.as_str(), out),
Step::Group(g) => account(&at.key("group").key("name"), g.name.as_str(), out),
_ => {}
}
}
fn apt(m: &Machine) -> bool {
m.os.is_some_and(|o| matches!(o.os().family, "debian" | "ubuntu" | "kali"))
}
fn account(at: &FieldPath, name: &str, out: &mut Problems) {
let ok = name.starts_with(|c: char| c.is_ascii_lowercase() || c == '_')
&& name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || "_.-".contains(c));
if !ok {
out.push(
Problem::new(
Code::InvalidValue,
at.clone(),
format!("`{name}`: a Linux account's name is lowercase letters, digits and `_.-`, starting with a letter or `_`"),
)
.fix(format!("`{}`", name.to_ascii_lowercase().replace(' ', "-"))),
);
}
}