sdl_parser/
lib.rs

1pub mod common;
2pub mod condition;
3mod constants;
4pub mod entity;
5pub mod evaluation;
6pub mod event;
7pub mod feature;
8pub mod goal;
9mod helpers;
10pub mod infrastructure;
11pub mod inject;
12mod library_item;
13pub mod metric;
14pub mod node;
15pub mod script;
16pub mod story;
17pub mod training_learning_objective;
18pub mod vulnerability;
19
20use crate::helpers::Connection;
21use anyhow::{anyhow, Ok, Result};
22use condition::{Condition, Conditions};
23use constants::MAX_LONG_NAME;
24use depper::{Dependencies, DependenciesBuilder};
25use entity::{Entities, Entity, Flatten};
26use evaluation::{Evaluation, Evaluations};
27use event::{Event, Events};
28use feature::{Feature, Features};
29use goal::Goals;
30use infrastructure::{Infrastructure, InfrastructureHelper, Properties};
31use inject::{Inject, Injects};
32pub use library_item::LibraryItem;
33use metric::{Metric, Metrics};
34use node::{Node, NodeType, Nodes};
35use script::{Script, Scripts};
36use serde::{Deserialize, Serialize};
37use std::collections::HashSet;
38use story::Stories;
39use training_learning_objective::{TrainingLearningObjective, TrainingLearningObjectives};
40use vulnerability::{Vulnerabilities, Vulnerability};
41
42pub trait Formalize {
43    fn formalize(&mut self) -> Result<()>;
44}
45
46#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
47pub struct Scenario {
48    #[serde(alias = "Name", alias = "NAME")]
49    pub name: String,
50    #[serde(default, alias = "Description", alias = "DESCRIPTION")]
51    pub description: Option<String>,
52    #[serde(alias = "Nodes", alias = "NODES")]
53    pub nodes: Option<Nodes>,
54    #[serde(alias = "Features", alias = "FEATURES")]
55    pub features: Option<Features>,
56    #[serde(
57        default,
58        rename = "infrastructure",
59        skip_serializing,
60        alias = "Infrastructure",
61        alias = "INFRASTRUCTURE"
62    )]
63    infrastructure_helper: Option<InfrastructureHelper>,
64    #[serde(
65        default,
66        skip_deserializing,
67        alias = "Infrastructure",
68        alias = "INFRASTRUCTURE"
69    )]
70    pub infrastructure: Option<Infrastructure>,
71    #[serde(alias = "Conditions", alias = "CONDITIONS")]
72    pub conditions: Option<Conditions>,
73    #[serde(alias = "Vulnerabilities", alias = "VULNERABILITIES")]
74    pub vulnerabilities: Option<Vulnerabilities>,
75    #[serde(alias = "Metrics", alias = "METRICS")]
76    pub metrics: Option<Metrics>,
77    #[serde(alias = "Evaluations", alias = "EVALUATIONS")]
78    pub evaluations: Option<Evaluations>,
79    #[serde(alias = "Tlos", alias = "TLOS")]
80    pub tlos: Option<TrainingLearningObjectives>,
81    #[serde(alias = "Entities", alias = "ENTITIES")]
82    pub entities: Option<Entities>,
83    #[serde(alias = "Goals", alias = "GOALS")]
84    pub goals: Option<Goals>,
85    #[serde(alias = "Injects", alias = "INJECTS")]
86    pub injects: Option<Injects>,
87    #[serde(alias = "Events", alias = "EVENTS")]
88    pub events: Option<Events>,
89    #[serde(alias = "Scripts", alias = "SCRIPTS")]
90    pub scripts: Option<Scripts>,
91    #[serde(alias = "Stories", alias = "STORIES")]
92    pub stories: Option<Stories>,
93}
94
95impl Scenario {
96    pub fn to_yaml(&self) -> Result<String> {
97        serde_yaml::to_string(&self).map_err(|e| anyhow!("Failed to serialize to yaml: {}", e))
98    }
99
100    pub fn from_yaml(yaml: &str) -> Result<Self> {
101        let mut schema: Self = serde_yaml::from_str(yaml)
102            .map_err(|e| anyhow!("Failed to deserialize from yaml: {}", e))?;
103        schema.formalize()?;
104        Ok(schema)
105    }
106
107    pub fn get_node_dependencies(&self) -> Result<Dependencies> {
108        let mut dependency_builder = Dependencies::builder();
109        if let Some(nodes_value) = &self.nodes {
110            for (node_name, _) in nodes_value.iter() {
111                dependency_builder = dependency_builder.add_element(node_name.to_string(), vec![]);
112            }
113        }
114
115        self.build_infrastructure_dependencies(dependency_builder)
116    }
117
118    pub fn get_feature_dependencies(&self) -> Result<Dependencies> {
119        let mut dependency_builder = Dependencies::builder();
120        if let Some(features_value) = &self.features {
121            for (feature_name, _) in features_value.iter() {
122                dependency_builder =
123                    dependency_builder.add_element(feature_name.to_string(), vec![]);
124            }
125        }
126        self.build_feature_dependencies(dependency_builder)
127    }
128
129    pub fn get_a_node_features_dependencies(
130        &self,
131        node_feature_name: &str,
132    ) -> Result<Dependencies> {
133        let mut dependency_builder = Dependencies::builder();
134
135        if let Some(features_value) = &self.features {
136            for (feature_name, _) in features_value.iter() {
137                if feature_name.eq_ignore_ascii_case(node_feature_name) {
138                    dependency_builder =
139                        dependency_builder.add_element(feature_name.to_string(), vec![]);
140                }
141            }
142        }
143        self.build_a_single_features_dependencies(dependency_builder, node_feature_name)
144    }
145
146    fn build_infrastructure_dependencies(
147        &self,
148        mut dependency_builder: depper::DependenciesBuilder,
149    ) -> Result<depper::Dependencies, anyhow::Error> {
150        if let Some(infrastructure) = &self.infrastructure {
151            for (node_name, infra_node) in infrastructure.iter() {
152                let mut dependencies: Vec<String> = vec![];
153
154                if let Some(links) = &infra_node.links {
155                    for link in links {
156                        dependencies.push(link.clone());
157                    }
158                }
159
160                if let Some(node_dependencies) = &infra_node.dependencies {
161                    dependencies.extend(node_dependencies.iter().cloned());
162                }
163
164                dependency_builder =
165                    dependency_builder.add_element(node_name.clone(), dependencies);
166            }
167        }
168        dependency_builder
169            .build()
170            .map_err(|e| anyhow!("Error building dependencies: {}", e))
171    }
172
173    fn build_feature_dependencies(
174        &self,
175        mut dependency_builder: depper::DependenciesBuilder,
176    ) -> Result<Dependencies, anyhow::Error> {
177        if let Some(features) = &self.features {
178            for (feature_name, feature) in features.iter() {
179                let mut dependencies: Vec<String> = vec![];
180
181                if let Some(feature_dependencies) = &feature.dependencies {
182                    dependencies.extend_from_slice(feature_dependencies.as_slice());
183                }
184                dependency_builder =
185                    dependency_builder.add_element(feature_name.to_owned(), dependencies);
186            }
187        }
188        dependency_builder.build()
189    }
190
191    fn build_a_single_features_dependencies(
192        &self,
193        mut dependency_builder: DependenciesBuilder,
194        feature_name: &str,
195    ) -> Result<Dependencies, anyhow::Error> {
196        dependency_builder =
197            self.get_a_parent_features_dependencies(feature_name, dependency_builder);
198
199        dependency_builder.build()
200    }
201
202    pub fn get_a_parent_features_dependencies(
203        &self,
204        feature_name: &str,
205        mut dependency_builder: DependenciesBuilder,
206    ) -> DependenciesBuilder {
207        if let Some(features) = &self.features {
208            if let Some(feature) = features.get(feature_name) {
209                if let Some(dependencies) = &feature.dependencies {
210                    dependency_builder = dependency_builder
211                        .add_element(feature_name.to_owned(), dependencies.to_vec());
212
213                    for feature_name in dependencies.iter() {
214                        dependency_builder = self
215                            .get_a_parent_features_dependencies(feature_name, dependency_builder)
216                    }
217                    return dependency_builder;
218                } else {
219                    return dependency_builder.add_element(feature_name.to_owned(), vec![]);
220                }
221            }
222        }
223        dependency_builder
224    }
225
226    fn verify_dependencies(&self) -> Result<()> {
227        self.get_node_dependencies()?;
228        self.get_feature_dependencies()?;
229        Ok(())
230    }
231
232    fn verify_switch_counts(&self) -> Result<()> {
233        if let Some(infrastructure) = &self.infrastructure {
234            if let Some(nodes) = &self.nodes {
235                for (node_name, infra_node) in infrastructure.iter() {
236                    if infra_node.count > 1 {
237                        if let Some(node) = nodes.get(node_name) {
238                            match node.type_field {
239                                NodeType::Switch(_) => {
240                                    return Err(anyhow!(
241                                        "Node {} is a switch with a count higher than 1",
242                                        node_name
243                                    ));
244                                }
245                                _ => continue,
246                            }
247                        }
248                    }
249                }
250            }
251        }
252        Ok(())
253    }
254
255    pub fn verify_nodes(&self) -> Result<()> {
256        let feature_names = self
257            .features
258            .as_ref()
259            .map(|feature_map| feature_map.keys().cloned().collect::<Vec<String>>());
260        let condition_names = self
261            .conditions
262            .as_ref()
263            .map(|condition_map| condition_map.keys().cloned().collect::<Vec<String>>());
264        let inject_names = self
265            .injects
266            .as_ref()
267            .map(|inject_map| inject_map.keys().cloned().collect::<Vec<String>>());
268        let vulnerability_names = self
269            .vulnerabilities
270            .as_ref()
271            .map(|vulnerability_map| vulnerability_map.keys().cloned().collect::<Vec<String>>());
272        if let Some(nodes) = &self.nodes {
273            for named_node in nodes.iter() {
274                let name = named_node.0;
275                if name.len() > MAX_LONG_NAME {
276                    return Err(anyhow!(
277                        "{} is too long, maximum node name length is {:?}",
278                        name,
279                        MAX_LONG_NAME
280                    ));
281                }
282                match &named_node.1.type_field {
283                    NodeType::VM(vm) => {
284                        let named_vm = (named_node.0, vm);
285                        Connection::<Feature>::validate_connections(&named_vm, &feature_names)?;
286                        Connection::<Vulnerability>::validate_connections(
287                            &named_vm,
288                            &vulnerability_names,
289                        )?;
290                        Connection::<Condition>::validate_connections(
291                            &(named_node.0, vm, &self.infrastructure),
292                            &condition_names,
293                        )?;
294                        Connection::<Inject>::validate_connections(
295                            &(named_node.0, vm, &self.infrastructure),
296                            &inject_names,
297                        )?;
298                    }
299                    _ => continue,
300                }
301            }
302        }
303        Ok(())
304    }
305
306    pub fn verify_infrastructure(&self) -> Result<()> {
307        let node_names = self
308            .nodes
309            .as_ref()
310            .map(|node_map| node_map.keys().cloned().collect::<HashSet<String>>())
311            .unwrap_or_default();
312
313        if let Some(infrastructure) = &self.infrastructure {
314            for (infrastructure_name, infra_node) in infrastructure {
315                if !node_names.contains(infrastructure_name) {
316                    return Err(anyhow!(
317                        "Infrastructure entry \"{}\" does not exist under Nodes",
318                        infrastructure_name
319                    ));
320                }
321
322                let mut all_dependencies = HashSet::new();
323                if let Some(links) = &infra_node.links {
324                    for link in links {
325                        if !infrastructure.contains_key(link) {
326                            return Err(anyhow!(
327                                "Infrastructure entry \"{}\" does not exist under Infrastructure even though it is a dependency for \"{}\"",
328                                link, infrastructure_name
329                            ));
330                        }
331                        all_dependencies.insert(link.clone());
332                    }
333                }
334                if let Some(dependencies) = &infra_node.dependencies {
335                    for dependency in dependencies {
336                        if !infrastructure.contains_key(dependency) {
337                            return Err(anyhow!(
338                                "Infrastructure entry \"{}\" does not exist under Infrastructure even though it is a dependency for \"{}\"",
339                                dependency, infrastructure_name
340                            ));
341                        }
342                        all_dependencies.insert(dependency.clone());
343                    }
344                }
345
346                if let Some(Properties::Simple { cidr, gateway }) = &infra_node.properties {
347                    if !cidr.contains(*gateway) {
348                        return Err(anyhow!(
349                            "Gateway {} is not within CIDR {} for node {}",
350                            gateway,
351                            cidr,
352                            infrastructure_name
353                        ));
354                    }
355                }
356
357                if let Some(Properties::Complex(properties_list)) = &infra_node.properties {
358                    if let Some(links) = &infra_node.links {
359                        let link_set: HashSet<&String> = links.iter().collect();
360
361                        for property_map in properties_list {
362                            for (link, ip) in property_map {
363                                if !link_set.contains(link) {
364                                    return Err(anyhow!(
365                                        "Property key '{}' in properties of node '{}' does not exist in its links: {:?}",
366                                        link,
367                                        infrastructure_name,
368                                        links
369                                    ));
370                                }
371
372                                if let Some(linked_node) = infrastructure.get(link) {
373                                    if let Some(Properties::Simple { cidr, .. }) =
374                                        &linked_node.properties
375                                    {
376                                        if !cidr.contains(*ip) {
377                                            return Err(anyhow!(
378                                                "IP address '{}' for '{}' in properties of node '{}' is not within the CIDR '{}' of the linked node '{}'",
379                                                ip, link, infrastructure_name, cidr, link
380                                            ));
381                                        }
382                                    } else {
383                                        return Err(anyhow!(
384                                            "Linked node '{}' does not have a valid CIDR in its properties",
385                                            link
386                                        ));
387                                    }
388                                } else {
389                                    return Err(anyhow!(
390                                        "Linked node '{}' referenced in properties of '{}' does not exist in the infrastructure",
391                                        link, infrastructure_name
392                                    ));
393                                }
394                            }
395                        }
396                    } else {
397                        return Err(anyhow!(
398                            "Links must be defined to validate Complex properties for node '{}'",
399                            infrastructure_name
400                        ));
401                    }
402                }
403            }
404        }
405
406        Ok(())
407    }
408
409    pub fn verify_evaluations(&self) -> Result<()> {
410        let metric_names = self
411            .metrics
412            .as_ref()
413            .map(|metric_map| metric_map.keys().cloned().collect::<Vec<String>>());
414        if let Some(evaluations) = &self.evaluations {
415            for named_evaluation in evaluations.iter() {
416                Connection::<Metric>::validate_connections(&named_evaluation, &metric_names)?;
417                Evaluation::validate_evaluation_metric_scores(
418                    named_evaluation.1,
419                    self.metrics.as_ref(),
420                )?;
421            }
422        }
423        Ok(())
424    }
425
426    fn verify_training_learning_objectives(&self) -> Result<()> {
427        let evaluation_names = self
428            .evaluations
429            .as_ref()
430            .map(|evaluation_map| evaluation_map.keys().cloned().collect::<Vec<String>>());
431
432        if let Some(training_learning_objectives) = &self.tlos {
433            for named_tlo in training_learning_objectives {
434                Connection::<Evaluation>::validate_connections(&named_tlo, &evaluation_names)?;
435            }
436        }
437        Ok(())
438    }
439
440    fn verify_metrics(&self) -> Result<()> {
441        let condition_names = self
442            .conditions
443            .as_ref()
444            .map(|condition_map| condition_map.keys().cloned().collect::<Vec<String>>());
445
446        let mut unique_conditions_under_metrics = HashSet::new();
447
448        if let Some(metrics) = &self.metrics {
449            for (metric_name, metric) in metrics.iter() {
450                if let Some(condition) = &metric.condition {
451                    if !unique_conditions_under_metrics.insert(condition) {
452                        return Err(anyhow!(
453                            "Duplicate condition '{}' found under metrics. Each condition must be unique for every metric.",
454                            condition
455                        ));
456                    }
457                }
458                (metric_name, metric).validate_connections(&condition_names)?;
459            }
460        }
461        Ok(())
462    }
463
464    fn verify_features(&self) -> Result<()> {
465        let vulnerability_names = self
466            .vulnerabilities
467            .as_ref()
468            .map(|vulnerability_map| vulnerability_map.keys().cloned().collect::<Vec<String>>());
469        if let Some(features) = &self.features {
470            for named_feature in features.iter() {
471                named_feature.validate_connections(&vulnerability_names)?;
472            }
473        }
474        Ok(())
475    }
476
477    fn verify_entities(&self) -> Result<()> {
478        let vulnerability_names = self
479            .vulnerabilities
480            .as_ref()
481            .map(|vulnerability_map| vulnerability_map.keys().cloned().collect::<Vec<String>>());
482        let tlo_names = self
483            .tlos
484            .as_ref()
485            .map(|tlo_map| tlo_map.keys().cloned().collect::<Vec<String>>());
486        let event_names = self
487            .events
488            .as_ref()
489            .map(|event_map| event_map.keys().cloned().collect::<Vec<String>>());
490
491        if let Some(entities) = &self.entities {
492            for named_entity in entities.flatten().iter() {
493                Connection::<TrainingLearningObjective>::validate_connections(
494                    &named_entity,
495                    &tlo_names,
496                )?;
497                Connection::<Vulnerability>::validate_connections(
498                    &named_entity,
499                    &vulnerability_names,
500                )?;
501                Connection::<Event>::validate_connections(&named_entity, &event_names)?;
502            }
503        }
504        Ok(())
505    }
506
507    fn verify_goals(&self) -> Result<()> {
508        let tlo_names = self
509            .tlos
510            .as_ref()
511            .map(|tlo_map| tlo_map.keys().cloned().collect::<Vec<String>>());
512        if let Some(goals) = &self.goals {
513            for goal in goals.iter() {
514                goal.validate_connections(&tlo_names)?;
515            }
516        }
517        Ok(())
518    }
519
520    fn verify_injects(&self) -> Result<()> {
521        let entity_names = self.entities.as_ref().map(|entity_map| {
522            entity_map
523                .flatten()
524                .keys()
525                .cloned()
526                .collect::<Vec<String>>()
527        });
528        let tlo_names = self
529            .tlos
530            .as_ref()
531            .map(|tlo_map| tlo_map.keys().cloned().collect::<Vec<String>>());
532
533        if let Some(injects) = &self.injects {
534            for named_inject in injects.iter() {
535                Connection::<Entity>::validate_connections(&named_inject, &entity_names)?;
536                Connection::<TrainingLearningObjective>::validate_connections(
537                    &named_inject,
538                    &tlo_names,
539                )?;
540            }
541        }
542        Ok(())
543    }
544
545    fn verify_roles(&self) -> Result<()> {
546        if let Some(nodes) = &self.nodes {
547            let all_entity_names = self
548                .entities
549                .clone()
550                .map(|entities| entities.flatten().keys().cloned().collect::<Vec<String>>());
551
552            for (node_name, node) in nodes {
553                match &node.type_field {
554                    NodeType::VM(vm) => {
555                        Connection::<Entity>::validate_connections(
556                            &(node_name, &vm.roles),
557                            &all_entity_names,
558                        )?;
559
560                        let feature_roles = vm.features.values().cloned().collect::<Vec<String>>();
561                        Connection::<Node>::validate_connections(
562                            &(node_name, &vm.roles),
563                            &Some(feature_roles),
564                        )?;
565                        let condition_roles =
566                            vm.conditions.values().cloned().collect::<Vec<String>>();
567                        Connection::<Node>::validate_connections(
568                            &(node_name, &vm.roles),
569                            &Some(condition_roles),
570                        )?;
571                    }
572                    _ => continue,
573                }
574            }
575        }
576        Ok(())
577    }
578
579    fn verify_events(&self) -> Result<()> {
580        let condition_names = self
581            .conditions
582            .as_ref()
583            .map(|entity_map| entity_map.keys().cloned().collect::<Vec<String>>());
584        let inject_names = self
585            .injects
586            .as_ref()
587            .map(|tlo_map| tlo_map.keys().cloned().collect::<Vec<String>>());
588
589        if let Some(events) = &self.events {
590            for named_event in events.iter() {
591                Connection::<Condition>::validate_connections(&named_event, &condition_names)?;
592                Connection::<Inject>::validate_connections(&named_event, &inject_names)?;
593            }
594        }
595        Ok(())
596    }
597
598    fn verify_scripts(&self) -> Result<()> {
599        let event_names = self
600            .events
601            .as_ref()
602            .map(|entity_map| entity_map.keys().cloned().collect::<Vec<String>>());
603
604        if let Some(scripts) = &self.scripts {
605            for named_script in scripts.iter() {
606                Connection::<Event>::validate_connections(&named_script, &event_names)?;
607            }
608        }
609        Ok(())
610    }
611
612    fn verify_stories(&self) -> Result<()> {
613        let script_names = self
614            .scripts
615            .as_ref()
616            .map(|entity_map| entity_map.keys().cloned().collect::<Vec<String>>());
617
618        if let Some(stories) = &self.stories {
619            for named_story in stories.iter() {
620                Connection::<Script>::validate_connections(&named_story, &script_names)?;
621            }
622        }
623        Ok(())
624    }
625}
626
627impl Formalize for Scenario {
628    fn formalize(&mut self) -> Result<()> {
629        if let Some(infrastructure_helper) = &self.infrastructure_helper {
630            self.infrastructure = Some(Infrastructure::from(infrastructure_helper.clone()));
631        }
632
633        if let Some(mut nodes) = self.nodes.clone() {
634            nodes.iter_mut().try_for_each(move |(_, node)| {
635                if let NodeType::VM(ref mut vm) = node.type_field {
636                    vm.formalize()?;
637                }
638                Ok(())
639            })?;
640            self.nodes = Some(nodes);
641        }
642
643        if let Some(mut infrastructure) = self.infrastructure.clone() {
644            infrastructure
645                .iter_mut()
646                .try_for_each(move |(_, infra_node)| {
647                    infra_node.formalize()?;
648                    Ok(())
649                })?;
650            self.infrastructure = Some(infrastructure);
651        }
652
653        if let Some(features) = &mut self.features {
654            features.iter_mut().try_for_each(move |(_, feature)| {
655                feature.formalize()?;
656                Ok(())
657            })?;
658        }
659
660        if let Some(mut conditions) = self.conditions.clone() {
661            conditions.iter_mut().try_for_each(move |(_, condition)| {
662                condition.formalize()?;
663                Ok(())
664            })?;
665            self.conditions = Some(conditions);
666        }
667
668        if let Some(mut metrics) = self.metrics.clone() {
669            metrics.iter_mut().try_for_each(move |(_, metric)| {
670                metric.formalize()?;
671                Ok(())
672            })?;
673            self.metrics = Some(metrics);
674        }
675
676        if let Some(mut evaluations) = self.evaluations.clone() {
677            evaluations
678                .iter_mut()
679                .try_for_each(move |(_, evaluation)| {
680                    evaluation.formalize()?;
681                    Ok(())
682                })?;
683            self.evaluations = Some(evaluations);
684        }
685
686        if let Some(mut vulnerabilities) = self.vulnerabilities.clone() {
687            vulnerabilities
688                .iter_mut()
689                .try_for_each(move |(_, vulnerability)| {
690                    vulnerability.formalize()?;
691                    Ok(())
692                })?;
693            self.vulnerabilities = Some(vulnerabilities);
694        }
695
696        if let Some(mut goals) = self.goals.clone() {
697            goals.iter_mut().try_for_each(move |(_, goal)| {
698                goal.formalize()?;
699                Ok(())
700            })?;
701            self.goals = Some(goals);
702        }
703
704        if let Some(mut injects) = self.injects.clone() {
705            injects.iter_mut().try_for_each(move |(_, inject)| {
706                inject.formalize()?;
707                Ok(())
708            })?;
709            self.injects = Some(injects);
710        }
711
712        if let Some(mut events) = self.events.clone() {
713            events.iter_mut().try_for_each(move |(_, event)| {
714                event.formalize()?;
715                Ok(())
716            })?;
717            self.events = Some(events);
718        }
719
720        if let Some(mut scripts) = self.scripts.clone() {
721            scripts.iter_mut().try_for_each(move |(_, script)| {
722                script.formalize()?;
723                Ok(())
724            })?;
725            self.scripts = Some(scripts);
726        }
727
728        if let Some(mut stories) = self.stories.clone() {
729            stories.iter_mut().try_for_each(move |(_, story)| {
730                story.formalize()?;
731                Ok(())
732            })?;
733            self.stories = Some(stories);
734        }
735
736        self.verify_entities()?;
737        self.verify_goals()?;
738        self.verify_nodes()?;
739        self.verify_infrastructure()?;
740        self.verify_evaluations()?;
741        self.verify_switch_counts()?;
742        self.verify_features()?;
743        self.verify_dependencies()?;
744        self.verify_metrics()?;
745        self.verify_training_learning_objectives()?;
746        self.verify_roles()?;
747        self.verify_injects()?;
748        self.verify_events()?;
749        self.verify_scripts()?;
750        self.verify_stories()?;
751        Ok(())
752    }
753}
754
755pub fn parse_sdl(sdl_string: &str) -> Result<Scenario> {
756    Scenario::from_yaml(sdl_string)
757}
758
759#[cfg(test)]
760mod tests {
761    use super::*;
762
763    #[test]
764    fn can_parse_minimal_sdl() {
765        let minimal_sdl = r#"
766                name: test-scenario
767
768        "#;
769        let parsed_schema = parse_sdl(minimal_sdl).unwrap();
770        insta::assert_yaml_snapshot!(parsed_schema);
771    }
772
773    #[test]
774    fn includes_nodes() {
775        let sdl = r#"
776            name: test-scenario
777            description: some-description
778            nodes:
779                win10:
780                    type: VM
781                    description: win-10-description
782                    source: windows10
783                    resources:
784                        ram: 4 gib
785                        cpu: 2
786                deb10:
787                    type: VM
788                    description: deb-10-description
789                    source:
790                        name: debian10
791                        version: '*'
792                    resources:
793                        ram: 2 gib
794                        cpu: 1
795
796        "#;
797        let nodes = parse_sdl(sdl).unwrap().nodes;
798        insta::with_settings!({sort_maps => true}, {
799                insta::assert_yaml_snapshot!(nodes);
800        });
801    }
802
803    #[test]
804    fn includes_infrastructure() {
805        let sdl = r#"
806            name: test-scenario
807            description: some-description
808            nodes:
809                win10:
810                    type: VM
811                    description: win-10-description
812                    source: windows10
813                    resources:
814                        ram: 4 gib
815                        cpu: 2
816                deb10:
817                    type: VM
818                    description: deb-10-description
819                    source:
820                        name: debian10
821                        version: '*'
822                    resources:
823                        ram: 2 gib
824                        cpu: 1
825            infrastructure:
826                win10:
827                    count: 10
828                    dependencies:
829                        - deb10
830                deb10: 3
831
832        "#;
833        let infrastructure = parse_sdl(sdl).unwrap().infrastructure;
834        insta::with_settings!({sort_maps => true}, {
835                insta::assert_yaml_snapshot!(infrastructure);
836        });
837    }
838
839    #[test]
840    fn includes_features() {
841        let sdl = r#"
842            name: test-scenario
843            description: some-description
844            features:
845                my-cool-service:
846                    type: service
847                    source: some-service
848                my-cool-config:
849                    type: configuration
850                    source: some-configuration
851                    dependencies:
852                        - my-cool-service
853                my-cool-artifact:
854                    type: artifact
855                    source:
856                        name: dl_library
857                        version: 1.2.3
858                    dependencies:
859                        - my-cool-service
860        "#;
861        let features = parse_sdl(sdl).unwrap().features;
862        insta::with_settings!({sort_maps => true}, {
863                insta::assert_yaml_snapshot!(features);
864        });
865    }
866
867    #[test]
868    #[should_panic(expected = "VM \"win-10\" has Features but none found under Scenario")]
869    fn feature_missing_from_scenario() {
870        let sdl = r#"
871            name: test-scenario
872            description: some-description
873            nodes:
874                win-10:
875                    type: VM
876                    source: windows10
877                    resources:
878                        ram: 4 GiB
879                        cpu: 2
880                    roles:
881                        moderator: "name"
882                    features:
883                        my-cool-service: "moderator"
884
885        "#;
886        parse_sdl(sdl).unwrap();
887    }
888
889    #[test]
890    #[should_panic(expected = "Role admin not found under for Node win-10's roles")]
891    fn feature_role_missing_from_node() {
892        let sdl = r#"
893            name: test-scenario
894            description: some-description
895            nodes:
896                win-10:
897                    type: VM
898                    source: windows10
899                    resources:
900                        ram: 4 GiB
901                        cpu: 2
902                    roles:
903                        moderator: "name"
904                    features:
905                        my-cool-service: "admin"
906            features:
907                my-cool-service:
908                    type: service
909                    source: some-service
910
911        "#;
912        parse_sdl(sdl).unwrap();
913    }
914
915    #[test]
916    fn includes_conditions_nodes_and_infrastructure() {
917        let sdl = r#"
918            name: test-scenario
919            description: some-description
920            nodes:
921                win10:
922                    type: VM
923                    description: win-10-description
924                    source: windows10
925                    resources:
926                        ram: 4 gib
927                        cpu: 2
928                    roles:
929                        admin: "username"
930                    conditions:
931                        condition-1: "admin"
932                deb10:
933                    type: VM
934                    description: deb-10-description
935                    source:
936                        name: debian10
937                        version: '*'
938                    resources:
939                        ram: 2 gib
940                        cpu: 1
941                    roles:
942                        admin: "username"
943                        moderator: "name"
944                    conditions:
945                        condition-2: "moderator"
946                        condition-3: "admin"
947            infrastructure:
948                win10:
949                    count: 1
950                    dependencies:
951                        - deb10
952                deb10: 1
953            conditions:
954                condition-1:
955                    command: executable/path.sh
956                    interval: 30
957                condition-2:
958                    source: digital-library-package
959                condition-3:
960                    command: executable/path.sh
961                    interval: 30
962
963        "#;
964        let conditions = parse_sdl(sdl).unwrap();
965        insta::with_settings!({sort_maps => true}, {
966                insta::assert_yaml_snapshot!(conditions);
967        });
968    }
969
970    #[test]
971    #[should_panic(
972        expected = "Node \"win10\" can not have count bigger than 1, if it has conditions defined"
973    )]
974    fn condition_vm_count_in_infrastructure_over_1() {
975        let sdl = r#"
976            name: test-scenario
977            description: some-description
978            nodes:
979                win10:
980                    type: VM
981                    description: win-10-description
982                    source: windows10
983                    resources:
984                        ram: 4 gib
985                        cpu: 2
986                    roles:
987                        admin: "username"
988                    conditions:
989                        condition-1: "admin"
990                deb10:
991                    type: VM
992                    description: deb-10-description
993                    source:
994                        name: debian10
995                        version: '*'
996                    resources:
997                        ram: 2 gib
998                        cpu: 1
999            infrastructure:
1000                win10:
1001                    count: 3
1002                    dependencies:
1003                        - deb10
1004                deb10: 1
1005            conditions:
1006                condition-1:
1007                    command: executable/path.sh
1008                    interval: 30
1009        "#;
1010        parse_sdl(sdl).unwrap();
1011    }
1012
1013    #[test]
1014    #[should_panic(expected = "Node \"win10\" has Conditions but none found under Scenario")]
1015    fn condition_doesnt_exist() {
1016        let sdl = r#"
1017            name: test-scenario
1018            description: some-description
1019            nodes:
1020                win10:
1021                    type: VM
1022                    description: win-10-description
1023                    source: windows10
1024                    resources:
1025                        ram: 4 gib
1026                        cpu: 2
1027                    roles:
1028                        admin: "username"
1029                    conditions:
1030                        condition-1: "admin"
1031            infrastructure:
1032                win10: 1
1033
1034        "#;
1035        parse_sdl(sdl).unwrap();
1036    }
1037
1038    #[test]
1039    #[should_panic(expected = "Role admin not found under for Node win-10's roles")]
1040    fn condition_role_missing_from_node() {
1041        let sdl = r#"
1042            name: test-scenario
1043            description: some-description
1044            nodes:
1045                win-10:
1046                    type: VM
1047                    source: windows10
1048                    resources:
1049                        ram: 4 GiB
1050                        cpu: 2
1051                    roles:
1052                        moderator: "name"
1053                    conditions:
1054                        condition-1: "admin"
1055            conditions:
1056                condition-1:
1057                    command: executable/path.sh
1058                    interval: 30
1059
1060        "#;
1061        parse_sdl(sdl).unwrap();
1062    }
1063
1064    #[test]
1065    #[should_panic(
1066        expected = "my-really-really-superlong-non-compliant-name is too long, maximum node name length is 35"
1067    )]
1068    fn too_long_node_name_is_disallowed() {
1069        let sdl = r#"
1070            name: test-scenario
1071            description: some-description
1072            nodes:
1073                my-really-really-superlong-non-compliant-name:
1074                    type: VM
1075                    description: win-10-description
1076                    source: windows10
1077                    resources:
1078                        ram: 4 gib
1079                        cpu: 2
1080        "#;
1081        parse_sdl(sdl).unwrap();
1082    }
1083
1084    #[test]
1085    fn parent_features_dependencies_are_built_correctly() {
1086        let sdl = r#"
1087            name: test-scenario
1088            description: some-description
1089            features:
1090                parent-service:
1091                    type: Service
1092                    source: some-service
1093                    dependencies:
1094                        - child-service
1095                        - child-config
1096                child-config:
1097                    type: Configuration
1098                    source: some-config
1099                child-service:
1100                    type: Service
1101                    source:
1102                        name: child-service
1103                        version: 1.0.0
1104                    dependencies:
1105                        - grandchild-config
1106                        - grandchild-artifact
1107                grandchild-config:
1108                    type: Configuration
1109                    source:
1110                        name: some-config
1111                        version: 1.0.0
1112                grandchild-artifact:
1113                    type: Artifact
1114                    source:
1115                        name: cool-artifact
1116                        version: 1.0.0
1117                    dependencies:
1118                        - grandgrandchild-artifact
1119                grandgrandchild-artifact:
1120                    type: Artifact
1121                    source: some-artifact
1122                    dependencies:
1123                        - grandchild-config
1124                unrelated-artifact:
1125                    type: Artifact
1126                    source: some-artifact
1127                    dependencies:
1128                        - very-unrelated-artifact
1129                very-unrelated-artifact:
1130                    type: Artifact
1131                    source: some-artifact
1132        "#;
1133        let scenario = parse_sdl(sdl).unwrap();
1134        let dependencies = scenario
1135            .get_a_node_features_dependencies("parent-service")
1136            .unwrap();
1137
1138        insta::assert_debug_snapshot!(dependencies.generate_tranches().unwrap());
1139    }
1140
1141    #[test]
1142    #[should_panic(
1143        expected = "Duplicate condition 'condition-1' found under metrics. Each condition must be unique for every metric."
1144    )]
1145    fn condition_used_by_multiple_metrics() {
1146        let sdl = r#"
1147            name: test-scenario
1148            description: some-description
1149            metrics:
1150                metric-1:
1151                    type: CONDITIONAL
1152                    max-score: 10
1153                    condition: condition-1
1154                metric-2:
1155                    type: CONDITIONAL
1156                    max-score: 10
1157                    condition: condition-1
1158            conditions:
1159                condition-1:
1160                    command: executable/path.sh
1161                    interval: 30
1162        "#;
1163        parse_sdl(sdl).unwrap();
1164    }
1165}