use crate::{
constants::{default_node_count, MINIMUM_NODE_COUNT},
helpers::Connection,
Formalize,
};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]
pub struct InfraNode {
#[serde(default, alias = "Name", alias = "NAME")]
pub name: Option<String>,
#[serde(default = "default_node_count", alias = "Count", alias = "COUNT")]
pub count: i32,
#[serde(default, alias = "Links", alias = "LINKS")]
pub links: Option<Vec<String>>,
#[serde(default, alias = "Dependencies", alias = "DEPENDENCIES")]
pub dependencies: Option<Vec<String>>,
#[serde(alias = "Description", alias = "DESCRIPTION")]
pub description: Option<String>,
}
impl InfraNode {
pub fn new(potential_count: Option<i32>) -> Self {
Self {
count: match potential_count {
Some(count) => count,
None => default_node_count(),
},
..Default::default()
}
}
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum HelperNode {
Empty,
Short(i32),
Long(InfraNode),
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum InfrastructureHelper {
InfrastructureHelper(HashMap<String, HelperNode>),
}
pub type Infrastructure = HashMap<String, InfraNode>;
impl Formalize for InfraNode {
fn formalize(&mut self) -> Result<()> {
if self.count < MINIMUM_NODE_COUNT {
return Err(anyhow!(
"Infrastructure Count field can not be less than {MINIMUM_NODE_COUNT}"
));
}
Ok(())
}
}
impl From<HelperNode> for InfraNode {
fn from(helper_node: HelperNode) -> Self {
match helper_node {
HelperNode::Short(value) => InfraNode::new(Some(value)),
HelperNode::Long(infranode) => infranode,
HelperNode::Empty => InfraNode::new(None),
}
}
}
impl From<InfrastructureHelper> for Infrastructure {
fn from(helper_infrastructure: InfrastructureHelper) -> Self {
match helper_infrastructure {
InfrastructureHelper::InfrastructureHelper(helper_infrastructure) => {
helper_infrastructure
.iter()
.map(|(node_name, helper_node)| {
let infra_node: InfraNode = helper_node.clone().into();
(node_name.to_owned(), infra_node)
})
.collect::<Infrastructure>()
}
}
}
}
impl Connection<Infrastructure> for &String {
fn validate_connections(&self, potential_node_names: &Option<Vec<String>>) -> Result<()> {
if let Some(node_names) = potential_node_names {
if !node_names.contains(self) {
return Err(anyhow!(
"Infrastructure entry {self} does not exist under Nodes"
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_sdl;
#[test]
fn infranode_count_longhand_is_parsed() {
let sdl = r#"
count: 23
"#;
let infra_node = serde_yaml::from_str::<InfraNode>(sdl).unwrap();
insta::assert_debug_snapshot!(infra_node);
}
#[test]
fn infranode_count_shorthand_is_parsed() {
let sdl = r#"
23
"#;
let infra_node: InfraNode = serde_yaml::from_str::<HelperNode>(sdl).unwrap().into();
insta::assert_debug_snapshot!(infra_node);
}
#[test]
fn infranode_with_links_and_dependencies_is_parsed() {
let sdl = r#"
count: 25
links:
- switch-2
dependencies:
- windows-10
- windows-10-vuln-1
"#;
let infra_node = serde_yaml::from_str::<InfraNode>(sdl).unwrap();
insta::assert_debug_snapshot!(infra_node);
}
#[test]
fn infranode_with_default_count_is_parsed() {
let sdl = r#"
links:
- switch-1
dependencies:
- windows-10
- windows-10-vuln-1
"#;
let infra_node = serde_yaml::from_str::<InfraNode>(sdl).unwrap();
insta::assert_debug_snapshot!(infra_node);
}
#[test]
fn simple_infrastructure_is_parsed() {
let sdl = r#"
windows-10-vuln-1:
count: 10
description: "A vulnerable Windows 10 machine"
debian-2:
count: 4
description: "A Debian server"
"#;
let infrastructure = serde_yaml::from_str::<Infrastructure>(sdl).unwrap();
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(infrastructure);
});
}
#[test]
fn simple_infrastructure_with_shorthand_is_parsed() {
let sdl = r#"
windows-10-vuln-2:
count: 10
windows-10-vuln-1: 10
ubuntu-10: 5
"#;
let infrastructure_helper = serde_yaml::from_str::<InfrastructureHelper>(sdl).unwrap();
let infrastructure: Infrastructure = infrastructure_helper.into();
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(infrastructure);
});
}
#[test]
fn bigger_infrastructure_is_parsed() {
let sdl = r#"
switch-1: 1
windows-10: 3
windows-10-vuln-1:
count: 1
switch-2:
count: 2
links:
- switch-1
ubuntu-10:
links:
- switch-1
dependencies:
- windows-10
- windows-10-vuln-1
"#;
let infrastructure_helper = serde_yaml::from_str::<InfrastructureHelper>(sdl).unwrap();
let infrastructure: Infrastructure = infrastructure_helper.into();
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(infrastructure);
});
}
#[test]
fn sdl_keys_are_valid_in_lowercase_uppercase_capitalized() {
let sdl = r#"
switch-1: 1
windows-10: 3
windows-10-vuln-1:
Count: 1
DEPENDENCIES:
- windows-10
switch-2:
COUNT: 2
Links:
- switch-1
ubuntu-10:
LINKS:
- switch-1
Dependencies:
- windows-10
- windows-10-vuln-1
"#;
let infrastructure_helper = serde_yaml::from_str::<InfrastructureHelper>(sdl).unwrap();
let infrastructure: Infrastructure = infrastructure_helper.into();
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(infrastructure);
});
}
#[test]
fn empty_count_is_allowed() {
let sdl = r#"
switch-1:
"#;
serde_yaml::from_str::<InfrastructureHelper>(sdl).unwrap();
}
#[should_panic(expected = "Infrastructure Count field can not be less than 1")]
#[test]
fn infranode_with_negative_count_is_rejected() {
let sdl = r#"
name: test-scenario
description: some-description
nodes:
win-10:
type: VM
resources:
ram: 2 gib
cpu: 2
source: windows10
infrastructure:
win-10: -1
"#;
parse_sdl(sdl).unwrap();
}
#[should_panic(expected = "Infrastructure entry debian does not exist under Nodes")]
#[test]
fn infranode_with_unknown_name_is_rejected() {
let sdl = r#"
name: test-scenario
description: some-description
nodes:
win-10:
type: VM
resources:
ram: 2 gib
cpu: 2
source: windows10
infrastructure:
debian: 1
"#;
parse_sdl(sdl).unwrap();
}
#[should_panic(
expected = "Infrastructure entry \"main-switch\" does not exist under Infrastructure even though it is a dependency for \"win-10\""
)]
#[test]
fn error_on_missing_infrastructure_link() {
let sdl = r#"
name: test-scenario
description: some-description
nodes:
win-10:
type: VM
resources:
ram: 2 gib
cpu: 2
source: windows10
main-switch:
type: Switch
infrastructure:
win-10:
count: 1
links:
- main-switch
"#;
parse_sdl(sdl).unwrap();
}
#[should_panic(
expected = "Infrastructure entry \"router\" does not exist under Infrastructure even though it is a dependency for \"win-10\""
)]
#[test]
fn error_on_missing_infrastructure_dependency() {
let sdl = r#"
name: test-scenario
description: some-description
nodes:
win-10:
type: VM
resources:
ram: 2 gib
cpu: 2
source: windows10
router:
type: VM
resources:
ram: 2 gib
cpu: 2
source: debian11
infrastructure:
win-10:
count: 1
dependencies:
- router
"#;
parse_sdl(sdl).unwrap();
}
}