sdl_parser/
feature.rs

1use crate::{
2    common::{HelperSource, Source},
3    helpers::Connection,
4    vulnerability::Vulnerability,
5    Formalize,
6};
7use anyhow::{anyhow, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
12pub enum FeatureType {
13    #[serde(alias = "service", alias = "SERVICE")]
14    Service,
15    #[serde(alias = "configuration", alias = "CONFIGURATION")]
16    Configuration,
17    #[serde(alias = "artifact", alias = "ARTIFACT")]
18    Artifact,
19}
20
21#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
22pub struct Feature {
23    #[serde(alias = "Name", alias = "NAME")]
24    pub name: Option<String>,
25    #[serde(rename = "type", alias = "Type", alias = "TYPE")]
26    pub feature_type: FeatureType,
27    #[serde(
28        default,
29        rename = "source",
30        alias = "Source",
31        alias = "SOURCE",
32        skip_serializing
33    )]
34    _source_helper: Option<HelperSource>,
35    #[serde(default, skip_deserializing)]
36    pub source: Option<Source>,
37    #[serde(default, alias = "Dependencies", alias = "DEPENDENCIES")]
38    pub dependencies: Option<Vec<String>>,
39    #[serde(default, alias = "Vulnerabilities", alias = "VULNERABILITIES")]
40    pub vulnerabilities: Option<Vec<String>>,
41    #[serde(default, alias = "Destination", alias = "DESTINATION")]
42    pub destination: Option<String>,
43    #[serde(alias = "Description", alias = "DESCRIPTION")]
44    pub description: Option<String>,
45    #[serde(alias = "Environment", alias = "ENVIRONMENT")]
46    pub environment: Option<Vec<String>>,
47}
48
49impl Connection<Vulnerability> for (&String, &Feature) {
50    fn validate_connections(
51        &self,
52        potential_vulnerability_names: &Option<Vec<String>>,
53    ) -> Result<()> {
54        if let Some(feature_vulnerabilities) = &self.1.vulnerabilities {
55            if let Some(vulnerabilities) = potential_vulnerability_names {
56                for vulnerability_name in feature_vulnerabilities.iter() {
57                    if !vulnerabilities.contains(vulnerability_name) {
58                        return Err(anyhow!(
59                            "Feature \"{feature_name}\" Vulnerability \"{vulnerability_name}\" not found under Scenario Vulnerabilities",
60                            feature_name = self.0
61                        ));
62                    }
63                }
64            } else {
65                return Err(anyhow!(
66                    "Feature \"{feature_name}\" has Vulnerabilities but none found under Scenario",
67                    feature_name = self.0
68                ));
69            }
70        }
71        Ok(())
72    }
73}
74
75pub type Features = HashMap<String, Feature>;
76
77impl Formalize for Feature {
78    fn formalize(&mut self) -> Result<()> {
79        if let Some(helper_source) = &self._source_helper {
80            self.source = Some(helper_source.to_owned().into());
81        } else {
82            return Err(anyhow!("Feature missing Source field"));
83        }
84        Ok(())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::parse_sdl;
92
93    #[test]
94    fn feature_source_fields_are_mapped_correctly() {
95        let sdl = r#"
96            name: test-scenario
97            description: some-description
98            features:
99                my-cool-feature:
100                    type: Service
101                    source: some-service
102                my-cool-feature-config:
103                    type: Configuration
104                    source:
105                        name: cool-config
106                        version: 1.0.0
107        "#;
108        let features = parse_sdl(sdl).unwrap().features;
109        insta::with_settings!({sort_maps => true}, {
110                insta::assert_yaml_snapshot!(features);
111        });
112    }
113
114    #[test]
115    fn feature_source_longhand_is_parsed() {
116        let longhand_source = r#"
117            type: artifact
118            source:
119                name: artifact-name
120                version: 1.2.3
121        "#;
122        let feature = serde_yaml::from_str::<Feature>(longhand_source).unwrap();
123        insta::assert_debug_snapshot!(feature);
124    }
125    #[test]
126    fn feature_source_shorthand_is_parsed() {
127        let shorthand_source = r#"
128            type: artifact
129            source: artifact-name
130        "#;
131        let feature = serde_yaml::from_str::<Feature>(shorthand_source).unwrap();
132        insta::assert_debug_snapshot!(feature);
133    }
134
135    #[test]
136    fn feature_includes_dependencies() {
137        let feature_sdl = r#"
138            type: Service
139            source: some-service
140            dependencies:
141                - some-virtual-machine
142                - some-switch
143                - something-else
144        "#;
145        let feature = serde_yaml::from_str::<Feature>(feature_sdl).unwrap();
146        insta::assert_debug_snapshot!(feature);
147    }
148
149    #[test]
150    fn cyclic_feature_dependency_is_detected() {
151        let sdl = r#"
152            name: test-scenario
153            description: some-description
154            features:
155                my-cool-feature:
156                    type: Service
157                    source: some-service
158                    dependencies: 
159                        - my-less-cool-feature
160                my-less-cool-feature:
161                    type: Configuration
162                    source:
163                        name: cool-config
164                        version: 1.0.0
165                    dependencies: 
166                        - my-cool-feature
167        "#;
168        let features = parse_sdl(sdl);
169        assert!(features.is_err());
170        assert_eq!(
171            features.err().unwrap().to_string(),
172            "Cyclic dependency detected"
173        );
174    }
175
176    #[test]
177    fn feature_cyclic_self_dependency_is_detected() {
178        let sdl = r#"
179            name: test-scenario
180            description: some-description
181            features:
182                my-cool-feature:
183                    type: Service
184                    source: some-service
185                    dependencies: 
186                        - my-cool-feature
187        "#;
188        let features = parse_sdl(sdl);
189        assert!(features.is_err());
190        assert_eq!(
191            features.err().unwrap().to_string(),
192            "Cyclic dependency detected"
193        );
194    }
195
196    #[test]
197    fn can_parse_destination_environment() {
198        let feature = r#"
199                    type: Service
200                    source: some-service
201                    dependencies: 
202                        - my-cool-feature
203                    environment: 
204                        - ENV_VAR_1=ENV_VALUE_1
205                        - ENV_VAR_2=ENV_VALUE_2
206                    destination: some-destination
207        "#;
208
209        serde_yaml::from_str::<Feature>(feature).unwrap();
210    }
211}