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}