Skip to content

Commit

Permalink
Merge pull request #755 from thillux/strip-build-deps-2
Browse files Browse the repository at this point in the history
more explicit build dependency handling
  • Loading branch information
Shnatsel authored Jul 31, 2024
2 parents ebc5b4f + 52a8170 commit 6b262d3
Show file tree
Hide file tree
Showing 15 changed files with 317 additions and 32 deletions.
15 changes: 11 additions & 4 deletions cargo-cyclonedx/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ Defaults to the host target, as printed by 'rustc -vV'"
/// The CycloneDX specification version to output: `1.3`, `1.4` or `1.5`. Defaults to 1.3
#[clap(long = "spec-version")]
pub spec_version: Option<SpecVersion>,

/// List only dependencies of kind normal (no build deps, no dev deps)
#[clap(name = "no-build-deps", long = "no-build-deps")]
pub no_build_deps: bool,
}

impl Args {
Expand Down Expand Up @@ -170,6 +174,7 @@ impl Args {

let describe = self.describe;
let spec_version = self.spec_version;
let only_normal_deps = Some(self.no_build_deps);

Ok(SbomConfig {
format: self.format,
Expand All @@ -180,6 +185,7 @@ impl Args {
license_parser,
describe,
spec_version,
only_normal_deps,
})
}
}
Expand All @@ -190,6 +196,11 @@ pub enum ArgsError {
FilenameOverrideError(#[from] FilenameOverrideError),
}

#[cfg(test)]
pub fn parse_to_config(args: &[&str]) -> SbomConfig {
Args::parse_from(args.iter()).as_config().unwrap()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -222,10 +233,6 @@ mod tests {
assert!(!contains_feature(&config, ""));
}

fn parse_to_config(args: &[&str]) -> SbomConfig {
Args::parse_from(args.iter()).as_config().unwrap()
}

fn contains_feature(config: &SbomConfig, feature: &str) -> bool {
config
.features
Expand Down
2 changes: 2 additions & 0 deletions cargo-cyclonedx/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct SbomConfig {
pub license_parser: Option<LicenseParserOptions>,
pub describe: Option<Describe>,
pub spec_version: Option<SpecVersion>,
pub only_normal_deps: Option<bool>,
}

impl SbomConfig {
Expand All @@ -57,6 +58,7 @@ impl SbomConfig {
.or_else(|| self.license_parser.clone()),
describe: other.describe.or(self.describe),
spec_version: other.spec_version.or(self.spec_version),
only_normal_deps: other.only_normal_deps.or(self.only_normal_deps),
}
}

Expand Down
141 changes: 125 additions & 16 deletions cargo-cyclonedx/src/generator.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::config::Describe;
use std::cmp::min;
use std::collections::HashSet;
/*
* This file is part of CycloneDX Rust Cargo.
*
Expand Down Expand Up @@ -68,6 +70,35 @@ 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, Hash)]
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"),
}
}
}

pub struct SbomGenerator {
config: SbomConfig,
Expand Down Expand Up @@ -98,11 +129,13 @@ 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)
all_dependencies(member, &packages, &resolve, config)
} else {
top_level_dependencies(member, &packages, &resolve)
top_level_dependencies(member, &packages, &resolve, config)
};

let manifest_path = packages[member].manifest_path.clone().into_std_path_buf();
Expand All @@ -128,7 +161,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 +182,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 +204,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 +229,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 +251,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 +587,57 @@ 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.clone(), PrivateDepKind::Runtime);

type DepNode = (PackageId, PrivateDepKind, PrivateDepKind);

let mut nodes_to_visit: Vec<DepNode> = vec![];
nodes_to_visit.push((
root.clone(),
PrivateDepKind::Runtime,
PrivateDepKind::Runtime,
));

let mut visited_nodes: HashSet<DepNode> = HashSet::new();

// 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 let Some((pkg_id, node_kind, path_node_kind)) = nodes_to_visit.pop() {
visited_nodes.insert((pkg_id.clone(), node_kind, path_node_kind));

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

let node = &resolve[&pkg_id];
for child_dep in &node.deps {
for dep_kind in &child_dep.dep_kinds {
let current_kind = PrivateDepKind::from(&dep_kind.kind);
let new_path_node_kind = min(current_kind, path_node_kind);

let dep_node: DepNode = (child_dep.pkg.clone(), current_kind, new_path_node_kind);
if !visited_nodes.contains(&dep_node) {
nodes_to_visit.push(dep_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 @@ -584,13 +680,15 @@ fn top_level_dependencies(
root: &PackageId,
packages: &PackageMap,
resolve: &ResolveMap,
config: &SbomConfig,
) -> (PackageMap, ResolveMap) {
log::trace!("Adding top-level dependencies to SBOM");

// Only include packages that have dependency kinds other than "Development"
let root_node = strip_dev_dependencies(&resolve[root]);
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 All @@ -615,6 +713,7 @@ fn all_dependencies(
root: &PackageId,
packages: &PackageMap,
resolve: &ResolveMap,
config: &SbomConfig,
) -> (PackageMap, ResolveMap) {
log::trace!("Adding all dependencies to SBOM");

Expand All @@ -635,9 +734,11 @@ fn all_dependencies(
// If we haven't processed this node yet...
if !out_resolve.contains_key(&node.id) {
// Add the node to the output
out_resolve.insert(node.id.to_owned(), strip_dev_dependencies(node));
out_resolve.insert(node.id.to_owned(), add_filtered_dependencies(node, config));
// Queue its dependencies for the next BFS loop iteration
next_queue.extend(non_dev_dependencies(&node.deps).map(|dep| &resolve[&dep.pkg]));
next_queue.extend(
filtered_dependencies(&node.deps, config).map(|dep| &resolve[&dep.pkg]),
);
}
}
std::mem::swap(&mut current_queue, &mut next_queue);
Expand All @@ -653,20 +754,27 @@ fn all_dependencies(
(out_packages, out_resolve)
}

fn strip_dev_dependencies(node: &Node) -> Node {
fn add_filtered_dependencies(node: &Node, config: &SbomConfig) -> Node {
let mut node = node.clone();
node.deps = non_dev_dependencies(&node.deps).cloned().collect();
node.deps = filtered_dependencies(&node.deps, config).cloned().collect();
node.dependencies = node.deps.iter().map(|d| d.pkg.to_owned()).collect();
node
}

/// Filters out dependencies only used for development, and not affecting the final binary.
/// These are specified under `[dev-dependencies]` in Cargo.toml.
fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator<Item = &NodeDep> {
fn filtered_dependencies<'a>(
input: &'a [NodeDep],
config: &'a SbomConfig,
) -> impl Iterator<Item = &'a NodeDep> {
input.iter().filter(|p| {
p.dep_kinds
.iter()
.any(|dep| dep.kind != DependencyKind::Development)
p.dep_kinds.iter().any(|dep| {
if let Some(true) = config.only_normal_deps {
dep.kind == DependencyKind::Normal
} else {
dep.kind != DependencyKind::Development
}
})
})
}

Expand All @@ -677,6 +785,7 @@ fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator<Item = &NodeDep> {
/// * `package_name` - Package from which this SBOM was generated
/// * `sbom_config` - Configuration options used during generation
/// * `target_kinds` - Detailed information on the kinds of targets in `sbom`
#[derive(Debug)]
pub struct GeneratedSbom {
pub bom: Bom,
pub manifest_path: PathBuf,
Expand Down
Loading

0 comments on commit 6b262d3

Please sign in to comment.