1
1
use crate :: config:: Describe ;
2
+ use std:: cmp:: min;
3
+ use std:: collections:: HashSet ;
2
4
/*
3
5
* This file is part of CycloneDX Rust Cargo.
4
6
*
@@ -68,6 +70,35 @@ use validator::validate_email;
68
70
// Maps from PackageId to Package for efficiency - faster lookups than in a Vec
69
71
type PackageMap = BTreeMap < PackageId , Package > ;
70
72
type ResolveMap = BTreeMap < PackageId , Node > ;
73
+ type DependencyKindMap = BTreeMap < PackageId , DependencyKind > ;
74
+
75
+ /// The values are ordered from weakest to strongest so that casting to integer would make sense
76
+ #[ derive( Debug , PartialEq , Eq , PartialOrd , Ord , Copy , Clone , Hash ) ]
77
+ enum PrivateDepKind {
78
+ Development ,
79
+ Build ,
80
+ Runtime ,
81
+ }
82
+ impl From < PrivateDepKind > for DependencyKind {
83
+ fn from ( priv_kind : PrivateDepKind ) -> Self {
84
+ match priv_kind {
85
+ PrivateDepKind :: Development => DependencyKind :: Development ,
86
+ PrivateDepKind :: Build => DependencyKind :: Build ,
87
+ PrivateDepKind :: Runtime => DependencyKind :: Normal ,
88
+ }
89
+ }
90
+ }
91
+
92
+ impl From < & DependencyKind > for PrivateDepKind {
93
+ fn from ( kind : & DependencyKind ) -> Self {
94
+ match kind {
95
+ DependencyKind :: Normal => PrivateDepKind :: Runtime ,
96
+ DependencyKind :: Development => PrivateDepKind :: Development ,
97
+ DependencyKind :: Build => PrivateDepKind :: Build ,
98
+ _ => panic ! ( "Unknown dependency kind" ) ,
99
+ }
100
+ }
101
+ }
71
102
72
103
pub struct SbomGenerator {
73
104
config : SbomConfig ,
@@ -98,11 +129,13 @@ impl SbomGenerator {
98
129
for member in members. iter ( ) {
99
130
log:: trace!( "Processing the package {}" , member) ;
100
131
132
+ let dep_kinds = index_dep_kinds ( member, & resolve) ;
133
+
101
134
let ( dependencies, pruned_resolve) =
102
135
if config. included_dependencies ( ) == IncludedDependencies :: AllDependencies {
103
- all_dependencies ( member, & packages, & resolve)
136
+ all_dependencies ( member, & packages, & resolve, config )
104
137
} else {
105
- top_level_dependencies ( member, & packages, & resolve)
138
+ top_level_dependencies ( member, & packages, & resolve, config )
106
139
} ;
107
140
108
141
let manifest_path = packages[ member] . manifest_path . clone ( ) . into_std_path_buf ( ) ;
@@ -128,7 +161,7 @@ impl SbomGenerator {
128
161
crate_hashes,
129
162
} ;
130
163
let ( bom, target_kinds) =
131
- generator. create_bom ( member, & dependencies, & pruned_resolve) ?;
164
+ generator. create_bom ( member, & dependencies, & pruned_resolve, & dep_kinds ) ?;
132
165
133
166
let generated = GeneratedSbom {
134
167
bom,
@@ -149,14 +182,15 @@ impl SbomGenerator {
149
182
package : & PackageId ,
150
183
packages : & PackageMap ,
151
184
resolve : & ResolveMap ,
185
+ dep_kinds : & DependencyKindMap ,
152
186
) -> Result < ( Bom , TargetKinds ) , GeneratorError > {
153
187
let mut bom = Bom :: default ( ) ;
154
188
let root_package = & packages[ package] ;
155
189
156
190
let components: Vec < _ > = packages
157
191
. values ( )
158
192
. filter ( |p| & p. id != package)
159
- . map ( |component| self . create_component ( component, root_package) )
193
+ . map ( |component| self . create_component ( component, root_package, dep_kinds ) )
160
194
. collect ( ) ;
161
195
162
196
bom. components = Some ( Components ( components) ) ;
@@ -170,7 +204,12 @@ impl SbomGenerator {
170
204
Ok ( ( bom, target_kinds) )
171
205
}
172
206
173
- fn create_component ( & self , package : & Package , root_package : & Package ) -> Component {
207
+ fn create_component (
208
+ & self ,
209
+ package : & Package ,
210
+ root_package : & Package ,
211
+ dep_kinds : & DependencyKindMap ,
212
+ ) -> Component {
174
213
let name = package. name . to_owned ( ) . trim ( ) . to_string ( ) ;
175
214
let version = package. version . to_string ( ) ;
176
215
@@ -190,7 +229,13 @@ impl SbomGenerator {
190
229
) ;
191
230
192
231
component. purl = purl;
193
- component. scope = Some ( Scope :: Required ) ;
232
+ component. scope = match dep_kinds
233
+ . get ( & package. id )
234
+ . unwrap_or ( & DependencyKind :: Normal )
235
+ {
236
+ DependencyKind :: Normal => Some ( Scope :: Required ) ,
237
+ _ => Some ( Scope :: Excluded ) ,
238
+ } ;
194
239
component. external_references = Self :: get_external_references ( package) ;
195
240
component. licenses = self . get_licenses ( package) ;
196
241
component. hashes = self . get_hashes ( package) ;
@@ -206,7 +251,7 @@ impl SbomGenerator {
206
251
/// Same as [Self::create_component] but also includes information
207
252
/// on binaries and libraries comprising it as subcomponents
208
253
fn create_toplevel_component ( & self , package : & Package ) -> ( Component , TargetKinds ) {
209
- let mut top_component = self . create_component ( package, package) ;
254
+ let mut top_component = self . create_component ( package, package, & DependencyKindMap :: new ( ) ) ;
210
255
let mut subcomponents: Vec < Component > = Vec :: new ( ) ;
211
256
let mut target_kinds = HashMap :: new ( ) ;
212
257
for tgt in filter_targets ( & package. targets ) {
@@ -542,6 +587,57 @@ fn index_resolve(packages: Vec<Node>) -> ResolveMap {
542
587
. collect ( )
543
588
}
544
589
590
+ fn index_dep_kinds ( root : & PackageId , resolve : & ResolveMap ) -> DependencyKindMap {
591
+ // cache strongest found dependency kind for every node
592
+ let mut id_to_dep_kind: HashMap < PackageId , PrivateDepKind > = HashMap :: new ( ) ;
593
+ id_to_dep_kind. insert ( root. clone ( ) , PrivateDepKind :: Runtime ) ;
594
+
595
+ type DepNode = ( PackageId , PrivateDepKind , PrivateDepKind ) ;
596
+
597
+ let mut nodes_to_visit: Vec < DepNode > = vec ! [ ] ;
598
+ nodes_to_visit. push ( (
599
+ root. clone ( ) ,
600
+ PrivateDepKind :: Runtime ,
601
+ PrivateDepKind :: Runtime ,
602
+ ) ) ;
603
+
604
+ let mut visited_nodes: HashSet < DepNode > = HashSet :: new ( ) ;
605
+
606
+ // perform a simple iterative DFS over the dependencies,
607
+ // mark child deps with the minimum of parent kind and their own strongest value
608
+ // therefore e.g. mark decendants of build dependencies as build dependencies,
609
+ // as long as they never occur as normal dependency.
610
+ while let Some ( ( pkg_id, node_kind, path_node_kind) ) = nodes_to_visit. pop ( ) {
611
+ visited_nodes. insert ( ( pkg_id. clone ( ) , node_kind, path_node_kind) ) ;
612
+
613
+ let dep_kind_on_previous_visit = id_to_dep_kind. get ( & pkg_id) ;
614
+ // insert/update a nodes dependency kind, when its new or stronger than the previous value
615
+ if dep_kind_on_previous_visit. is_none ( )
616
+ || path_node_kind > * dep_kind_on_previous_visit. unwrap ( )
617
+ {
618
+ let _ = id_to_dep_kind. insert ( pkg_id. clone ( ) , path_node_kind) ;
619
+ }
620
+
621
+ let node = & resolve[ & pkg_id] ;
622
+ for child_dep in & node. deps {
623
+ for dep_kind in & child_dep. dep_kinds {
624
+ let current_kind = PrivateDepKind :: from ( & dep_kind. kind ) ;
625
+ let new_path_node_kind = min ( current_kind, path_node_kind) ;
626
+
627
+ let dep_node: DepNode = ( child_dep. pkg . clone ( ) , current_kind, new_path_node_kind) ;
628
+ if !visited_nodes. contains ( & dep_node) {
629
+ nodes_to_visit. push ( dep_node) ;
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ id_to_dep_kind
636
+ . iter ( )
637
+ . map ( |( x, y) | ( ( * x) . clone ( ) , DependencyKind :: from ( * y) ) )
638
+ . collect ( )
639
+ }
640
+
545
641
#[ derive( Error , Debug ) ]
546
642
pub enum GeneratorError {
547
643
#[ error( "Expected a root package in the cargo config: {config_filepath}" ) ]
@@ -584,13 +680,15 @@ fn top_level_dependencies(
584
680
root : & PackageId ,
585
681
packages : & PackageMap ,
586
682
resolve : & ResolveMap ,
683
+ config : & SbomConfig ,
587
684
) -> ( PackageMap , ResolveMap ) {
588
685
log:: trace!( "Adding top-level dependencies to SBOM" ) ;
589
686
590
687
// Only include packages that have dependency kinds other than "Development"
591
- let root_node = strip_dev_dependencies ( & resolve[ root] ) ;
688
+ let root_node = add_filtered_dependencies ( & resolve[ root] , config ) ;
592
689
593
690
let mut pkg_result = PackageMap :: new ( ) ;
691
+
594
692
// Record the root package, then its direct non-dev dependencies
595
693
pkg_result. insert ( root. to_owned ( ) , packages[ root] . to_owned ( ) ) ;
596
694
for id in & root_node. dependencies {
@@ -615,6 +713,7 @@ fn all_dependencies(
615
713
root : & PackageId ,
616
714
packages : & PackageMap ,
617
715
resolve : & ResolveMap ,
716
+ config : & SbomConfig ,
618
717
) -> ( PackageMap , ResolveMap ) {
619
718
log:: trace!( "Adding all dependencies to SBOM" ) ;
620
719
@@ -635,9 +734,11 @@ fn all_dependencies(
635
734
// If we haven't processed this node yet...
636
735
if !out_resolve. contains_key ( & node. id ) {
637
736
// Add the node to the output
638
- out_resolve. insert ( node. id . to_owned ( ) , strip_dev_dependencies ( node) ) ;
737
+ out_resolve. insert ( node. id . to_owned ( ) , add_filtered_dependencies ( node, config ) ) ;
639
738
// Queue its dependencies for the next BFS loop iteration
640
- next_queue. extend ( non_dev_dependencies ( & node. deps ) . map ( |dep| & resolve[ & dep. pkg ] ) ) ;
739
+ next_queue. extend (
740
+ filtered_dependencies ( & node. deps , config) . map ( |dep| & resolve[ & dep. pkg ] ) ,
741
+ ) ;
641
742
}
642
743
}
643
744
std:: mem:: swap ( & mut current_queue, & mut next_queue) ;
@@ -653,20 +754,27 @@ fn all_dependencies(
653
754
( out_packages, out_resolve)
654
755
}
655
756
656
- fn strip_dev_dependencies ( node : & Node ) -> Node {
757
+ fn add_filtered_dependencies ( node : & Node , config : & SbomConfig ) -> Node {
657
758
let mut node = node. clone ( ) ;
658
- node. deps = non_dev_dependencies ( & node. deps ) . cloned ( ) . collect ( ) ;
759
+ node. deps = filtered_dependencies ( & node. deps , config ) . cloned ( ) . collect ( ) ;
659
760
node. dependencies = node. deps . iter ( ) . map ( |d| d. pkg . to_owned ( ) ) . collect ( ) ;
660
761
node
661
762
}
662
763
663
764
/// Filters out dependencies only used for development, and not affecting the final binary.
664
765
/// These are specified under `[dev-dependencies]` in Cargo.toml.
665
- fn non_dev_dependencies ( input : & [ NodeDep ] ) -> impl Iterator < Item = & NodeDep > {
766
+ fn filtered_dependencies < ' a > (
767
+ input : & ' a [ NodeDep ] ,
768
+ config : & ' a SbomConfig ,
769
+ ) -> impl Iterator < Item = & ' a NodeDep > {
666
770
input. iter ( ) . filter ( |p| {
667
- p. dep_kinds
668
- . iter ( )
669
- . any ( |dep| dep. kind != DependencyKind :: Development )
771
+ p. dep_kinds . iter ( ) . any ( |dep| {
772
+ if let Some ( true ) = config. only_normal_deps {
773
+ dep. kind == DependencyKind :: Normal
774
+ } else {
775
+ dep. kind != DependencyKind :: Development
776
+ }
777
+ } )
670
778
} )
671
779
}
672
780
@@ -677,6 +785,7 @@ fn non_dev_dependencies(input: &[NodeDep]) -> impl Iterator<Item = &NodeDep> {
677
785
/// * `package_name` - Package from which this SBOM was generated
678
786
/// * `sbom_config` - Configuration options used during generation
679
787
/// * `target_kinds` - Detailed information on the kinds of targets in `sbom`
788
+ #[ derive( Debug ) ]
680
789
pub struct GeneratedSbom {
681
790
pub bom : Bom ,
682
791
pub manifest_path : PathBuf ,
0 commit comments