use anyhow::anyhow;
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use self::location::{Location, SymbolicLocation};
use crate::{
InputKey, audit::AuditError, finding::location::LocationKind, models::AsDocument,
registry::input::Group,
};
use yamlpatch::{self, Patch};
pub(crate) mod location;
#[derive(
Copy,
Clone,
Debug,
Default,
Eq,
Hash,
Ord,
PartialOrd,
PartialEq,
Serialize,
Deserialize,
ValueEnum,
)]
pub(crate) enum Persona {
Auditor,
Pedantic,
#[default]
Regular,
}
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Serialize, Deserialize)]
pub(crate) enum Confidence {
Low,
Medium,
High,
}
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialOrd, PartialEq, Serialize, Deserialize)]
pub(crate) enum Severity {
Informational,
Low,
Medium,
High,
}
#[derive(Copy, Clone, Serialize)]
pub(crate) struct Determinations {
pub(crate) confidence: Confidence,
pub(crate) severity: Severity,
pub(super) persona: Persona,
}
#[derive(Copy, Clone, Debug, Default)]
pub(crate) enum FixDisposition {
#[allow(dead_code)]
Safe,
#[default]
Unsafe,
}
pub(crate) struct Fix<'doc> {
#[allow(dead_code)]
pub(crate) title: String,
pub(crate) key: &'doc InputKey,
pub(crate) disposition: FixDisposition,
pub(crate) patches: Vec<Patch<'doc>>,
}
impl Fix<'_> {
pub(crate) fn apply(
&self,
document: &yamlpath::Document,
) -> anyhow::Result<yamlpath::Document> {
match yamlpatch::apply_yaml_patches(document, &self.patches) {
Ok(new_document) => Ok(new_document),
Err(e) => Err(anyhow!("fix failed: {e}")),
}
}
}
pub(crate) struct Finding<'doc> {
pub(crate) ident: &'static str,
pub(crate) desc: &'static str,
pub(crate) url: &'static str,
pub(crate) determinations: Determinations,
pub(crate) locations: Vec<Location<'doc>>,
pub(crate) tip: Option<String>,
pub(crate) ignored: bool,
pub(crate) fixes: Vec<Fix<'doc>>,
}
impl Finding<'_> {
pub(crate) fn to_markdown(&self) -> String {
format!(
"`{ident}`: {desc}\n\nDocs: <{url}>",
ident = self.ident,
desc = self.desc,
url = self.url
)
}
pub(crate) fn visible_locations(&self) -> impl Iterator<Item = &Location<'_>> {
self.locations.iter().filter(|l| !l.symbolic.is_hidden())
}
pub(crate) fn primary_location(&self) -> &Location<'_> {
self.locations
.iter()
.find(|l| l.symbolic.is_primary())
.expect("internal error: finding has no primary location")
}
pub(crate) fn input_group(&self) -> &Group {
self.primary_location().symbolic.key.group()
}
}
pub(crate) struct FindingBuilder<'doc> {
ident: &'static str,
desc: &'static str,
url: &'static str,
severity: Severity,
confidence: Confidence,
persona: Persona,
raw_locations: Vec<Location<'doc>>,
locations: Vec<SymbolicLocation<'doc>>,
tip: Option<String>,
fixes: Vec<Fix<'doc>>,
}
impl<'doc> FindingBuilder<'doc> {
pub(crate) fn new(ident: &'static str, desc: &'static str, url: &'static str) -> Self {
Self {
ident,
desc,
url,
severity: Severity::Low,
confidence: Confidence::Low,
persona: Default::default(),
raw_locations: vec![],
locations: vec![],
tip: None,
fixes: vec![],
}
}
pub(crate) fn severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub(crate) fn confidence(mut self, confidence: Confidence) -> Self {
self.confidence = confidence;
self
}
pub(crate) fn persona(mut self, persona: Persona) -> Self {
self.persona = persona;
self
}
pub(crate) fn add_raw_location(mut self, location: Location<'doc>) -> Self {
self.raw_locations.push(location);
self
}
pub(crate) fn add_location(mut self, location: SymbolicLocation<'doc>) -> Self {
self.locations.push(location);
self
}
pub(crate) fn tip(mut self, tip: impl Into<String>) -> Self {
self.tip = Some(tip.into());
self
}
pub(crate) fn fix(mut self, fix: Fix<'doc>) -> Self {
self.fixes.push(fix);
self
}
pub(crate) fn build<'a>(
self,
document: &'a impl AsDocument<'a, 'doc>,
) -> Result<Finding<'doc>, AuditError> {
let mut locations = self
.locations
.iter()
.map(|l| l.clone().concretize(document.as_document()))
.collect::<anyhow::Result<Vec<_>>>()
.map_err(|e| AuditError::new(self.ident, e))?;
locations.extend(self.raw_locations);
if locations.len() == 1
&& let Some(location) = locations.get_mut(0)
{
location.symbolic.kind = LocationKind::Primary;
} else if !locations.iter().any(|l| l.symbolic.is_primary()) {
return Err(AuditError::new(
self.ident,
anyhow!("API misuse: at least one location must be marked with primary()"),
));
}
let should_ignore = Self::ignored_from_inlined_comment(&locations, self.ident);
Ok(Finding {
ident: self.ident,
desc: self.desc,
url: self.url,
determinations: Determinations {
confidence: self.confidence,
severity: self.severity,
persona: self.persona,
},
locations,
tip: self.tip,
ignored: should_ignore,
fixes: self.fixes,
})
}
fn ignored_from_inlined_comment(locations: &[Location], id: &str) -> bool {
locations
.iter()
.flat_map(|l| &l.concrete.comments)
.any(|c| c.ignores(id))
}
}