Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion cargo-sbom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Options:
--cargo-package <CARGO_PACKAGE>
The specific package (in a Cargo workspace) to generate an SBOM for. If not specified this is all packages in the workspace.
--output-format <OUTPUT_FORMAT>
The SBOM output format. [default: spdx_json_2_3] [possible values: spdx_json_2_3, cyclone_dx_json_1_4, cyclone_dx_json_1_6]
The SBOM output format. [default: spdx_json_2_3] [possible values: spdx_json_2_3, spdx_json_3_0_1, cyclone_dx_json_1_4, cyclone_dx_json_1_5, cyclone_dx_json_1_6]
--project-directory <PROJECT_DIRECTORY>
The directory to the Cargo project. [default: .]
-h, --help
Expand Down Expand Up @@ -76,6 +76,25 @@ $ cargo sbom
<rest of output omitted>
```

### Create a SPDX 3.0.1 SBOM for a Cargo project

```shell
$ cargo sbom --output-format=spdx_json_3_0_1
{
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
"@graph": [
{
"type": "CreationInfo",
"@id": "_:creationinfo",
"createdBy": ["http://spdx.example.com/Agent/cargo-sbom-v0.10.0"],
"specVersion": "3.0.1",
"created": "2024-03-06T00:00:00Z"
},
<rest of output omitted>
]
}
```

### Create a CycloneDx SBOM in Github Actions

In a Github Actions workflow:
Expand Down
33 changes: 32 additions & 1 deletion cargo-sbom/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
//! --cargo-package <CARGO_PACKAGE>
//! The specific package (in a Cargo workspace) to generate an SBOM for. If not specified this is all packages in the workspace.
//! --output-format <OUTPUT_FORMAT>
//! The SBOM output format. [default: spdx_json_2_3] [possible values: spdx_json_2_3, cyclone_dx_json_1_4, cyclone_dx_json_1_5, cyclone_dx_json_1_6]
//! The SBOM output format. [default: spdx_json_2_3] [possible values: spdx_json_2_3, spdx_json_3_0_1, cyclone_dx_json_1_4, cyclone_dx_json_1_5, cyclone_dx_json_1_6]
//! --project-directory <PROJECT_DIRECTORY>
//! The directory to the Cargo project. [default: .]
//! -h, --help
Expand Down Expand Up @@ -76,6 +76,24 @@
//! <rest of output omitted>
//! ```
//!
//! ### Create a SPDX 3.0.1 SBOM for a Cargo project
//!
//! ```shell
//! $ cargo sbom --output-format=spdx_json_3_0_1
//! {
//! "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
//! "@graph": [
//! {
//! "type": "CreationInfo",
//! "@id": "_:creationinfo",
//! "createdBy": ["http://spdx.example.com/Agent/cargo-sbom-v0.10.0"],
//! "specVersion": "3.0.1",
//! "created": "2024-03-06T00:00:00Z"
//! }
//! ]
//! }
//! ```
//!
//! ### Create a CycloneDx SBOM in Github Actions
//!
//! In a Github Actions workflow:
Expand Down Expand Up @@ -205,6 +223,7 @@ pub mod built_info {
#[allow(non_camel_case_types)]
enum OutputFormat {
SpdxJson_2_3,
SpdxJson_3_0_1,
CycloneDxJson_1_4,
CycloneDxJson_1_5,
CycloneDxJson_1_6,
Expand Down Expand Up @@ -327,6 +346,18 @@ fn try_main() -> Result<()> {
"{}",
serde_json::to_string_pretty(&spdx)?
)?;
} else if matches!(opt.output_format, OutputFormat::SpdxJson_3_0_1) {
let spdx = util::spdx::convert_3_0_1(
opt.cargo_package,
&opt.project_directory,
&cargo_manifest_path,
&graph,
)?;
writeln!(
std::io::stdout(),
"{}",
serde_json::to_string_pretty(&spdx)?
)?;
}

Ok(())
Expand Down
171 changes: 171 additions & 0 deletions cargo-sbom/src/util/spdx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,174 @@ pub fn convert(
.build()?,
)
}

/// Generate SPDX 3.0.1 element ID
fn generate_spdx_3_0_1_id(package: &Package) -> String {
format!(
"http://spdx.example.com/Package/{}/{}",
package.name, package.version
)
}

pub fn convert_3_0_1(
cargo_package: Option<String>,
project_directory: &Path,
_cargo_manifest_path: &Path,
graph: &Graph,
) -> Result<serde_spdx::spdx::v_3_0_1::Spdx> {
use serde_json::json;

let mut graph_elements = vec![];
let current_time =
chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

// Create CreationInfo element
let creation_info_json = json!({
"type": "CreationInfo",
"@id": "_:creationinfo",
"createdBy": [format!("http://spdx.example.com/Agent/{}-v{}", built_info::PKG_NAME, built_info::PKG_VERSION)],
"specVersion": "3.0.1",
"created": current_time
});

graph_elements.push(creation_info_json);

// Create Person element for the tool
let person_json = json!({
"type": "Person",
"spdxId": format!("http://spdx.example.com/Agent/{}-v{}", built_info::PKG_NAME, built_info::PKG_VERSION),
"name": format!("{} v{}", built_info::PKG_NAME, built_info::PKG_VERSION),
"creationInfo": "_:creationinfo"
});

graph_elements.push(person_json);

let mut package_ids = vec![];
let mut relationship_elements = vec![];

// Process packages
for root_package_id in graph.root_packages.iter() {
let root_node_index = graph
.nodes
.get(root_package_id)
.ok_or(anyhow!("No root node. Shouldn't reach here."))?;
let root = graph.graph[*root_node_index];
if let Some(r) = cargo_package.as_ref() {
if r != &root.name {
continue;
}
}

let mut dfs = petgraph::visit::Dfs::new(&graph.graph, *root_node_index);
while let Some(nx) = dfs.next(&graph.graph) {
let edges = graph.graph.edges(nx);
let package = graph.graph[nx];

let package_id = generate_spdx_3_0_1_id(package);
package_ids.push(package_id.clone());

let normalized_license = package
.license
.as_ref()
.and_then(|license| license::normalize_license_string(license).ok());

let mut package_json = json!({
"type": "software_Package",
"spdxId": package_id,
"name": package.name,
"downloadLocation": package
.source
.as_ref()
.map(|source| source.to_string())
.unwrap_or("NONE".to_string()),
"filesAnalyzed": false,
"licenseConcluded": normalized_license.as_deref().unwrap_or("NOASSERTION"),
"copyrightText": "NOASSERTION",
"creationInfo": "_:creationinfo",
"versionInfo": package.version.to_string()
});

if let Some(license_declared) = normalized_license {
package_json["licenseDeclared"] = json!(license_declared);
}

if let Some(description) = package.description.as_ref() {
package_json["description"] = json!(description);
}

graph_elements.push(package_json);

// Create relationship elements
edges.for_each(|e| {
let source = &graph.graph[e.source()];
let target = &graph.graph[e.target()];
let relationship_id =
format!("_:relationship-{}-{}", source.name, target.name);

let relationship_json = json!({
"type": "Relationship",
"spdxId": relationship_id,
"relationshipType": "dependsOn",
"from": generate_spdx_3_0_1_id(source),
"to": [generate_spdx_3_0_1_id(target)],
"creationInfo": "_:creationinfo"
});

relationship_elements.push(relationship_json);
});
}
}

// Add relationships to graph
graph_elements.extend(relationship_elements);

// Create SBOM element
let absolute_project_directory = project_directory.canonicalize()?;
let manifest_folder = absolute_project_directory
.file_name()
.ok_or(anyhow!("Failed to determine parent folder of Cargo.toml. Unable to assign a SPDX document name."))?;
let name = cargo_package
.unwrap_or_else(|| manifest_folder.to_string_lossy().to_string());

let sbom_id = format!("http://spdx.example.com/Sbom/{}", name);
let sbom_json = json!({
"type": "software_Sbom",
"spdxId": sbom_id,
"creationInfo": "_:creationinfo",
"rootElement": package_ids
});

graph_elements.push(sbom_json);

// Create SpdxDocument
let document_id = format!("http://spdx.example.com/Document/{}", name);
let mut element_ids = vec![sbom_id];
element_ids.extend(package_ids);
element_ids.push(format!(
"http://spdx.example.com/Agent/{}-v{}",
built_info::PKG_NAME,
built_info::PKG_VERSION
));

let document_json = json!({
"type": "SpdxDocument",
"spdxId": document_id,
"creationInfo": "_:creationinfo",
"rootElement": [format!("http://spdx.example.com/Sbom/{}", name)],
"element": element_ids,
"profileConformance": ["core", "software"]
});

graph_elements.push(document_json);

// Create the final SPDX structure
let spdx_json = json!({
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
"@graph": graph_elements
});

// Convert to our struct
let spdx: serde_spdx::spdx::v_3_0_1::Spdx =
serde_json::from_value(spdx_json)?;
Ok(spdx)
}
4 changes: 3 additions & 1 deletion serde-spdx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ default = []
[dependencies]
anyhow = "1.0.98"
derive_builder = "0.20.0"
serde = "1.0.204"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
thiserror = "2.0.12"

[dev-dependencies]

[build-dependencies]
anyhow = "1.0.98"
prettyplease = "0.2.25"
Expand Down
22 changes: 16 additions & 6 deletions serde-spdx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# serde-spdx

This crate provides a type safe [serde](https://serde.rs/) compatible
[SPDX](https://spdx.dev/) format. It is intended for use
in Rust code which may need to read or write SPDX files.
[SPDX](https://spdx.dev/) format. It supports both SPDX 2.3 and SPDX 3.0.1 formats.
It is intended for use in Rust code which may need to read or write SPDX files.

The latest [documentation can be found here](https://docs.rs/serde_spdx).

Expand All @@ -18,10 +18,11 @@ the official website:

## Usage

For most cases, simply use the root [spdx::v_2_3::Spdx] struct with [serde] to read
and write to and from the struct.
For most cases, simply use the root [spdx::v_2_3::Spdx] struct for SPDX 2.3 or
[spdx::v_3_0_1::Spdx] struct for SPDX 3.0.1 with [serde] to read and write to and
from the struct.

## Example
## SPDX 2.3 Example

```rust
use serde_spdx::spdx::v_2_3::Spdx;
Expand All @@ -30,11 +31,20 @@ let data = fs::read_to_string("sbom.spdx.json");
let spdx: Spdx = serde_json::from_str(&data).unwrap();
```

## SPDX 3.0.1 Example

```rust
use serde_spdx::spdx::v_3_0_1::Spdx;

let data = fs::read_to_string("sbom.spdx.jsonld");
let spdx: Spdx = serde_json::from_str(&data).unwrap();
```

Because many of the [spdx::v_2_3::Spdx] structures contain a lot of optional fields,
it is often convenient to use the builder pattern to contstruct these structs.
Each structure has a builder with a default.

## Example
## Builder Example

```rust
use serde_spdx::spdx::v_2_3::SpdxCreationInfoBuilder;
Expand Down
1 change: 1 addition & 0 deletions serde-spdx/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ fn generate_schema(version_str: &str) -> Result<()> {

fn main() -> Result<()> {
generate_schema("2_3")?;
generate_schema("3_0_1")?;

Ok(())
}
Loading
Loading