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}