use crate::config::IgnoreCve;
use crate::i18n::Locale;
use crate::sbom_generation::domain::license_policy::LicensePolicy;
use crate::sbom_generation::domain::vulnerability::Severity;
use crate::shared::error::SbomError;
use crate::shared::Result;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct SbomRequest {
pub project_path: PathBuf,
pub include_dependency_info: bool,
pub exclude_patterns: Vec<String>,
pub dry_run: bool,
pub check_cve: bool,
pub severity_threshold: Option<Severity>,
pub cvss_threshold: Option<f32>,
pub ignore_cves: Vec<IgnoreCve>,
pub check_license: bool,
pub license_policy: Option<LicensePolicy>,
pub suggest_fix: bool,
pub locale: Locale,
}
impl SbomRequest {
pub fn builder() -> SbomRequestBuilder {
SbomRequestBuilder::new()
}
}
#[derive(Debug, Clone)]
pub struct SbomRequestBuilder {
project_path: Option<PathBuf>,
include_dependency_info: bool,
exclude_patterns: Vec<String>,
dry_run: bool,
check_cve: bool,
severity_threshold: Option<Severity>,
cvss_threshold: Option<f32>,
ignore_cves: Vec<IgnoreCve>,
check_license: bool,
license_policy: Option<LicensePolicy>,
suggest_fix: bool,
locale: Locale,
}
impl SbomRequestBuilder {
pub fn new() -> Self {
Self {
project_path: None,
include_dependency_info: false,
exclude_patterns: Vec::new(),
dry_run: false,
check_cve: false,
severity_threshold: None,
cvss_threshold: None,
ignore_cves: Vec::new(),
check_license: false,
license_policy: None,
suggest_fix: false,
locale: Locale::default(),
}
}
pub fn project_path(mut self, path: impl Into<PathBuf>) -> Self {
self.project_path = Some(path.into());
self
}
pub fn include_dependency_info(mut self, include: bool) -> Self {
self.include_dependency_info = include;
self
}
pub fn exclude_patterns(mut self, patterns: Vec<String>) -> Self {
self.exclude_patterns = patterns;
self
}
pub fn dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub fn check_cve(mut self, check: bool) -> Self {
self.check_cve = check;
self
}
pub fn severity_threshold_opt(mut self, severity: Option<Severity>) -> Self {
self.severity_threshold = severity;
self
}
pub fn cvss_threshold_opt(mut self, cvss: Option<f32>) -> Self {
self.cvss_threshold = cvss;
self
}
pub fn ignore_cves(mut self, cves: Vec<IgnoreCve>) -> Self {
self.ignore_cves = cves;
self
}
pub fn check_license(mut self, check: bool) -> Self {
self.check_license = check;
self
}
pub fn license_policy(mut self, policy: Option<LicensePolicy>) -> Self {
self.license_policy = policy;
self
}
pub fn suggest_fix(mut self, suggest: bool) -> Self {
self.suggest_fix = suggest;
self
}
pub fn locale(mut self, locale: Locale) -> Self {
self.locale = locale;
self
}
pub fn build(self) -> Result<SbomRequest> {
let project_path = self.project_path.ok_or_else(|| SbomError::Validation {
message: "project_path is required".into(),
})?;
Ok(SbomRequest {
project_path,
include_dependency_info: self.include_dependency_info,
exclude_patterns: self.exclude_patterns,
dry_run: self.dry_run,
check_cve: self.check_cve,
severity_threshold: self.severity_threshold,
cvss_threshold: self.cvss_threshold,
ignore_cves: self.ignore_cves,
check_license: self.check_license,
license_policy: self.license_policy,
suggest_fix: self.suggest_fix,
locale: self.locale,
})
}
}
impl Default for SbomRequestBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::IgnoreCve;
#[test]
fn test_builder_with_only_project_path() {
let request = SbomRequest::builder()
.project_path("/test/project")
.build()
.unwrap();
assert_eq!(request.project_path, PathBuf::from("/test/project"));
assert!(!request.include_dependency_info);
assert!(request.exclude_patterns.is_empty());
assert!(!request.dry_run);
assert!(!request.check_cve);
assert!(request.severity_threshold.is_none());
assert!(request.cvss_threshold.is_none());
assert!(request.ignore_cves.is_empty());
}
#[test]
fn test_builder_without_project_path_fails() {
let result = SbomRequest::builder().build();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("project_path is required"));
}
#[test]
fn test_builder_with_all_options() {
let request = SbomRequest::builder()
.project_path("/test/project")
.include_dependency_info(true)
.exclude_patterns(vec!["test-*".to_string()])
.dry_run(true)
.check_cve(true)
.severity_threshold_opt(Some(Severity::High))
.cvss_threshold_opt(Some(7.5))
.ignore_cves(vec![IgnoreCve {
id: "CVE-2024-1234".to_string(),
reason: Some("test".to_string()),
}])
.build()
.unwrap();
assert_eq!(request.project_path, PathBuf::from("/test/project"));
assert!(request.include_dependency_info);
assert_eq!(request.exclude_patterns, vec!["test-*".to_string()]);
assert!(request.dry_run);
assert!(request.check_cve);
assert_eq!(request.severity_threshold, Some(Severity::High));
assert_eq!(request.cvss_threshold, Some(7.5));
assert_eq!(request.ignore_cves.len(), 1);
assert_eq!(request.ignore_cves[0].id, "CVE-2024-1234");
}
#[test]
fn test_exclude_patterns_accumulates() {
let request = SbomRequest::builder()
.project_path("/test/project")
.exclude_patterns(vec![
"pattern1".to_string(),
"pattern2".to_string(),
"pattern3".to_string(),
])
.build()
.unwrap();
assert_eq!(request.exclude_patterns.len(), 3);
assert_eq!(
request.exclude_patterns,
vec!["pattern1", "pattern2", "pattern3"]
);
}
#[test]
fn test_exclude_patterns_replaces_previous() {
let request = SbomRequest::builder()
.project_path("/test/project")
.exclude_patterns(vec!["old-pattern".to_string()])
.exclude_patterns(vec!["new-pattern".to_string()])
.build()
.unwrap();
assert_eq!(request.exclude_patterns, vec!["new-pattern".to_string()]);
}
#[test]
fn test_severity_threshold_opt_with_some() {
let request = SbomRequest::builder()
.project_path("/test/project")
.severity_threshold_opt(Some(Severity::Critical))
.build()
.unwrap();
assert_eq!(request.severity_threshold, Some(Severity::Critical));
}
#[test]
fn test_severity_threshold_opt_with_none() {
let request = SbomRequest::builder()
.project_path("/test/project")
.severity_threshold_opt(None)
.build()
.unwrap();
assert!(request.severity_threshold.is_none());
}
#[test]
fn test_cvss_threshold_opt_with_some() {
let request = SbomRequest::builder()
.project_path("/test/project")
.cvss_threshold_opt(Some(8.0))
.build()
.unwrap();
assert_eq!(request.cvss_threshold, Some(8.0));
}
#[test]
fn test_cvss_threshold_opt_with_none() {
let request = SbomRequest::builder()
.project_path("/test/project")
.cvss_threshold_opt(None)
.build()
.unwrap();
assert!(request.cvss_threshold.is_none());
}
#[test]
fn test_builder_default_trait() {
let builder = SbomRequestBuilder::default();
let builder_new = SbomRequestBuilder::new();
assert!(builder.project_path.is_none());
assert!(builder_new.project_path.is_none());
assert!(!builder.include_dependency_info);
assert!(!builder_new.include_dependency_info);
}
#[test]
fn test_project_path_accepts_string() {
let request = SbomRequest::builder()
.project_path("./relative/path")
.build()
.unwrap();
assert_eq!(request.project_path, PathBuf::from("./relative/path"));
}
#[test]
fn test_project_path_accepts_pathbuf() {
let path = PathBuf::from("/absolute/path");
let request = SbomRequest::builder()
.project_path(path.clone())
.build()
.unwrap();
assert_eq!(request.project_path, path);
}
}