use crate::core::types::ForjarConfig;
use serde::Serialize;
use std::path::PathBuf;
pub mod checks;
pub mod locate;
pub mod sarif;
#[cfg(test)]
#[path = "tests_gate.rs"]
mod tests_gate;
pub const QUALITY_GATE_ERROR_CODE: &str = "FORJAR_QUALITY_GATE_VIOLATION";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GateLevel {
Error,
Warning,
Note,
}
impl GateLevel {
pub const fn sarif_level(self) -> &'static str {
match self {
GateLevel::Error => "error",
GateLevel::Warning => "warning",
GateLevel::Note => "note",
}
}
pub fn from_severity_str(s: &str) -> Self {
match s {
"error" => GateLevel::Error,
"info" => GateLevel::Note,
_ => GateLevel::Warning,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GateFinding {
pub rule_id: String,
pub level: GateLevel,
pub resource_id: String,
pub message: String,
pub remediation: Option<String>,
pub yaml_line: Option<usize>,
pub script_kind: Option<&'static str>,
pub script_line: Option<usize>,
}
impl GateFinding {
pub fn new(
rule_id: impl Into<String>,
level: GateLevel,
resource_id: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
rule_id: rule_id.into(),
level,
resource_id: resource_id.into(),
message: message.into(),
remediation: None,
yaml_line: None,
script_kind: None,
script_line: None,
}
}
pub fn in_script(mut self, kind: &'static str, line: usize) -> Self {
self.script_kind = Some(kind);
self.script_line = Some(line);
self
}
pub fn with_remediation(mut self, r: Option<String>) -> Self {
self.remediation = r;
self
}
pub fn render(&self) -> String {
match (self.resource_id.as_str(), self.script_kind) {
("", _) => format!("{}: {}", self.rule_id, self.message),
(id, Some(kind)) => format!("{} {id}/{kind}: {}", self.rule_id, self.message),
(id, None) => format!("{} {id}: {}", self.rule_id, self.message),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct GateThresholds {
pub max_cyclomatic: Option<usize>,
pub policy_dir: Option<PathBuf>,
pub complexity_is_error: bool,
}
#[derive(Debug, Clone)]
pub struct GateReport {
pub findings: Vec<GateFinding>,
pub scripts_analysed: usize,
pub resources_checked: usize,
}
impl GateReport {
pub fn passed(&self) -> bool {
self.error_count() == 0
}
pub fn error_count(&self) -> usize {
self.findings
.iter()
.filter(|f| f.level == GateLevel::Error)
.count()
}
pub fn advisory_count(&self) -> usize {
self.findings.len() - self.error_count()
}
pub fn render(&self) -> Vec<String> {
let mut lines: Vec<String> = self
.findings
.iter()
.filter(|f| f.level == GateLevel::Error)
.map(GateFinding::render)
.collect();
if !self.findings.is_empty() {
lines.push(format!(
"quality gate: {} error(s), {} advisory across {} resource(s), {} generated script(s)",
self.error_count(),
self.advisory_count(),
self.resources_checked,
self.scripts_analysed
));
}
lines
}
pub fn to_sarif(&self, artifact_uri: &str) -> serde_json::Value {
sarif::findings_to_sarif(&self.findings, artifact_uri)
}
}
pub fn evaluate(
config: &ForjarConfig,
yaml_text: Option<&str>,
thresholds: &GateThresholds,
) -> GateReport {
let scripts = checks::generate_scripts(config);
let mut findings = Vec::new();
checks::check_shell_complexity(&scripts, thresholds, &mut findings);
checks::check_plaintext_secrets(config, &scripts, &mut findings);
checks::check_shell_injection(&scripts, &mut findings);
checks::check_compliance(config, thresholds, &mut findings);
if let Some(text) = yaml_text {
locate::annotate(text, &mut findings);
}
GateReport {
findings,
scripts_analysed: scripts.len(),
resources_checked: config.resources.len(),
}
}