Skip to content

Commit

Permalink
track dependency kinds in order to set scope
Browse files Browse the repository at this point in the history
Signed-off-by: Markus Theil <theil.markus@gmail.com>
  • Loading branch information
thillux committed Jul 10, 2024
1 parent 4bc0721 commit 401638a
Showing 1 changed file with 101 additions and 6 deletions.
107 changes: 101 additions & 6 deletions cargo-cyclonedx/src/generator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::config::Describe;
use std::cmp::min;
/*
* This file is part of CycloneDX Rust Cargo.
*
Expand Down Expand Up @@ -54,8 +55,8 @@ use once_cell::sync::Lazy;
use regex::Regex;

use log::Level;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::{BTreeMap, LinkedList};
use std::convert::TryFrom;
use std::fs::File;
use std::io::BufWriter;
Expand All @@ -68,6 +69,42 @@ use validator::validate_email;
// Maps from PackageId to Package for efficiency - faster lookups than in a Vec
type PackageMap = BTreeMap<PackageId, Package>;
type ResolveMap = BTreeMap<PackageId, Node>;
type DependencyKindMap = BTreeMap<PackageId, DependencyKind>;

/// The values are ordered from weakest to strongest so that casting to integer would make sense
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
enum PrivateDepKind {
Development,
Build,
Runtime,
}
impl From<PrivateDepKind> for DependencyKind {
fn from(priv_kind: PrivateDepKind) -> Self {
match priv_kind {
PrivateDepKind::Development => DependencyKind::Development,
PrivateDepKind::Build => DependencyKind::Build,
PrivateDepKind::Runtime => DependencyKind::Normal,
}
}
}

impl From<&DependencyKind> for PrivateDepKind {
fn from(kind: &DependencyKind) -> Self {
match kind {
DependencyKind::Normal => PrivateDepKind::Runtime,
DependencyKind::Development => PrivateDepKind::Development,
DependencyKind::Build => PrivateDepKind::Build,
_ => panic!("Unknown dependency kind"),
}
}
}

fn strongest_dep_kind(deps: &[cargo_metadata::DepKindInfo]) -> PrivateDepKind {
deps.iter()
.map(|d| PrivateDepKind::from(&d.kind))
.max()
.unwrap_or(PrivateDepKind::Runtime) // for compatibility with Rust earlier than 1.41
}

pub struct SbomGenerator {
config: SbomConfig,
Expand Down Expand Up @@ -98,6 +135,8 @@ impl SbomGenerator {
for member in members.iter() {
log::trace!("Processing the package {}", member);

let dep_kinds = index_dep_kinds(member, &resolve);

let (dependencies, pruned_resolve) =
if config.included_dependencies() == IncludedDependencies::AllDependencies {
all_dependencies(member, &packages, &resolve, config)
Expand Down Expand Up @@ -128,7 +167,7 @@ impl SbomGenerator {
crate_hashes,
};
let (bom, target_kinds) =
generator.create_bom(member, &dependencies, &pruned_resolve)?;
generator.create_bom(member, &dependencies, &pruned_resolve, &dep_kinds)?;

let generated = GeneratedSbom {
bom,
Expand All @@ -149,14 +188,15 @@ impl SbomGenerator {
package: &PackageId,
packages: &PackageMap,
resolve: &ResolveMap,
dep_kinds: &DependencyKindMap,
) -> Result<(Bom, TargetKinds), GeneratorError> {
let mut bom = Bom::default();
let root_package = &packages[package];

let components: Vec<_> = packages
.values()
.filter(|p| &p.id != package)
.map(|component| self.create_component(component, root_package))
.map(|component| self.create_component(component, root_package, dep_kinds))
.collect();

bom.components = Some(Components(components));
Expand All @@ -170,7 +210,12 @@ impl SbomGenerator {
Ok((bom, target_kinds))
}

fn create_component(&self, package: &Package, root_package: &Package) -> Component {
fn create_component(
&self,
package: &Package,
root_package: &Package,
dep_kinds: &DependencyKindMap,
) -> Component {
let name = package.name.to_owned().trim().to_string();
let version = package.version.to_string();

Expand All @@ -190,7 +235,13 @@ impl SbomGenerator {
);

component.purl = purl;
component.scope = Some(Scope::Required);
component.scope = match dep_kinds
.get(&package.id)
.unwrap_or(&DependencyKind::Normal)
{
DependencyKind::Normal => Some(Scope::Required),
_ => Some(Scope::Excluded),
};
component.external_references = Self::get_external_references(package);
component.licenses = self.get_licenses(package);
component.hashes = self.get_hashes(package);
Expand All @@ -206,7 +257,7 @@ impl SbomGenerator {
/// Same as [Self::create_component] but also includes information
/// on binaries and libraries comprising it as subcomponents
fn create_toplevel_component(&self, package: &Package) -> (Component, TargetKinds) {
let mut top_component = self.create_component(package, package);
let mut top_component = self.create_component(package, package, &DependencyKindMap::new());
let mut subcomponents: Vec<Component> = Vec::new();
let mut target_kinds = HashMap::new();
for tgt in filter_targets(&package.targets) {
Expand Down Expand Up @@ -542,6 +593,49 @@ fn index_resolve(packages: Vec<Node>) -> ResolveMap {
.collect()
}

fn index_dep_kinds(root: &PackageId, resolve: &ResolveMap) -> DependencyKindMap {
// cache strongest found dependency kind for every node
let mut id_to_dep_kind: HashMap<&PackageId, PrivateDepKind> = HashMap::new();
id_to_dep_kind.insert(root, PrivateDepKind::Runtime);

let root_node = &resolve[root];
let mut nodes_to_visit = LinkedList::<&Node>::new();
nodes_to_visit.push_front(root_node);

// perform a simple iterative DFS over the dependencies,
// mark child deps with the minimum of parent kind and their own strongest value
// therefore e.g. mark decendants of build dependencies as build dependencies,
// as long as they never occur as normal dependency.
while !nodes_to_visit.is_empty() {
let node = nodes_to_visit.pop_front().unwrap();
let parent_node_kind = id_to_dep_kind[&node.id];

for child_dep in &node.deps {
let child_node = &resolve[&child_dep.pkg];
let dep_kind = strongest_dep_kind(child_dep.dep_kinds.as_slice());
let child_node_kind = min(parent_node_kind, dep_kind);

let dep_kind_on_previous_visit = id_to_dep_kind.get(&child_node.id);
// insert/update a nodes dependency kind, when its new or stronger than the previous value
if dep_kind_on_previous_visit.is_none()
|| child_node_kind > *dep_kind_on_previous_visit.unwrap()
{
let _ = id_to_dep_kind.insert(&child_node.id, child_node_kind);
}

// cargo metadata already return a DAG, so assume this will terminate
// and don't mark already visited notes. It's ok and necessary to visit
// deps as often as they occur.
nodes_to_visit.push_front(child_node);
}
}

id_to_dep_kind
.iter()
.map(|(x, y)| ((*x).clone(), DependencyKind::from(*y)))
.collect()
}

#[derive(Error, Debug)]
pub enum GeneratorError {
#[error("Expected a root package in the cargo config: {config_filepath}")]
Expand Down Expand Up @@ -592,6 +686,7 @@ fn top_level_dependencies(
let root_node = add_filtered_dependencies(&resolve[root], config);

let mut pkg_result = PackageMap::new();

// Record the root package, then its direct non-dev dependencies
pkg_result.insert(root.to_owned(), packages[root].to_owned());
for id in &root_node.dependencies {
Expand Down

0 comments on commit 401638a

Please sign in to comment.