use geekorm::prelude::*;
use log::debug;
use super::{SecuritySeverity, advisories::AdvisoriesMetadata};
use crate::{
KonarrError,
bom::sbom::BomVulnerability,
models::{
Component, Dependencies, Snapshot,
security::{Advisories, AdvisorySource},
},
};
#[derive(Data, Debug, Clone, Default, PartialEq)]
pub enum SecurityState {
#[default]
Vulnerable,
Secure,
Unfixable,
}
#[derive(Table, Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Alerts {
#[geekorm(primary_key, auto_increment)]
pub id: PrimaryKey<i32>,
#[geekorm(searchable)]
pub name: String,
#[geekorm(new = "SecurityState::Vulnerable")]
pub state: SecurityState,
#[geekorm(foreign_key = "Snapshot.id")]
pub snapshot_id: ForeignKey<i32, Snapshot>,
#[geekorm(foreign_key = "Dependencies.id")]
pub dependency_id: ForeignKey<i32, Dependencies>,
#[geekorm(foreign_key = "Advisories.id")]
pub advisory_id: ForeignKey<i32, Advisories>,
#[serde(skip)]
#[geekorm(skip)]
pub metadata: Vec<AdvisoriesMetadata>,
#[geekorm(new = "chrono::Utc::now()")]
pub created_at: chrono::DateTime<chrono::Utc>,
#[geekorm(new = "chrono::Utc::now()")]
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl Alerts {
pub async fn find_or_create<'a, T>(&mut self, connection: &'a T) -> Result<(), geekorm::Error>
where
T: geekorm::GeekConnection<Connection = T> + 'a,
{
let item = Self::query_first(
connection,
Self::query_select()
.where_eq("name", self.name.clone())
.and()
.where_eq("snapshot_id", self.snapshot_id.clone())
.and()
.where_eq("dependency_id", self.dependency_id.clone())
.and()
.where_eq("advisory_id", self.advisory_id.clone())
.build()?,
)
.await;
match item {
Ok(alert) => {
self.id = alert.id;
}
_ => {
self.save(connection).await?;
}
}
Ok(())
}
pub async fn filter_severity<'a, T>(
connection: &'a T,
severity: SecuritySeverity,
page: &Page,
) -> Result<Vec<Self>, geekorm::Error>
where
T: geekorm::GeekConnection<Connection = T> + 'a,
{
let mut alerts = Alerts::query(
connection,
Alerts::query_select()
.join(Advisories::table())
.where_eq("Advisories.severity", severity)
.page(page)
.build()?,
)
.await?;
for alert in alerts.iter_mut() {
alert.fetch(connection).await?;
alert.fetch_metadata(connection).await?;
}
Ok(alerts)
}
pub async fn count_vulnerable<'a, T>(connection: &'a T) -> Result<u32, geekorm::Error>
where
T: geekorm::GeekConnection<Connection = T> + 'a,
{
Ok(Self::row_count(
connection,
Self::query_count()
.where_eq("state", SecurityState::Vulnerable)
.build()?,
)
.await? as u32)
}
pub async fn close<'a, T>(&mut self, connection: &'a T) -> Result<(), geekorm::Error>
where
T: geekorm::GeekConnection<Connection = T> + 'a,
{
self.state = SecurityState::Secure;
self.update(connection).await
}
pub async fn fetch_metadata<'a, T>(&mut self, connection: &'a T) -> Result<(), geekorm::Error>
where
T: geekorm::GeekConnection<Connection = T> + 'a,
{
self.metadata =
AdvisoriesMetadata::fetch_by_id(connection, self.advisory_id.data.name.clone()).await?;
Ok(())
}
pub async fn fetch_severity<'a, T>(
&mut self,
connection: &'a T,
) -> Result<SecuritySeverity, KonarrError>
where
T: GeekConnection<Connection = T> + 'a,
{
let advisory = self.fetch_advisory_id(connection).await?;
Ok(advisory.severity)
}
pub async fn from_bom_vulnerability<'a, T>(
connection: &'a T,
snapshot: &Snapshot,
vulnerability: &BomVulnerability,
) -> Result<Vec<Self>, KonarrError>
where
T: GeekConnection<Connection = T> + 'a,
{
let mut alerts = Vec::new();
for affected in &vulnerability.components {
let (mut component, _) = Component::from_purl(affected.purl.clone())?;
component.find_or_create(connection).await?;
debug!("Alert Component: {:?}", component);
let dependency = match Dependencies::fetch_dependency_by_snapshot(
connection,
snapshot.id,
component.id,
)
.await
{
Ok(dep) => dep,
Err(_) => {
log::error!(
"Failed to fetch dependency for BOM vulnerability: {}",
affected.purl
);
continue;
}
};
debug!("Alert Dependency: {:?}", dependency);
let mut advisory = Advisories::new(
vulnerability.name.clone(),
AdvisorySource::from(vulnerability.source.clone()),
SecuritySeverity::from(&vulnerability.severity),
);
advisory.fetch_or_create(connection).await?;
advisory.fetch_metadata(connection).await?;
if !advisory.has_metadata("description") {
if let Some(desc) = &vulnerability.description {
advisory
.add_metadata(connection, "description", desc)
.await?;
}
}
if !advisory.has_metadata("url") {
if let Some(url) = &vulnerability.url {
advisory.add_metadata(connection, "url", url).await?;
}
}
if !advisory.has_metadata("source") {
advisory
.add_metadata(connection, "source", &vulnerability.source)
.await?;
}
debug!("Alert Advisory: {:?}", advisory);
let mut alert = Alerts::new(
vulnerability.name.clone(),
snapshot.id,
dependency.id,
advisory.id,
);
alert.find_or_create(connection).await?;
if alert.state != SecurityState::Vulnerable {
alert.state = SecurityState::Vulnerable;
alert.update(connection).await?;
}
debug!("Alert: {:?}", alert);
alerts.push(alert);
}
Ok(alerts)
}
pub fn description(&self) -> Option<String> {
self.advisory_id
.data
.metadata
.iter()
.find(|m| m.key == "description")
.map(|m| m.value.clone())
}
pub fn url(&self) -> Option<String> {
self.advisory_id
.data
.metadata
.iter()
.find(|m| m.key == "url")
.map(|m| m.value.clone())
}
}
impl From<Option<String>> for SecurityState {
fn from(state: Option<String>) -> Self {
match state {
Some(value) => SecurityState::from(value),
None => SecurityState::Vulnerable,
}
}
}