use crate::{
common::{HelperSource, Source},
helpers::Connection,
vulnerability::Vulnerability,
Formalize,
};
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub enum FeatureType {
#[serde(alias = "service", alias = "SERVICE")]
Service,
#[serde(alias = "configuration", alias = "CONFIGURATION")]
Configuration,
#[serde(alias = "artifact", alias = "ARTIFACT")]
Artifact,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
pub struct Feature {
#[serde(alias = "Name", alias = "NAME")]
pub name: Option<String>,
#[serde(rename = "type", alias = "Type", alias = "TYPE")]
pub feature_type: FeatureType,
#[serde(
default,
rename = "source",
alias = "Source",
alias = "SOURCE",
skip_serializing
)]
_source_helper: Option<HelperSource>,
#[serde(default, skip_deserializing)]
pub source: Option<Source>,
#[serde(default, alias = "Dependencies", alias = "DEPENDENCIES")]
pub dependencies: Option<Vec<String>>,
#[serde(default, alias = "Vulnerabilities", alias = "VULNERABILITIES")]
pub vulnerabilities: Option<Vec<String>>,
#[serde(default, alias = "Destination", alias = "DESTINATION")]
pub destination: Option<String>,
#[serde(alias = "Description", alias = "DESCRIPTION")]
pub description: Option<String>,
#[serde(alias = "Environment", alias = "ENVIRONMENT")]
pub environment: Option<Vec<String>>,
}
impl Connection<Vulnerability> for (&String, &Feature) {
fn validate_connections(
&self,
potential_vulnerability_names: &Option<Vec<String>>,
) -> Result<()> {
if let Some(feature_vulnerabilities) = &self.1.vulnerabilities {
if let Some(vulnerabilities) = potential_vulnerability_names {
for vulnerability_name in feature_vulnerabilities.iter() {
if !vulnerabilities.contains(vulnerability_name) {
return Err(anyhow!(
"Feature \"{feature_name}\" Vulnerability \"{vulnerability_name}\" not found under Scenario Vulnerabilities",
feature_name = self.0
));
}
}
} else {
return Err(anyhow!(
"Feature \"{feature_name}\" has Vulnerabilities but none found under Scenario",
feature_name = self.0
));
}
}
Ok(())
}
}
pub type Features = HashMap<String, Feature>;
impl Formalize for Feature {
fn formalize(&mut self) -> Result<()> {
if let Some(helper_source) = &self._source_helper {
self.source = Some(helper_source.to_owned().into());
} else {
return Err(anyhow!("Feature missing Source field"));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_sdl;
#[test]
fn feature_source_fields_are_mapped_correctly() {
let sdl = r#"
name: test-scenario
description: some-description
start: 2022-01-20T13:00:00Z
end: 2022-01-20T23:00:00Z
features:
my-cool-feature:
type: Service
source: some-service
my-cool-feature-config:
type: Configuration
source:
name: cool-config
version: 1.0.0
"#;
let features = parse_sdl(sdl).unwrap().features;
insta::with_settings!({sort_maps => true}, {
insta::assert_yaml_snapshot!(features);
});
}
#[test]
fn feature_source_longhand_is_parsed() {
let longhand_source = r#"
type: artifact
source:
name: artifact-name
version: 1.2.3
"#;
let feature = serde_yaml::from_str::<Feature>(longhand_source).unwrap();
insta::assert_debug_snapshot!(feature);
}
#[test]
fn feature_source_shorthand_is_parsed() {
let shorthand_source = r#"
type: artifact
source: artifact-name
"#;
let feature = serde_yaml::from_str::<Feature>(shorthand_source).unwrap();
insta::assert_debug_snapshot!(feature);
}
#[test]
fn feature_includes_dependencies() {
let feature_sdl = r#"
type: Service
source: some-service
dependencies:
- some-virtual-machine
- some-switch
- something-else
"#;
let feature = serde_yaml::from_str::<Feature>(feature_sdl).unwrap();
insta::assert_debug_snapshot!(feature);
}
#[test]
fn cyclic_feature_dependency_is_detected() {
let sdl = r#"
name: test-scenario
description: some-description
start: 2022-01-20T13:00:00Z
end: 2022-01-20T23:00:00Z
features:
my-cool-feature:
type: Service
source: some-service
dependencies:
- my-less-cool-feature
my-less-cool-feature:
type: Configuration
source:
name: cool-config
version: 1.0.0
dependencies:
- my-cool-feature
"#;
let features = parse_sdl(sdl);
assert!(features.is_err());
assert_eq!(
features.err().unwrap().to_string(),
"Cyclic dependency detected"
);
}
#[test]
fn feature_cyclic_self_dependency_is_detected() {
let sdl = r#"
name: test-scenario
description: some-description
start: 2022-01-20T13:00:00Z
end: 2022-01-20T23:00:00Z
features:
my-cool-feature:
type: Service
source: some-service
dependencies:
- my-cool-feature
"#;
let features = parse_sdl(sdl);
assert!(features.is_err());
assert_eq!(
features.err().unwrap().to_string(),
"Cyclic dependency detected"
);
}
#[test]
fn can_parse_destination_environment() {
let feature = r#"
type: Service
source: some-service
dependencies:
- my-cool-feature
environment:
- ENV_VAR_1=ENV_VALUE_1
- ENV_VAR_2=ENV_VALUE_2
destination: some-destination
"#;
serde_yaml::from_str::<Feature>(feature).unwrap();
}
}