use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{
helpers::Connection, training_learning_objective::TrainingLearningObjective, Formalize,
};
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct Goal {
#[serde(alias = "Name", alias = "NAME")]
pub name: Option<String>,
#[serde(alias = "Description", alias = "DESCRIPTION")]
pub description: Option<String>,
#[serde(alias = "Tlos", alias = "TLOS")]
pub tlos: Vec<String>,
}
pub type Goals = HashMap<String, Goal>;
impl Formalize for Goal {
fn formalize(&mut self) -> Result<()> {
if self.tlos.is_empty() {
return Err(anyhow::anyhow!("Goal requires at least one TLO"));
}
Ok(())
}
}
impl Connection<TrainingLearningObjective> for (&String, &Goal) {
fn validate_connections(&self, potential_tlo_names: &Option<Vec<String>>) -> Result<()> {
let tlos = &self.1.tlos;
if let Some(tlo_names) = potential_tlo_names {
for tlo_name in tlos {
if !tlo_names.contains(tlo_name) {
return Err(anyhow!("TLO \"{tlo_name}\" not found under Scenario TLOs"));
}
}
} else {
return Err(anyhow!(
"Goal requires at least one TLO but none found under Scenario"
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_sdl;
#[test]
fn parses_sdl_with_goals() {
let sdl = r#"
name: test-scenario
description: some-description
start: 2022-01-20T13:00:00Z
end: 2022-01-20T23:00:00Z
conditions:
condition-1:
command: executable/path.sh
interval: 30
metrics:
metric-1:
type: MANUAL
artifact: true
max-score: 10
metric-2:
type: CONDITIONAL
max-score: 10
condition: condition-1
vulnerabilities:
vulnerability-1:
name: Some other vulnerability
description: some-description
technical: false
class: CWE-1343
vulnerability-2:
name: Some vulnerability
description: some-description
technical: false
class: CWE-1341
evaluations:
evaluation-1:
description: some description
metrics:
- metric-1
- metric-2
min-score: 50
capabilities:
capability-1:
description: "Can defend against Dirty Cow"
condition: condition-1
vulnerabilities:
- vulnerability-1
- vulnerability-2
capability-2:
description: "Can defend against Dirty Cow"
condition: condition-1
vulnerabilities:
- vulnerability-1
- vulnerability-2
tlos:
tlo-1:
description: some description
evaluation: evaluation-1
capabilities:
- capability-1
- capability-2
goals:
goal-1:
description: "new goal"
tlos:
- tlo-1
"#;
let goals = parse_sdl(sdl).unwrap().goals;
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(goals);
});
}
#[test]
#[should_panic(expected = "Goal requires at least one TLO but none found under Scenario")]
fn fails_without_tlos() {
let sdl = r#"
name: test-scenario
description: some-description
start: 2022-01-20T13:00:00Z
end: 2022-01-20T23:00:00Z
goals:
goal-1:
description: "new goal"
tlos:
- tlo-1
"#;
parse_sdl(sdl).unwrap();
}
#[test]
fn parses_single_goal() {
let goal_yml = r#"
description: "new goal"
tlos:
- tlo-1
- tlo-2
- tlo-3
"#;
serde_yaml::from_str::<Goal>(goal_yml).unwrap();
}
#[test]
#[should_panic(expected = "Goal requires at least one TLO")]
fn fails_with_empty_tlo_list() {
let goal_yml = r#"
description: "new goal"
tlos:
"#;
serde_yaml::from_str::<Goal>(goal_yml)
.unwrap()
.formalize()
.unwrap();
}
}