Skip to content

Commit

Permalink
Return simplified SBOM status for a given vulnerability.
Browse files Browse the repository at this point in the history
All SBOMs referencing purls that have a status from an advisory
are evaluated. If a given SBOM resolves to a set of statuses
(which may be a combination of statuses, such as "fixed" *and* "not_affected")
those should be considered the absolute status of that SBOM _per that advisory_.

For an SBOM that contains *no* status set, the status shall be interpreted
as _possibly affected_ but otherwise not asserted either way.
  • Loading branch information
bobmcwhirter committed Jul 12, 2024
1 parent 995e050 commit a2810d5
Show file tree
Hide file tree
Showing 5 changed files with 444 additions and 116 deletions.
202 changes: 202 additions & 0 deletions common/src/cpe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,208 @@ impl FromStr for Cpe {
}
}

pub trait CpeCompare: cpe::cpe::Cpe {
fn is_superset<O: CpeCompare>(&self, other: &O) -> bool {
self.compare(other).superset()
}

fn compare<O: CpeCompare>(&self, other: &O) -> CpeCmpResult {
let part = if self.part() != other.part() {
CpeCmp::Disjoint
} else {
CpeCmp::Equal
};

let vendor = Self::component_compare(self.vendor(), other.vendor());
let product = Self::component_compare(self.product(), other.product());
let version = Self::component_compare(self.version(), other.version());
let update = Self::component_compare(self.update(), other.update());
let edition = Self::component_compare(self.edition(), other.edition());
let language = Self::language_compare(self.language(), other.language());

CpeCmpResult {
part,
vendor,
product,
version,
update,
edition,
language,
}
}

fn language_compare(source: &cpe::cpe::Language, target: &cpe::cpe::Language) -> CpeCmp {
match (source, target) {
(cpe::cpe::Language::Any, _) => CpeCmp::Superset,
(_, cpe::cpe::Language::Any) => CpeCmp::Subset,
(
cpe::cpe::Language::Language(source_lang),
cpe::cpe::Language::Language(target_lang),
) => {
if source_lang == target_lang {
CpeCmp::Equal
} else {
CpeCmp::Disjoint
}
}
}
}

fn component_compare(
source: cpe::component::Component,
target: cpe::component::Component,
) -> CpeCmp {
if source == target {
return CpeCmp::Equal;
}

match (source, target) {
(
cpe::component::Component::Value(source_val),
cpe::component::Component::Value(target_val),
) => {
if source_val.to_lowercase() == target_val.to_lowercase() {
CpeCmp::Equal
} else {
CpeCmp::Disjoint
}
}
(cpe::component::Component::Any, _) => CpeCmp::Superset,
(_, cpe::component::Component::Any) => CpeCmp::Subset,
(cpe::component::Component::NotApplicable, _)
| (_, cpe::component::Component::NotApplicable) => CpeCmp::Subset,
}
}
}

impl<T: cpe::cpe::Cpe> CpeCompare for T {
// defaults are perfectly sufficient.
}

#[allow(unused)]
pub enum CpeCmp {
Undefined,
Superset,
Equal,
Subset,
Disjoint,
}

pub struct CpeCmpResult {
part: CpeCmp,
vendor: CpeCmp,
product: CpeCmp,
version: CpeCmp,
update: CpeCmp,
edition: CpeCmp,
language: CpeCmp,
}

#[allow(unused)]
impl CpeCmpResult {
pub fn disjoint(&self) -> bool {
matches!(self.part, CpeCmp::Disjoint)
|| matches!(self.vendor, CpeCmp::Disjoint)
|| matches!(self.product, CpeCmp::Disjoint)
|| matches!(self.version, CpeCmp::Disjoint)
|| matches!(self.update, CpeCmp::Disjoint)
|| matches!(self.edition, CpeCmp::Disjoint)
|| matches!(self.language, CpeCmp::Disjoint)
}

pub fn superset(&self) -> bool {
matches!(self.part, CpeCmp::Superset | CpeCmp::Equal)
&& matches!(self.vendor, CpeCmp::Superset | CpeCmp::Equal)
&& matches!(self.product, CpeCmp::Superset | CpeCmp::Equal)
&& matches!(self.version, CpeCmp::Superset | CpeCmp::Equal)
&& matches!(self.update, CpeCmp::Superset | CpeCmp::Equal)
&& matches!(self.edition, CpeCmp::Superset | CpeCmp::Equal)
&& matches!(self.language, CpeCmp::Superset | CpeCmp::Disjoint)
}

pub fn subset(&self) -> bool {
matches!(self.part, CpeCmp::Subset | CpeCmp::Equal)
&& matches!(self.vendor, CpeCmp::Subset | CpeCmp::Equal)
&& matches!(self.product, CpeCmp::Subset | CpeCmp::Equal)
&& matches!(self.version, CpeCmp::Subset | CpeCmp::Equal)
&& matches!(self.update, CpeCmp::Subset | CpeCmp::Equal)
&& matches!(self.edition, CpeCmp::Subset | CpeCmp::Equal)
&& matches!(self.language, CpeCmp::Subset | CpeCmp::Disjoint)
}

pub fn equal(&self) -> bool {
matches!(self.part, CpeCmp::Equal)
&& matches!(self.vendor, CpeCmp::Equal)
&& matches!(self.product, CpeCmp::Equal)
&& matches!(self.version, CpeCmp::Equal)
&& matches!(self.update, CpeCmp::Equal)
&& matches!(self.edition, CpeCmp::Equal)
&& matches!(self.language, CpeCmp::Disjoint)
}
}

#[macro_export]
macro_rules! apply {
($c: expr, $v:expr => $n:ident) => {
if let Some($n) = &$v.$n {
$c.$n($n);
}
};
($c: expr, $v:expr => $n:ident, $($m:ident),+) => {
apply!($c, $v => $n );
apply!($c, $v => $($m),+)
};
}

#[macro_export]
macro_rules! apply_fix {
($c: expr, $v:expr => $n:ident) => {
if let Some($n) = &$v.$n {
if $n == "*" {
$c.$n("");
} else {
$c.$n($n);
}

}
};
($c: expr, $v:expr => $n:ident, $($m:tt),+) => {
apply_fix!($c, $v => $n );
apply_fix!($c, $v => $($m),+)
};
}

#[macro_export]
macro_rules! impl_try_into_cpe {
($ty:ty) => {
impl TryInto<cpe::uri::OwnedUri> for &$ty {
type Error = cpe::error::CpeError;

fn try_into(self) -> Result<cpe::uri::OwnedUri, Self::Error> {
use $crate::apply_fix;
use $crate::apply;

let mut cpe = Uri::builder();

apply!(cpe, self => part);
apply_fix!(cpe, self => vendor, product, version, update, edition);

// apply the fix for the language field

if let Some(language) = &self.language {
if language == "*" {
cpe.language("ANY");
} else {
cpe.language(language);
}
}

Ok(cpe.validate()?.to_owned())
}
}
};
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
6 changes: 6 additions & 0 deletions entity/src/package_relates_to_package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,9 @@ pub enum Relation {
}

impl ActiveModelBehavior for ActiveModel {}

impl Related<super::sbom::Entity> for Entity {
fn to() -> RelationDef {
Relation::Sbom.def()
}
}
8 changes: 8 additions & 0 deletions entity/src/sbom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub enum Relation {
Files,
#[sea_orm(has_one = "super::sbom_node::Entity")]
Node,
#[sea_orm(has_many = "super::package_relates_to_package::Entity")]
PackageRelatesToPackages,
}

pub struct SbomNodeLink;
Expand Down Expand Up @@ -63,6 +65,12 @@ impl Related<super::sbom_node::Entity> for Entity {
}
}

impl Related<super::package_relates_to_package::Entity> for Entity {
fn to() -> RelationDef {
Relation::PackageRelatesToPackages.def()
}
}

impl ActiveModelBehavior for ActiveModel {}

impl TryFilterForId for Entity {
Expand Down
Loading

0 comments on commit a2810d5

Please sign in to comment.