-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathlib.rs
More file actions
130 lines (119 loc) · 4.7 KB
/
lib.rs
File metadata and controls
130 lines (119 loc) · 4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#![forbid(unsafe_code)]
use std::str::FromStr;
pub use auditable_serde;
use auditable_serde::{Package, Source};
pub use cyclonedx_bom;
use cyclonedx_bom::models::property::{Properties, Property};
use cyclonedx_bom::prelude::*;
use cyclonedx_bom::{
external_models::uri::Purl,
models::{
component::Classification,
component::Component,
dependency::{Dependencies, Dependency},
metadata::Metadata,
},
};
/// Converts the metadata embedded by `cargo auditable` to a minimal CycloneDX document
/// that is heavily optimized to reduce the size
pub fn auditable_to_minimal_cdx(input: &auditable_serde::VersionInfo) -> Bom {
let mut bom = Bom {
serial_number: None, // the serial number would mess with reproducible builds
..Default::default()
};
// The toplevel component goes into its own field, as per the spec:
// https://cyclonedx.org/docs/1.5/json/#metadata_component
let (root_idx, root_pkg) = root_package(input);
let root_component = pkg_to_component(root_pkg, root_idx);
let metadata = Metadata {
component: Some(root_component),
..Default::default()
};
bom.metadata = Some(metadata);
// Fill in the component list, excluding the toplevel component (already encoded)
let components: Vec<Component> = input
.packages
.iter()
.enumerate()
.filter(|(_idx, pkg)| !pkg.root)
.map(|(idx, pkg)| pkg_to_component(pkg, idx))
.collect();
let components = Components(components);
bom.components = Some(components);
// Populate the dependency tree. Actually really easy, it's the same format as ours!
let dependencies: Vec<Dependency> = input
.packages
.iter()
.enumerate()
.map(|(idx, pkg)| Dependency {
dependency_ref: idx.to_string(),
dependencies: pkg.dependencies.iter().map(|idx| idx.to_string()).collect(),
})
.collect();
let dependencies = Dependencies(dependencies);
bom.dependencies = Some(dependencies);
// Validate the generated SBOM if running in debug mode (or release with debug assertions)
if cfg!(debug_assertions) {
assert!(bom.validate().passed());
}
bom
}
fn pkg_to_component(pkg: &auditable_serde::Package, idx: usize) -> Component {
let component_type = if pkg.root {
Classification::Application
} else {
Classification::Library
};
// The only requirement for `bom_ref` according to the spec is that it's unique,
// so we just keep the unique numbering already used in the original
let bom_ref = idx.to_string();
let mut result = Component::new(
component_type,
&pkg.name,
&pkg.version.to_string(),
Some(bom_ref),
);
// PURL encodes the package origin (registry, git, local) - sort of, anyway
let purl = purl(pkg);
let purl = Purl::from_str(&purl).unwrap();
result.purl = Some(purl);
// Record the dependency kind
match pkg.kind {
// `Runtime` is the default and does not need to be recorded.
auditable_serde::DependencyKind::Runtime => (),
auditable_serde::DependencyKind::Build => {
let p = Property::new("cdx:rustc:dependency_kind".to_owned(), "build");
result.properties = Some(Properties(vec![p]));
}
}
result
}
fn root_package(input: &auditable_serde::VersionInfo) -> (usize, &Package) {
// we can unwrap here because VersionInfo is already validated during deserialization
input
.packages
.iter()
.enumerate()
.find(|(_idx, pkg)| pkg.root)
.expect("VersionInfo contains no root package!")
}
fn purl(pkg: &auditable_serde::Package) -> String {
// The purl crate exposed by `cyclonedx-bom` doesn't support the qualifiers we need,
// so we just build the PURL as a string.
// Yeah, we could use *yet another* dependency to build the PURL,
// but we use it such trivial ways that it isn't worth the trouble.
// Specifically, the crate names that crates.io accepts don't need percent-encoding
// and the fixed values we put in arguments don't either
// (but percent-encoding is underspecified and not interoperable anyway,
// see e.g. https://github.com/package-url/purl-spec/pull/261)
let mut purl = format!("pkg:cargo/{}@{}", pkg.name, pkg.version);
purl.push_str(match &pkg.source {
Source::CratesIo => "", // this is the default, nothing to qualify
Source::Git => "&vcs_url=redacted",
Source::Local => "&download_url=redacted",
Source::Registry => "&repository_url=redacted",
Source::Other(_) => "&download_url=redacted",
unknown => panic!("Unknown source: {unknown:?}"),
});
purl
}