Skip to content
Open
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
24 changes: 24 additions & 0 deletions completions/bun-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -3372,6 +3372,30 @@
"flags": [],
"positionalArgs": []
},
"sbom": {
"name": "sbom",
"description": "generate a Software Bill of Materials (SBOM) from the lockfile",
"flags": [
{
"name": "format",
"description": "Output format: cyclonedx (default) or spdx",
"hasValue": true,
"valueType": "val",
"required": false,
"multiple": false
},
{
"name": "outfile",
"shortName": "o",
"description": "Write the SBOM to a file instead of stdout",
"hasValue": true,
"valueType": "val",
"required": false,
"multiple": false
}
],
"positionalArgs": []
},
"why": {
"name": "why",
"description": "<pkg> show dependency tree explaining why a package is installed",
Expand Down
19 changes: 19 additions & 0 deletions docs/pm/cli/pm.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,25 @@ From a workspace root, every workspace's dependencies are listed; from inside a

Requires both `bun.lock` and `node_modules`. Packages in the lockfile but missing from `node_modules` (e.g. after `bun install --production`) are skipped with a warning.

## sbom

Generate a Software Bill of Materials (SBOM) from the lockfile in [CycloneDX 1.7](https://cyclonedx.org/) or [SPDX 2.3](https://spdx.dev/) JSON format:

```bash terminal icon="terminal"
# CycloneDX 1.7 (default) to stdout
bun pm sbom

# SPDX 2.3 to a file
bun pm sbom --format spdx -o sbom.spdx.json
```

For every package in the lockfile the SBOM includes the name and resolved version, a [purl](https://github.com/package-url/purl-spec) identifier, the download location (registry tarball URL, or a `git+<url>@<revision>` locator for git dependencies), the integrity hash, and the full dependency graph. Dev dependencies are marked `excluded` (CycloneDX) / `DEV_DEPENDENCY_OF` (SPDX) and optional dependencies are marked `optional` / `OPTIONAL_DEPENDENCY_OF`, so downstream scanners like Grype, Trivy, or Dependency-Track can distinguish production from development packages.

| Flag | Description |
| ---------------------- | ------------------------------------------ |
| `--format <format>` | `cyclonedx` (default) or `spdx` |
| `-o, --outfile <path>` | Write the SBOM to a file instead of stdout |

## whoami

Print your npm username. Requires you to be logged in (`bunx npm login`) with credentials in either `bunfig.toml` or `.npmrc`:
Expand Down
3 changes: 3 additions & 0 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ impl PackageManagerCommand {
<b><green>bun pm<r> <blue>ls<r> list the dependency tree according to the current lockfile
<d>├<r> <cyan>--all<r> list the entire dependency tree according to the current lockfile
<d>└<r> <cyan>--trusted<r> list only trusted dependencies
<b><green>bun pm<r> <blue>sbom<r> generate a Software Bill of Materials (SBOM)
<d>├<r> <cyan>--format<r> cyclonedx (default) or spdx
<d>└<r> <cyan>-o, --outfile<r> write the SBOM to a file instead of stdout
<b><green>bun pm<r> <blue>why<r> <d>\<pkg\><r> show dependency tree explaining why a package is installed
<b><green>bun pm<r> <blue>licenses<r> list installed packages grouped by license
<d>├<r> <cyan>--json<r> output as JSON
Expand Down
17 changes: 17 additions & 0 deletions src/install/PackageManager/CommandLineArguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ pub(crate) static PM_PARAMS: &[ParamType] = concat_params![
clap::param!(
"--depth <NUM> Maximum depth of the dependency tree to display"
),
clap::param!(
"--format <STR> SBOM output format: cyclonedx (default) or spdx"
),
clap::param!(
"-o, --outfile <STR> Write the SBOM to a file instead of stdout"
),
clap::param!("<POS> ... "),
]
];
Expand Down Expand Up @@ -550,6 +556,10 @@ pub struct CommandLineArguments {
pub dev_only: bool,
pub long: bool,

// `bun pm sbom` options
pub sbom_format: Option<&'static [u8]>,
pub sbom_outfile: Option<&'static [u8]>,

// `bun audit` options
pub audit_level: Option<AuditLevel>,
pub audit_ignore_list: &'static [&'static [u8]],
Expand Down Expand Up @@ -637,6 +647,9 @@ impl Default for CommandLineArguments {
dev_only: false,
long: false,

sbom_format: None,
sbom_outfile: None,

audit_level: None,
audit_ignore_list: &[],

Expand Down Expand Up @@ -1726,6 +1739,10 @@ Full documentation is available at <magenta>https://bun.com/docs/pm/cli/prune<r>
}
cli.dev_only = args.flag(b"--dev");
cli.long = args.flag(b"--long");

// `bun pm sbom` command options
cli.sbom_format = args.option(b"--format");
cli.sbom_outfile = args.option(b"--outfile");
}

// `bun pm why` and `bun why` options
Expand Down
10 changes: 10 additions & 0 deletions src/install/PackageManager/PackageManagerOptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ pub struct Options {
pub top_only: bool,
pub depth: Option<usize>,

// `bun pm sbom` command options
pub sbom_format: Option<&'static [u8]>,
pub sbom_outfile: Option<&'static [u8]>,

/// isolated installs (pnpm-like) or hoisted installs (yarn-like, original)
pub node_linker: NodeLinker,

Expand Down Expand Up @@ -152,6 +156,8 @@ impl Default for Options {
force: false,
top_only: false,
depth: None,
sbom_format: None,
sbom_outfile: None,
node_linker: NodeLinker::Auto,
public_hoist_pattern: None,
hoist_pattern: None,
Expand Down Expand Up @@ -890,6 +896,10 @@ impl Options {
// `bun pm why` command options
self.top_only = cli.top_only;
self.depth = cli.depth;

// `bun pm sbom` command options
self.sbom_format = cli.sbom_format;
self.sbom_outfile = cli.sbom_outfile;
} else {
self.log_level = if default_disable_progress_bar {
LogLevel::DefaultNoProgress
Expand Down
8 changes: 4 additions & 4 deletions src/install/integrity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ impl Integrity {
Integrity { value: out, tag }
}

pub(crate) fn slice(&self) -> &[u8] {
pub fn slice(&self) -> &[u8] {
&self.value[0..self.tag.digest_len()]
}

Expand Down Expand Up @@ -280,13 +280,13 @@ unsafe impl bytemuck::NoUninit for Tag {}
impl Tag {
pub(crate) const UNKNOWN: Tag = Tag(0);
/// "shasum" in the metadata
pub(crate) const SHA1: Tag = Tag(1);
pub const SHA1: Tag = Tag(1);
/// The value is a [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) value
pub const SHA256: Tag = Tag(2);
/// The value is a [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) value
pub(crate) const SHA384: Tag = Tag(3);
pub const SHA384: Tag = Tag(3);
/// The value is a [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) value
pub(crate) const SHA512: Tag = Tag(4);
pub const SHA512: Tag = Tag(4);

#[inline]
pub fn is_supported(self) -> bool {
Expand Down
2 changes: 1 addition & 1 deletion src/install/lockfile/Package/Meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub struct Meta {
pub(crate) id: PackageID,

pub(crate) man_dir: String,
pub(crate) integrity: Integrity,
pub integrity: Integrity,

/// Shouldn't be used directly. Use `Meta.has_install_script()` and
/// `Meta.set_has_install_script()` instead.
Expand Down
14 changes: 7 additions & 7 deletions src/install/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ impl<SemverInt: VersionInt> ResolutionType<SemverInt> {
}
/// `git` or `github` payload — they share the [`Repository`] shape.
#[inline]
pub(crate) fn repository(&self) -> &Repository {
pub fn repository(&self) -> &Repository {
debug_assert!(self.tag == Tag::Git || self.tag == Tag::Github);
// SAFETY: `git` and `github` occupy the same union slot type
// (`Repository`); tag asserted to be one of the two.
Expand Down Expand Up @@ -923,22 +923,22 @@ impl Default for Tag {

#[allow(non_upper_case_globals)]
impl Tag {
pub(crate) const Uninitialized: Tag = Tag(0);
pub const Uninitialized: Tag = Tag(0);
pub const Root: Tag = Tag(1);
pub const Npm: Tag = Tag(2);
pub const Folder: Tag = Tag(4);

pub(crate) const LocalTarball: Tag = Tag(8);
pub const LocalTarball: Tag = Tag(8);

pub(crate) const Github: Tag = Tag(16);
pub const Github: Tag = Tag(16);

pub(crate) const Git: Tag = Tag(32);
pub const Git: Tag = Tag(32);

pub const Symlink: Tag = Tag(64);

pub const Workspace: Tag = Tag(72);

pub(crate) const RemoteTarball: Tag = Tag(80);
pub const RemoteTarball: Tag = Tag(80);

// This is a placeholder for now.
// But the intent is to eventually support URL imports at the package manager level.
Expand All @@ -957,7 +957,7 @@ impl Tag {
// This is similar to how Go does it, except it wouldn't clone the whole repo.
// There are more efficient ways to do this, e.g. generate a .bun file just for all URL imports.
// There are questions of determinism, but perhaps that's what Integrity would do.
pub(crate) const SingleFileModule: Tag = Tag(100);
pub const SingleFileModule: Tag = Tag(100);
}

impl Tag {
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ pub(crate) mod patch_commit_command;
pub(crate) mod pm_licenses_command;
#[path = "pm_pkg_command.rs"]
pub mod pm_pkg_command;
#[path = "pm_sbom_command.rs"]
pub mod pm_sbom_command;
#[path = "pm_trusted_command.rs"]
pub mod pm_trusted_command;
pub mod pm_update_package_json;
Expand Down
7 changes: 7 additions & 0 deletions src/runtime/cli/package_manager_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use bun_sys::{self, Dir, Fd, File};
use crate::cli::Command;
use crate::cli::pm_licenses_command::{LicensesFlags, PmLicensesCommand};
use crate::cli::pm_pkg_command::PmPkgCommand;
use crate::cli::pm_sbom_command::PmSbomCommand;
use crate::cli::pm_trusted_command::{DefaultTrustedCommand, TrustCommand, UntrustedCommand};
use crate::cli::pm_version_command::PmVersionCommand;
use crate::cli::pm_view_command as PmViewCommand;
Expand Down Expand Up @@ -187,6 +188,9 @@ impl PackageManagerCommand {
<b><green>bun pm<r> <blue>ls<r> list the dependency tree according to the current lockfile\n\
<d>├<r> <cyan>--all<r> list the entire dependency tree according to the current lockfile\n\
<d>└<r> <cyan>--trusted<r> list only trusted dependencies\n\
<b><green>bun pm<r> <blue>sbom<r> generate a Software Bill of Materials (SBOM)\n\
<d>├<r> <cyan>--format<r> cyclonedx (default) or spdx\n\
<d>└<r> <cyan>-o, --outfile<r> write the SBOM to a file instead of stdout\n\
Comment thread
robobun marked this conversation as resolved.
<b><green>bun pm<r> <blue>why<r> <d>\\<pkg\\><r> show dependency tree explaining why a package is installed\n\
<b><green>bun pm<r> <blue>licenses<r> list installed packages grouped by license\n\
<d>├<r> <cyan>--json<r> output as JSON\n\
Expand Down Expand Up @@ -750,6 +754,9 @@ Learn more about these at <magenta>https://bun.com/docs/cli/pm<r>.\n";
let positionals: &[&[u8]] = pm.options.positionals;
PmLicensesCommand::exec(pm, positionals, &cwd, licenses_flags)?;
Global::exit(0);
} else if strings::eql_comptime(subcommand, b"sbom") {
PmSbomCommand::exec(&&mut *ctx, pm, &cwd)?;
Global::exit(0);
} else if strings::eql_comptime(subcommand, b"pkg") {
let positionals: &[&[u8]] = pm.options.positionals;
PmPkgCommand::exec(&&mut *ctx, pm, positionals, &cwd)?;
Expand Down
Loading