use crate::ir::DamlModule;
use serde::Serialize;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DetectError {
detector: String,
message: String,
}
impl DetectError {
pub fn new(detector: impl Into<String>, message: impl Into<String>) -> Self {
Self {
detector: detector.into(),
message: message.into(),
}
}
pub fn detector(&self) -> &str {
&self.detector
}
pub fn message(&self) -> &str {
&self.message
}
}
impl std::fmt::Display for DetectError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "detector '{}': {}", self.detector, self.message)
}
}
impl std::error::Error for DetectError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[non_exhaustive]
pub enum Severity {
Critical,
High,
Medium,
Low,
Info,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Critical => write!(f, "CRITICAL"),
Self::High => write!(f, "HIGH"),
Self::Medium => write!(f, "MEDIUM"),
Self::Low => write!(f, "LOW"),
Self::Info => write!(f, "INFO"),
}
}
}
impl std::str::FromStr for Severity {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"critical" => Ok(Self::Critical),
"high" => Ok(Self::High),
"medium" => Ok(Self::Medium),
"low" => Ok(Self::Low),
"info" => Ok(Self::Info),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Serialize)]
#[non_exhaustive]
pub struct Finding {
pub detector: String,
pub severity: Severity,
pub file: PathBuf,
pub line: usize,
pub column: usize,
pub message: String,
pub evidence: String,
}
pub fn parse_severity(s: &str) -> Option<Severity> {
s.parse().ok()
}
pub trait Detector {
fn name(&self) -> &str;
fn severity(&self) -> Severity;
fn description(&self) -> &str;
fn detect(&self, module: &DamlModule) -> Vec<Finding>;
fn try_detect(&self, module: &DamlModule) -> Result<Vec<Finding>, DetectError> {
Ok(self.detect(module))
}
}
pub struct ConfiguredDetector {
inner: Box<dyn Detector>,
name_override: Option<String>,
severity_override: Option<Severity>,
}
impl ConfiguredDetector {
pub fn new(
inner: Box<dyn Detector>,
name_override: Option<String>,
severity_override: Option<Severity>,
) -> Self {
Self {
inner,
name_override,
severity_override,
}
}
fn apply_overrides(&self, mut findings: Vec<Finding>) -> Vec<Finding> {
let name = self.name().to_string();
let severity = self.severity();
for finding in &mut findings {
if self.name_override.is_some() {
finding.detector = name.clone();
}
if self.severity_override.is_some() {
finding.severity = severity;
}
}
findings
}
}
impl Detector for ConfiguredDetector {
fn name(&self) -> &str {
self.name_override
.as_deref()
.unwrap_or_else(|| self.inner.name())
}
fn severity(&self) -> Severity {
self.severity_override
.unwrap_or_else(|| self.inner.severity())
}
fn description(&self) -> &str {
self.inner.description()
}
fn detect(&self, module: &DamlModule) -> Vec<Finding> {
self.apply_overrides(self.inner.detect(module))
}
fn try_detect(&self, module: &DamlModule) -> Result<Vec<Finding>, DetectError> {
self.inner
.try_detect(module)
.map(|findings| self.apply_overrides(findings))
.map_err(|e| DetectError::new(self.name(), e.message().to_string()))
}
}
pub fn find_duplicate_detector_name(detectors: &[Box<dyn Detector>]) -> Option<String> {
let mut seen = std::collections::HashSet::new();
for det in detectors {
if !seen.insert(det.name()) {
return Some(det.name().to_string());
}
}
None
}
#[cfg(all(test, feature = "js-runtime"))]
mod tests {
use super::*;
#[test]
fn returns_none_when_detector_names_are_unique() {
assert_eq!(
find_duplicate_detector_name(&crate::detectors::create_builtin_detectors()),
None
);
}
#[test]
fn returns_duplicate_detector_name() {
let mut doubled = crate::detectors::create_builtin_detectors();
doubled.extend(crate::detectors::create_builtin_detectors());
assert!(find_duplicate_detector_name(&doubled).is_some());
}
}