sdl_parser/
goal.rs

1use anyhow::{anyhow, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::{
6    helpers::Connection, training_learning_objective::TrainingLearningObjective, Formalize,
7};
8
9#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
10pub struct Goal {
11    #[serde(alias = "Name", alias = "NAME")]
12    pub name: Option<String>,
13    #[serde(alias = "Description", alias = "DESCRIPTION")]
14    pub description: Option<String>,
15    #[serde(alias = "Tlos", alias = "TLOS")]
16    pub tlos: Vec<String>,
17}
18
19pub type Goals = HashMap<String, Goal>;
20
21impl Formalize for Goal {
22    fn formalize(&mut self) -> Result<()> {
23        if self.tlos.is_empty() {
24            return Err(anyhow::anyhow!("Goal requires at least one TLO"));
25        }
26        Ok(())
27    }
28}
29
30impl Connection<TrainingLearningObjective> for (&String, &Goal) {
31    fn validate_connections(&self, potential_tlo_names: &Option<Vec<String>>) -> Result<()> {
32        let tlos = &self.1.tlos;
33
34        if let Some(tlo_names) = potential_tlo_names {
35            for tlo_name in tlos {
36                if !tlo_names.contains(tlo_name) {
37                    return Err(anyhow!("TLO \"{tlo_name}\" not found under Scenario TLOs"));
38                }
39            }
40        } else {
41            return Err(anyhow!(
42                "Goal requires at least one TLO but none found under Scenario"
43            ));
44        }
45
46        Ok(())
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::parse_sdl;
54
55    #[test]
56    fn parses_sdl_with_goals() {
57        let sdl = r#"
58          name: test-scenario
59          description: some-description
60          conditions:
61            condition-1:
62                command: executable/path.sh
63                interval: 30
64          metrics:
65              metric-1:
66                  type: MANUAL
67                  artifact: true
68                  max-score: 10
69              metric-2:
70                  type: CONDITIONAL
71                  max-score: 10
72                  condition: condition-1
73          vulnerabilities:
74              vulnerability-1:
75                  name: Some other vulnerability
76                  description: some-description
77                  technical: false
78                  class: CWE-1343
79              vulnerability-2:
80                  name: Some vulnerability
81                  description: some-description
82                  technical: false
83                  class: CWE-1341
84          evaluations:
85              evaluation-1:
86                  description: some description
87                  metrics:
88                      - metric-1
89                      - metric-2
90                  min-score: 50
91          tlos:
92              tlo-1:
93                  description: some description
94                  evaluation: evaluation-1
95          goals:
96            goal-1:
97                description: "new goal"
98                tlos: 
99                  - tlo-1                   
100      "#;
101        let goals = parse_sdl(sdl).unwrap().goals;
102        insta::with_settings!({sort_maps => true}, {
103                insta::assert_yaml_snapshot!(goals);
104        });
105    }
106
107    #[test]
108    #[should_panic(expected = "Goal requires at least one TLO but none found under Scenario")]
109    fn fails_without_tlos() {
110        let sdl = r#"
111            name: test-scenario
112            description: some-description
113            goals:
114                goal-1:
115                    description: "new goal"
116                    tlos: 
117                        - tlo-1                   
118      "#;
119        parse_sdl(sdl).unwrap();
120    }
121
122    #[test]
123    fn parses_single_goal() {
124        let goal_yml = r#"
125          description: "new goal"
126          tlos: 
127            - tlo-1
128            - tlo-2
129            - tlo-3
130        "#;
131        serde_yaml::from_str::<Goal>(goal_yml).unwrap();
132    }
133
134    #[test]
135    #[should_panic(expected = "Goal requires at least one TLO")]
136    fn fails_with_empty_tlo_list() {
137        let goal_yml = r#"
138          description: "new goal"
139          tlos:
140        "#;
141        serde_yaml::from_str::<Goal>(goal_yml)
142            .unwrap()
143            .formalize()
144            .unwrap();
145    }
146}