WP-20/21: contract and failing tests for vulnerability management
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@ -494,3 +494,150 @@ impl ClusterGateway for MemCluster {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
use domain::ports::{FindingRepository, VulnerabilityScanner};
|
||||
use domain::vuln::{
|
||||
Finding, FindingFilter, FindingStatus, RawFinding, Severity, SeverityCounts, TargetKind,
|
||||
};
|
||||
|
||||
pub fn raw(
|
||||
cve: &str,
|
||||
pkg: &str,
|
||||
installed: &str,
|
||||
sev: Severity,
|
||||
fixed: Option<&str>,
|
||||
) -> RawFinding {
|
||||
RawFinding {
|
||||
cve_id: cve.into(),
|
||||
severity: sev,
|
||||
package: pkg.into(),
|
||||
installed_version: installed.into(),
|
||||
fixed_version: fixed.map(String::from),
|
||||
title: format!("{cve} in {pkg}"),
|
||||
url: format!("https://nvd.nist.gov/vuln/detail/{cve}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub type ScanResults = Arc<Mutex<HashMap<String, Result<Vec<RawFinding>, String>>>>;
|
||||
|
||||
/// Scanner returning configurable results per target ("os" or image ref).
|
||||
#[derive(Default)]
|
||||
pub struct FakeScanner {
|
||||
pub results: ScanResults,
|
||||
}
|
||||
|
||||
impl FakeScanner {
|
||||
pub fn with(self, target: &str, r: Result<Vec<RawFinding>, &str>) -> Self {
|
||||
self.results
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(target.into(), r.map_err(String::from));
|
||||
self
|
||||
}
|
||||
fn get(&self, target: &str) -> Result<Vec<RawFinding>, DomainError> {
|
||||
match self.results.lock().unwrap().get(target) {
|
||||
Some(Ok(v)) => Ok(v.clone()),
|
||||
Some(Err(e)) => Err(DomainError::Unavailable(e.clone())),
|
||||
None => Ok(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VulnerabilityScanner for FakeScanner {
|
||||
async fn version(&self) -> Result<String, DomainError> {
|
||||
Ok("fake 0.1".into())
|
||||
}
|
||||
async fn scan_os(
|
||||
&self,
|
||||
out: &dyn domain::ports::LineSink,
|
||||
) -> Result<Vec<RawFinding>, DomainError> {
|
||||
out.line("scanning os");
|
||||
self.get("os")
|
||||
}
|
||||
async fn scan_image(
|
||||
&self,
|
||||
image: &str,
|
||||
out: &dyn domain::ports::LineSink,
|
||||
) -> Result<Vec<RawFinding>, DomainError> {
|
||||
out.line(&format!("scanning {image}"));
|
||||
self.get(image)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MemFindings(pub Mutex<Vec<Finding>>);
|
||||
|
||||
#[async_trait]
|
||||
impl FindingRepository for MemFindings {
|
||||
async fn active_by_target(&self, target: &str) -> Result<Vec<Finding>, DomainError> {
|
||||
Ok(self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|f| f.target == target && f.status != FindingStatus::Fixed)
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
async fn insert(&self, finding: &Finding) -> Result<(), DomainError> {
|
||||
self.0.lock().unwrap().push(finding.clone());
|
||||
Ok(())
|
||||
}
|
||||
async fn touch(
|
||||
&self,
|
||||
ids: &[Uuid],
|
||||
last_seen: chrono::DateTime<Utc>,
|
||||
) -> Result<(), DomainError> {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter_mut()
|
||||
.filter(|f| ids.contains(&f.id))
|
||||
.for_each(|f| f.last_seen = last_seen);
|
||||
Ok(())
|
||||
}
|
||||
async fn set_status(&self, id: Uuid, status: FindingStatus) -> Result<(), DomainError> {
|
||||
let mut v = self.0.lock().unwrap();
|
||||
let f = v
|
||||
.iter_mut()
|
||||
.find(|f| f.id == id)
|
||||
.ok_or(DomainError::NotFound)?;
|
||||
f.status = status;
|
||||
Ok(())
|
||||
}
|
||||
async fn get(&self, id: Uuid) -> Result<Option<Finding>, DomainError> {
|
||||
Ok(self.0.lock().unwrap().iter().find(|f| f.id == id).cloned())
|
||||
}
|
||||
async fn list(&self, filter: &FindingFilter) -> Result<Vec<Finding>, DomainError> {
|
||||
let mut v: Vec<Finding> = self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|f| filter.include_fixed || f.status != FindingStatus::Fixed)
|
||||
.filter(|f| filter.min_severity.is_none_or(|m| f.raw.severity >= m))
|
||||
.filter(|f| filter.target.as_ref().is_none_or(|t| &f.target == t))
|
||||
.filter(|f| filter.status.is_none_or(|s| f.status == s))
|
||||
.cloned()
|
||||
.collect();
|
||||
v.sort_by(|a, b| {
|
||||
b.raw
|
||||
.severity
|
||||
.cmp(&a.raw.severity)
|
||||
.then(a.raw.cve_id.cmp(&b.raw.cve_id))
|
||||
});
|
||||
Ok(v)
|
||||
}
|
||||
async fn counts(&self, kind: Option<TargetKind>) -> Result<SeverityCounts, DomainError> {
|
||||
let mut c = SeverityCounts::default();
|
||||
for f in
|
||||
self.0.lock().unwrap().iter().filter(|f| {
|
||||
f.status != FindingStatus::Fixed && kind.is_none_or(|k| f.target_kind == k)
|
||||
})
|
||||
{
|
||||
c.add(f.raw.severity);
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user