Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: cargo-sbom #3553

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from 3 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
121 changes: 121 additions & 0 deletions text/0000-cargo-sbom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
- Feature Name: `cargo-sbom`
- Start Date: 2023-11-01
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)

# Summary
[summary]: #summary

This RFC adds an option to Cargo that emits information for building a Software Bill of Materials (SBOM) precursor file alongside compiled artifacts. Similar to how Cargo "dep-info" (.d) files, this change emits SBOM data in a Cargo-specific format alongside outputs in the `target` directory. External tools or Cargo subcommands can consume this file and transform it into an SBOM such as SPDX or CycloneDX.

# Motivation
[motivation]: #motivation

An SBOM (software bill of materials) is a list of all components and dependencies used to build a piece of software. The two leading SBOM formats being adopted by industry are SPDX and CycloneDX. Both are still evolving and have multiple specification versions & data formats (JSON, XML).

New government initiatives aimed at improving the security of the software supply chain such as the US "Executive Order 14028: Improving the Nation's Cybersecurity" or the EU "Cyber Resilience Act" require a Software Bill of Materials. Generating accurate SBOMs with Cargo is currently difficult because, depending on target selection or activated features, the dependencies may be different.

For workspaces that generate multiple compiled artifacts, each artifact may have different dependencies referenced. Existing tools (see prior art section) attempt to approximate the correct dependency set, however precise dependency information for each compiled artifact is difficult without built-in Cargo support. Generating the SBOM information at the same time as the compiled artifact allows precise dependency information to be emitted for each compiled artifact.

# Guide-level explanation
[guide-level-explanation]: #guide-level-explanation

The generation of SBOM information is controlled by Cargo's configuration. To enable SBOM generation, set the following configuration:

```toml
[build]
sbom = true
```

Or use the environment variable `CARGO_BUILD_SBOM=true`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should we actually call this?

And is this a build param or a profile param?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought a build parameter made most sense, since there's 1 place to easily control it.

  • A CI environment wants to collect SBOM information on all binaries produced, so it sets CARGO_BUILD_SBOM=true
  • A Cargo subcommand wants to enable SBOM generation when re-invoking Cargo, so it sets the flag.

If we use profiles, then it becomes harder for tooling wrapping Cargo to unconditionally enable it for the current run.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile does not preclude environment variables because the manifest profile is layered with the config profile.

$ CARGO_PROFILE_DEV_OPT_LEVEL=10 cargo check
    Checking utf8parse v0.2.1
    Checking anstyle-query v1.0.0
    Checking colorchoice v1.0.0
    Checking anstyle v1.0.2
    Checking strsim v0.10.0
    Checking clap_lex v0.6.0 (/home/epage/src/personal/clap/clap_lex)
error: optimization level needs to be between 0-3, s or z (instead was `10`)

error: could not compile `anstyle-query` (lib) due to previous error
warning: build failed, waiting for other jobs to finish...
error: could not compile `clap_lex` (lib) due to previous error
error: could not compile `strsim` (lib) due to previous error
error: could not compile `utf8parse` (lib) due to previous error
error: could not compile `anstyle` (lib) due to previous error
error: could not compile `colorchoice` (lib) due to previous error

But it looks like the layering is all-or-nothing so setting one value might be ignored or cause other values to be ignored. That might be too disruptive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should capture this reasoning within the RFC's rationale section

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 for having this in a profile, because I very much expect people will want to have this in some profiles and not in others.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile for now is a set of compiler settings. I am not sure if we want to expand the meaning of it to include SBOM. Also given SBOM is only meaningful to final artifacts, if we put it in profiles we need to document profile.debug.sbom not working for dependencies. We already have some confusions for lto and abort (rust-lang/cargo#9330).

If people want to switch build configurations, should we work on stabilizing config-include instead?


If enabled, an SBOM file will be placed next to each compiled artifact for `bin`, `staticlib`, `cdylib` crate types in the `target` directory with the name `<artifact>.cargo-sbom.json`. The SBOM will contain information about dependencies used to build the compiled artifact.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

The SBOM file generated by Cargo is *not* intended as a final SBOM artifact, but rather a precursor. Post-processing tooling can use the information produced here as part of building a final SBOM.

The SBOM file will be written to disk before `rustc` is executed for the each artifact. This enables [`RUSTC_WORKSPACE_WRAPPER`](https://doc.rust-lang.org/cargo/reference/config.html#buildrustc-workspace-wrapper) to point at a program that can utilize the SBOM file to embed information into the binary if desired.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the SBOM precursor file will be written first, is there an intention to remove it if the production of the artifact, including perhaps the execution of any RUSTC_WORKSPACE_WRAPPER, fails?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion on this. Do you think it needs to be specified in the RFC?


## Format
The format will use JSON, but the exact format is not specified in this RFC. Additional fields can be added as needed. The JSON will include a `format-version` in case breaking changes are necessary.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean the config needs to support specifying the format version so people can opt-in to the new version or leave that as a future possibility? Do we tie this to the default (when its true) to the edition?


### Resolved Dependency Tree
The SBOM file will include the following information for each crate. Note that a crate refers to a [single build unit (library or executable)](https://doc.rust-lang.org/cargo/appendix/glossary.html#crate).
A [package](https://doc.rust-lang.org/cargo/appendix/glossary.html#package) may contain multiple crates.
- Package ID
- Name
- Version
- Source (registry / git / path etc.)
- Checksum (if available)
- Dependencies
- Type (normal, build)
- Activated features
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include env variables (I assume rustc reports to us what it read for us to fingerprint) or file paths (again, I assume rustc tells us what it read to fingerprint)?

Copy link
Member

@Noratrieb Noratrieb Jan 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rustc mentions all read env vars in depinfo. This does not include env vars read by proc macros through std::env::var.
Depinfo also contains the read files, but as far as I'm aware, the standard library sources aren't included yet (-Zbinary-dep-depinfo enables that).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note this can also be moved to a future possibility so long as we ensure the format can support this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NTIA released Minimum Elements for a SBOM. Currently, mainstream SBOM formats (e.g SPDX, CycloneDX, SWID ) are all extend from this minimum elements and add some optional fields.
May be we can also set the format align with the NITA minimum elements, like add author field

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 3052 intentionally made the author field optional.

Any SBOM initiative that can't cope with anonymously/pseudonymously authored code is overreach imo. The code can be perfectly viable. And it doesn't change the fact that authors can vanish or give false contact information anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As is mentioned below this section, you can query other metadata by looking up the dependency in cargo metadata based on the id, if the crate has published author metadata it will be available there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 3052 intentionally made the author field optional.

Any SBOM initiative that can't cope with anonymously/pseudonymously authored code is overreach imo. The code can be perfectly viable. And it doesn't change the fact that authors can vanish or give false contact information anyway.

I know what you mean. Maybe we're talking about two dimensions, and you're right from a security point of view. Simply from the point of view of this being a tool, providing the author field may be a better way for developers to use this tool to generate SBOM. :)


If a crate is used as both a normal dependency and a build dependency that is separately compiled, then separate entries will exist in the dependency tree with the correct activated features listed for each instance.

Checksum is an optional field, since only crates from registries have checksums. If a checksum is needed for a crate that comes from a path dependency for example, it will be up to the post-processing tool to produce an appropriate value.

If further information is needed (such as license), then the post-processing tool can use `cargo metadata` or another mechanism to find it.

### Resolved build confugration
- Rust toolchain version
- `RUSTFLAGS`
- Current build profile name
- Selected profile values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is any other config relevant to include?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we defer out all config to a future possibility so long as we make sure the format can support it?


## Security

Cargo's SBOM file provides a report of the components and dependencies used by cargo to build a software artifact.

Cargo does not defend against malicious components or dependencies changing the SBOM, or accidentally or maliciously concealing themselves from the SBOM. In particular, components or dependencies added by build scripts or external tools might not be accurately represented in the SBOM file produced by Cargo. Ideally, tools should provide their own SBOMs, and build scripts should modify the SBOM via supported cargo interfaces (see future possibilities).

# Drawbacks
[drawbacks]: #drawbacks

It introduces yet another SBOM format. However, the format is specifically designed to be used as an intermediate, to be converted to an industry-standard format by external tooling.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

Since there is no consensus on a single SBOM format within the software industry, and existing formats are still evolving, Cargo should not pick an existing SBOM format. If Cargo were to use existing SBOM formats, multiple formats (and multiple versions of each format) would need to be supported. The task of generating a specific SBOM format is best left to applications outside Cargo or Cargo extension. Cargo is not in a position to produce an SBOM that is compliant with internal and external regulations.

Unfortunately it's difficult to extract accurate SBOM information with existing options. Using the `Cargo.lock` file or `cargo metadata` overincludes dependencies. Additionally, since Cargo has many different commands that produce compiled artifacts (build, test, bench, etc.) and each of these commands take arguments that can affect the dependency list it's difficult to ensure that the correct dependency list is used.

Adding an option to `cargo metadata` to support resolver v2 would help with overinclusion of dependencies, but still makes it difficult to ensure the exact set of features, command-line arguments, and other options match the command used to produce the final artifact. The CLI would need substantial changes to allow users to control package and build target selection. Additionally, since build scripts (`build.rs`) may impact the output, `cargo metadata` may need to execute them.

Another alternative is to extract information by setting the `RUSTC_WRAPPER` environment variable, then capture feature flags and dependencies via a wrapper tool. This would require the wrapper tool to parse the rustc command line arguments to capture the set of feature flags and referenced dependencies. This approach would prevent other uses of `RUSTC_WRAPPER`, as well as being potentially fragile.

# Prior art
[prior-art]: #prior-art

* [RFC2801](https://github.com/rust-lang/rfcs/pull/2801): Proposes embedding dependency information directly into the binary. Implemented as the `cargo auditable` extension.
* [cargo-auditable](https://github.com/rust-secure-code/cargo-auditable): Cargo extension that embeds a subset of the information described in this RFC directly into the binary. The JSON format used by this RFC could be based on the cargo-auditable format.
* [cargo-cyclonedx](https://github.com/CycloneDX/cyclonedx-rust-cargo): Cargo extension to generate a CycloneDX SBOM.
* [cargo-bom](https://github.com/sensorfu/cargo-bom): Cargo extension to generate a BOM in an ASCII format including license information.
* [cargo build-plan (#5579)](https://github.com/rust-lang/cargo/issues/5579): Provides an option to emit a JSON representation of the commands to execute, without actually running them. This option has poor integration with `build.rs` and was [planned for deletion](https://github.com/rust-lang/cargo/issues/7614) in 2018.
* [cargo unit graph (#8002)](https://github.com/rust-lang/cargo/issues/8002): Very similar to what this RFC intends on writing to disk. However, since unit-graph runs wihtout a build, it cannot take `build.rs` output into account.

# Unresolved questions
[unresolved-questions]: #unresolved-questions

The exact specifics about what will be included in the SBOM and the specific JSON format are subject to change during the implementation of the RFC.

# Future possibilities
[future-possibilities]: #future-possibilities

Additional fields can be added to the SBOM without a breaking change as new requirements are identified such as:
* Environment variables read
* Additional profile flags
* External tool versions (linker, C compiler, OS)

## Build scripts
Build scripts could communicate back to Cargo to inject additional dependencies into the SBOM. For example, if a crate builds `C` code and then links with it, it would emit a build script instruction that causes Cargo to read in a file describing the `C` dependency.
```
cargo::sbom=<PATH>
````
Cargo would then include the additional dependency information in the SBOM graph.

## Embedding dependency information into binaries
The implementation of [RFC2801](https://github.com/rust-lang/rfcs/pull/2801) could be based on the information provided by this RFC.