1use anyhow::{Context as _, Result};
4
5#[derive(Debug, Clone)]
11pub struct CargoPackage {
12 pub name: String,
13 pub version: String,
14 pub source: Option<String>,
15}
16
17pub fn parse_cargo_lock(content: &str) -> Result<Vec<CargoPackage>> {
19 let parsed: toml::Value =
20 toml::from_str(content).context("sbom: failed to parse Cargo.lock as TOML")?;
21
22 let packages = parsed
23 .get("package")
24 .and_then(|p| p.as_array())
25 .map(|arr| {
26 arr.iter()
27 .filter_map(|entry| {
28 let name = entry.get("name")?.as_str()?.to_string();
29 let version = entry.get("version")?.as_str()?.to_string();
30 let source = entry
31 .get("source")
32 .and_then(|s| s.as_str())
33 .map(|s| s.to_string());
34 Some(CargoPackage {
35 name,
36 version,
37 source,
38 })
39 })
40 .collect()
41 })
42 .unwrap_or_default();
43
44 Ok(packages)
45}
46
47pub fn generate_cyclonedx(
54 project_name: &str,
55 version: &str,
56 timestamp: &str,
57 packages: &[CargoPackage],
58) -> Result<serde_json::Value> {
59 let components: Vec<serde_json::Value> = packages
60 .iter()
61 .map(|pkg| {
62 let mut component = serde_json::json!({
63 "type": "library",
64 "name": pkg.name,
65 "version": pkg.version,
66 "purl": format!("pkg:cargo/{}@{}", pkg.name, pkg.version),
67 });
68
69 if let Some(ref source) = pkg.source
70 && source.starts_with("registry+")
71 {
72 component["externalReferences"] = serde_json::json!([
73 {
74 "type": "distribution",
75 "url": format!("https://crates.io/crates/{}/{}", pkg.name, pkg.version)
76 }
77 ]);
78 }
79
80 component
81 })
82 .collect();
83
84 let sbom = serde_json::json!({
85 "bomFormat": "CycloneDX",
86 "specVersion": "1.5",
87 "version": 1,
88 "metadata": {
89 "timestamp": timestamp,
90 "component": {
91 "type": "application",
92 "name": project_name,
93 "version": version,
94 },
95 "tools": {
96 "components": [
97 {
98 "type": "application",
99 "name": "anodizer",
100 "publisher": "anodizer",
101 }
102 ]
103 }
104 },
105 "components": components,
106 });
107
108 Ok(sbom)
109}
110
111pub fn generate_spdx(
120 project_name: &str,
121 version: &str,
122 timestamp: &str,
123 namespace_uuid: &str,
124 packages: &[CargoPackage],
125) -> Result<serde_json::Value> {
126 let root_package = serde_json::json!({
128 "SPDXID": "SPDXRef-Package",
129 "name": project_name,
130 "versionInfo": version,
131 "downloadLocation": "NOASSERTION",
132 "filesAnalyzed": false,
133 });
134
135 let mut spdx_packages = vec![root_package];
136 let mut relationships = vec![serde_json::json!({
137 "spdxElementId": "SPDXRef-DOCUMENT",
138 "relatedSpdxElement": "SPDXRef-Package",
139 "relationshipType": "DESCRIBES",
140 })];
141
142 for (i, pkg) in packages.iter().enumerate() {
143 let spdx_id = format!("SPDXRef-Package-{}", i);
144
145 let download_location = if let Some(ref source) = pkg.source {
146 if source.starts_with("registry+") {
147 format!("https://crates.io/crates/{}/{}", pkg.name, pkg.version)
148 } else {
149 source.clone()
150 }
151 } else {
152 "NOASSERTION".to_string()
153 };
154
155 let pkg_entry = serde_json::json!({
156 "SPDXID": spdx_id,
157 "name": pkg.name,
158 "versionInfo": pkg.version,
159 "downloadLocation": download_location,
160 "filesAnalyzed": false,
161 "externalRefs": [
162 {
163 "referenceCategory": "PACKAGE-MANAGER",
164 "referenceType": "purl",
165 "referenceLocator": format!("pkg:cargo/{}@{}", pkg.name, pkg.version),
166 }
167 ],
168 });
169
170 spdx_packages.push(pkg_entry);
171
172 relationships.push(serde_json::json!({
173 "spdxElementId": "SPDXRef-Package",
174 "relatedSpdxElement": spdx_id,
175 "relationshipType": "DEPENDS_ON",
176 }));
177 }
178
179 let sbom = serde_json::json!({
180 "spdxVersion": "SPDX-2.3",
181 "dataLicense": "CC0-1.0",
182 "SPDXID": "SPDXRef-DOCUMENT",
183 "name": format!("{}-{}", project_name, version),
184 "documentNamespace": format!(
185 "https://spdx.org/spdxdocs/{}-{}-{}",
186 project_name, version, namespace_uuid,
187 ),
188 "creationInfo": {
189 "created": timestamp,
190 "creators": ["Tool: anodizer"],
191 },
192 "packages": spdx_packages,
193 "relationships": relationships,
194 });
195
196 Ok(sbom)
197}
198
199pub fn deterministic_uuid_from(seed: &str) -> String {
210 use std::collections::hash_map::DefaultHasher;
211 use std::hash::{Hash, Hasher};
212
213 let mut h1 = DefaultHasher::new();
214 seed.hash(&mut h1);
215 "anodizer-sbom-ns-v1".hash(&mut h1);
216 let h1 = h1.finish();
217
218 let mut h2 = DefaultHasher::new();
219 seed.hash(&mut h2);
220 "anodizer-sbom-ns-v2".hash(&mut h2);
221 let h2 = h2.finish();
222
223 format!(
224 "{:08x}-{:04x}-4{:03x}-{:04x}-{:012x}",
225 (h1 >> 32) as u32,
226 (h1 >> 16) as u16,
227 h1 as u16 & 0x0FFF,
228 (h2 >> 48) as u16 & 0x3FFF | 0x8000,
229 h2 & 0xFFFF_FFFF_FFFF,
230 )
231}