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}