diff --git a/README.md b/README.md index 59b801c..6f4f2a2 100644 --- a/README.md +++ b/README.md @@ -13,32 +13,109 @@ [![Coverage Status](https://coveralls.io/repos/github/erlef/rebar3_sbom/badge.svg?branch=main)](https://coveralls.io/github/erlef/rebar3_sbom?branch=main) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/11547/badge)](https://www.bestpractices.dev/projects/11547) -Generates a Software Bill-of-Materials (SBoM) in CycloneDX format +Generates a Software Bill-of-Materials (SBoM) for [Rebar3](https://rebar3.org) +projects, that follows the [CycloneDX](https://cyclonedx.org) specification +([1.6](https://spec.cyclonedx.org/1.6/)). -## Use +[CycloneDX](https://cyclonedx.org) is an open SBOM standard (maintained by the +[OWASP Foundation](https://owasp.org)) with broad support in security scanners, +dependency trackers, and compliance tools. -Add rebar3_sbom to your rebar config, either in a project or globally in -`~/.config/rebar3/rebar.config`: +## What is a Software Bill of Materials (SBoM) and why use it? + +In manufacturing, a Bill of Materials (BOM) is the list of raw materials, parts, +and sub-assemblies needed to build a product, so you know exactly what goes into +it, in what quantity, and from where. + +A **Software Bill of Materials (SBoM)** does the same for software: it **lists +the components of your application** (packages and libraries) along with their +`versions`, `source locations` (e.g. Hex, GitHub), `checksums` (cryptographic +hashes for integrity), and `licensing information`. + +That visibility supports **supply chain tracking** (what is in the software and +where it came from), faster response to vulnerabilities by **matching components +against vulnerability databases to respond to CVEs**, and **licence and compliance +reporting** (e.g. audits and due diligence). That is why **many organisations +and regulations expect or require an SBoM for critical software** — e.g. +safety-critical sectors, government procurement, or customers requiring supply +chain transparency. + +> #### Info {: .info} +> +> An SBoM is an inventory, not a certification. It does not claim that software is secure or compliant. It provides a factual inventory that other tools and processes use for security and compliance assessments. + +## Installation + +Add the plugin to your Rebar3 config, per project (recommended) or globally. + +**1. Project (recommended)**. In the project's `rebar.config`: + +```erlang +{plugins, [rebar3_sbom]}. +``` + +**2. Global**. In `~/.config/rebar3/rebar.config`: ```erlang {plugins, [rebar3_sbom]}. ``` -Then run the `sbom` task on a project: +Now, when you run any rebar3 command (e.g. `rebar3 compile` or `rebar3 sbom`), the plugin will be fetched. + +## Usage + +From the project directory (where your `rebar.config` lives), run: + +```bash +rebar3 sbom +``` + +Result: ``` -$ rebar3 sbom ===> Verifying dependencies... ===> CycloneDX SBoM written to bom.xml ``` +By default the SBoM is written to `bom.xml` (or `bom.json` if you use `--format json`). + + +See [Available options](#available-options) for all flags. + +### Choosing which profiles to include + +Only dependencies in the `default` profile are included by default. To generate an SBoM that also includes development dependencies (e.g. from the `test` or `docs` profiles), specify the profiles using `as`: + +```bash +rebar3 as default,test,docs sbom -o dev_bom.xml +``` + +## Available options + +The following command line options are supported: + +| Option | Short | Description | Default | +|--------|-------|-------------|---------| +| `--format` | `-F` | The file format of the SBoM output: `xml` or `json` | `xml` | +| `--output` | `-o` | The full path to the SBoM output file | `./bom.xml` or `./bom.json` | +| `--force` | `-f` | Overwrite existing files without prompting for confirmation | `false` | +| `--strict_version` | `-V` | When true, the BOM version is incremented only if the set of components differs from the existing BOM file (if any). Changes to metadata, timestamp, or other fields do not trigger a version increment. | `true` | +| `--author` | `-a` | The author of the SBoM | `GITHUB_ACTOR` env var; if unset, `authors` from `.app.src` file| + +To see all options and their descriptions, run: `rebar3 help sbom`. + + ## Configuration -You can configure additional SBoM metadata in your `rebar.config`: +Optional SBoM metadata, like `sbom_manufacturer` or `sbom_licenses`, is set in +`rebar.config` under the `rebar3_sbom` key. Both options are optional; omit them +if you don't need them. + +Example: ```erlang {rebar3_sbom, [ - {sbom_manufacturer, #{ % Optional, all fields inside are optional + {sbom_manufacturer, #{ % Optional; all fields inside are optional name => "Your Organization", url => ["https://example.com", "https://another-example.com"], address => #{ @@ -55,63 +132,34 @@ You can configure additional SBoM metadata in your `rebar.config`: phone => "123456789"} ] }}, - {sbom_licenses, ["Apache-2.0"]} % Optional + {sbom_licenses, ["Apache-2.0"]} % Optional; licenses for the SBoM document itself ]}. ``` +- `sbom_manufacturer` — **Who is producing the SBoM** (e.g. your organisation or + CI). If omitted, the `manufacturer` field is not included in the SBOM. All + fields inside are optional. +- `sbom_licenses` — Licenses for the SBoM document itself (the metadata), not + your project. If omitted, it defaults to your project's licenses from + `.app.src`. In the generated SBoM, these appear under `metadata.licenses`. + Your project's licenses are always read from `.app.src` and appear in + `metadata.component.licenses`. -**Note:** -- `sbom_manufacturer` (optional) identifies who is **producing the SBoM document** - (typically your organization or CI environment), not necessarily who developed - the software. If omitted, the manufacturer field is not included in the SBoM. -- `sbom_licenses` (optional) specifies the licenses for the **SBoM document itself** - (the metadata), not your project. If omitted, it defaults to the same licenses - as your project (from the `.app.src` file). -- Your project's licenses are automatically read from the `.app.src` file and - appear in `metadata.component.licenses`. - - -## Command Line Options - -The following command line options are supported: - - -F, --format the file format of the SBoM output, [xml|json], [default: xml] - -o, --output the full path to the SBoM output file [default: ./bom.[xml|json]] - -f, --force overwite existing files without prompting for confirmation - [default: false] - -V, --strict_version modify the version number of the BoM only when the content changes - [default: true] - -a --author the author of the SBoM - -**Author Fallback:** If `--author` is not specified, the plugin will fall back -to the `GITHUB_ACTOR` environment variable. If that variable isn't set, it will -use the authors from the project's `.app.src` file. - -By default only dependencies in the 'default' profile are included. To -generate an SBoM covering development environments specify the relevant -profiles using 'as': - -```bash -$ rebar3 as default,test,docs sbom -o dev_bom.xml -``` - -## Hash Generation +## Hash generation -For the main component (`metadata.component`), the plugin computes the SHA-256 hash of the release tarball (`-.tar.gz`) found in the release directory. +For the main component (the root application described by the BOM, in CycloneDX `metadata.component`), the plugin computes the `SHA-256` hash of the release tarball (`-.tar.gz`). The tarball is looked for under `rel//` relative to the project base directory (e.g. after `rebar3 release` or `rebar3 tar`). -If the tarball does not exist (e.g., because `rebar3 tar` hasn't been run), no hash is included for the main component, and a warning is logged. +If the tarball does not exist, no hash is included for the main component, and a warning is logged. ## CPE Generation -The plugin automatically generates a CPE (Common Platform Enumeration) identifier for the main component (`metadata.component`) using the GitHub link from your project's `.app.src` file. If no GitHub link is present, the CPE field is omitted from the SBoM. +The plugin automatically generates a CPE (Common Platform Enumeration) identifier for the main component (`metadata.component`) using the GitHub link from your project's `.app.src` file. Dependencies also get a CPE when the package has a GitHub link (e.g. from Hex metadata). If no GitHub link is present, the CPE field is omitted for that component. -To ensure CPE generation, add a GitHub link to your `.app.src` file. For example: +To ensure CPE generation for the main component, add a GitHub link to your `.app.src` file. For example: ```erlang {application, my_app, [ ... - {links, [ - {"GitHub", "https://github.com/your-org/my_app"} - ]} + {links, [{"GitHub", "https://github.com/your-org/my_app"}]} ]}. ``` @@ -121,13 +169,17 @@ The plugin supports external references for components, which are automatically All standard CycloneDX external reference types are supported. Additionally, for convenience, the plugin supports common field names used by the Erlang/Elixir community, which are automatically mapped to their CycloneDX equivalents: -- `"GitHub"` → `"vcs"` -- `"Homepage"` → `"website"` -- `"Changelog"` → `"release-notes"` -- `"Issues"` → `"issue-tracker"` -- `"Documentation"` → `"documentation"` +| Link name | CycloneDX type | +|------------------|--------------------| +| `"GitHub"` | `"vcs"` | +| `"Homepage"` | `"website"` | +| `"Changelog"` | `"release-notes"` | +| `"Issues"` | `"issue-tracker"` | +| `"Documentation"`| `"documentation"` | -**Note:** The plugin treats the names (i.e., `"Homepage"`, `"GitHub"`, etc.) in the `links` field as case-insensitive, so `"homepage"` and `"HOMEPAGE"` will also map to `"website"`, for example. +> #### Info {: .info} +> +> The plugin treats the names (i.e., `"Homepage"`, `"GitHub"`, etc.) in the `links` field as case-insensitive, so `"homepage"` and `"HOMEPAGE"` will also map to `"website"`, for example. You can use either the standard CycloneDX type names or the community convention names in your `.app.src` file: @@ -144,14 +196,49 @@ You can use either the standard CycloneDX type names or the community convention ]}. ``` -Development ------------ +## Merging and other ecosystems + +This plugin only considers `Rebar3/Hex` and `Git` (GitHub, Bitbucket) dependencies. For a full deployment SBoM (e.g. including NPM or OS packages), generate multiple CycloneDX BOMs and merge them with a tool such as [CycloneDX CLI](https://github.com/CycloneDX/cyclonedx-cli) using its merge command (e.g. `cyclonedx merge --input-files bom1.xml bom2.xml --output-file merged.xml`). + +## Development + +### Development environment + +You can get Erlang, Rebar3, and the tools used by this project in either of these ways: + +- **devenv**: From the repository root, run `devenv shell` to enter a shell + with Erlang, Rebar3, CycloneDX CLI, and other tools (see [devenv](https://devenv.sh)). +- **asdf**: With [asdf](https://asdf-vm.com/) installed, run `asdf install` in + the repository root; versions are defined in [`.tool-versions`](https://github.com/erlef/rebar3_sbom/blob/main/.tool-versions). + +Linting is configured in [`elvis.config`](elvis.config). Run `rebar3 lint` to +check code style before submitting. + +### Contributing + +For guidelines on how to contribute (bug reports, feature proposals, pull +requests), see the [Contributing guide](https://github.com/erlef/rebar3_sbom/blob/main/.github/CONTRIBUTING.md). + +### Generating documentation + +With Erlang/OTP 27+ and Rebar3 available (e.g. `devenv shell` or `.tool-versions`): + +```bash +rebar3 compile +rebar3 ex_doc +``` + +Open `doc/index.html` in a browser. + +### Running tests To run the full test suite locally, you need the CycloneDX CLI (`cyclonedx-cli`) available on your `PATH`, as it is used by `rebar3_sbom_validation_SUITE` to validate generated SBOMs. For example, on macOS or Linux with Homebrew: - brew tap cyclonedx/cyclonedx - brew install cyclonedx/cyclonedx/cyclonedx-cli +```bash +brew tap cyclonedx/cyclonedx +brew install cyclonedx/cyclonedx/cyclonedx-cli +``` -More informations about the tool: https://github.com/CycloneDX/cyclonedx-cli +More information about the tool: https://github.com/CycloneDX/cyclonedx-cli diff --git a/src/rebar3_sbom.erl b/src/rebar3_sbom.erl index 1b75c4d..113401a 100644 --- a/src/rebar3_sbom.erl +++ b/src/rebar3_sbom.erl @@ -6,6 +6,14 @@ -include("rebar3_sbom.hrl"). +-moduledoc """ +Entry point for the Rebar3 SBoM plugin. +Registers the `sbom` provider (see `m:rebar3_sbom_prv`). + +This module also exports the types used for CycloneDX SBoM structures +(`sbom/0`, `component/0`, `metadata/0`, etc.) for use by other modules. +""". + -export([init/1]). -export_type([ @@ -30,6 +38,7 @@ -type dependency() :: #dependency{}. -type sbom() :: #sbom{}. +-doc "Registers the `sbom` provider with Rebar3.". -spec init(rebar_state:t()) -> {ok, rebar_state:t()}. init(State) -> {ok, State1} = rebar3_sbom_prv:init(State), diff --git a/src/rebar3_sbom_cpe.erl b/src/rebar3_sbom_cpe.erl index fc0cc89..92c2943 100644 --- a/src/rebar3_sbom_cpe.erl +++ b/src/rebar3_sbom_cpe.erl @@ -8,6 +8,13 @@ % Includes -include("rebar3_sbom.hrl"). +-moduledoc """ +Builds CPE (Common Platform Enumeration) identifiers for SBoM components. Uses +the component name, version, and (for GitHub deps) vendor from the source URL. + +Used by the provider when building components (see `m:rebar3_sbom_prv`). +""". + %--- Macros -------------------------------------------------------------------- -define(CPE_PREFIX, <<"cpe:", ?CPE_VERSION/binary>>). % Includes the fields: @@ -28,6 +35,10 @@ %--- API ----------------------------------------------------------------------- +-doc """ +Returns a CPE string for the component, or `undefined` if no GitHub URL +for unknown names. CPE version is defined in `rebar3_sbom.hrl`. +""". -spec cpe(Name, Version, Url) -> CPE when Name :: bitstring(), Version :: bitstring(), diff --git a/src/rebar3_sbom_cyclonedx.erl b/src/rebar3_sbom_cyclonedx.erl index f7e0440..69465f5 100644 --- a/src/rebar3_sbom_cyclonedx.erl +++ b/src/rebar3_sbom_cyclonedx.erl @@ -10,9 +10,18 @@ -include("rebar3_sbom.hrl"). +-moduledoc """ +Builds a CycloneDX SBoM from the application, its dependencies, and metadata. + +License IDs are resolved via `m:rebar3_sbom_license`. +""". + +-doc "Builds a CycloneDX SBoM; serial (UUID) is generated automatically.". +-doc #{equiv => bom / 6}. bom(FileInfo, IsStrictVersion, App, Plugin, MetadataInfo) -> bom(FileInfo, IsStrictVersion, App, Plugin, uuid(), MetadataInfo). +-doc "Builds a CycloneDX SBoM with the given serial (UUID).". bom({FilePath, _} = FileInfo, IsStrictVersion, App, Plugin, Serial, MetadataInfo) -> {AppInfo, RawComponents} = App, {PluginInfo, PluginDepsInfo} = Plugin, diff --git a/src/rebar3_sbom_json.erl b/src/rebar3_sbom_json.erl index ac81842..228219c 100644 --- a/src/rebar3_sbom_json.erl +++ b/src/rebar3_sbom_json.erl @@ -8,6 +8,8 @@ -include("rebar3_sbom.hrl"). +-moduledoc "Encodes and decodes the SBoM to and from CycloneDX JSON.". + -define(SCHEMA, <<"http://cyclonedx.org/schema/bom-1.6.schema.json">>). encode(SBoM) -> diff --git a/src/rebar3_sbom_license.erl b/src/rebar3_sbom_license.erl index aa2c334..5dfc0af 100644 --- a/src/rebar3_sbom_license.erl +++ b/src/rebar3_sbom_license.erl @@ -5,6 +5,13 @@ -export([spdx_id/1]). +-moduledoc """ +Maps license strings (e.g. from Hex or `.app.src`) to SPDX license identifiers. + +Used when filling component and metadata licenses in the SBoM +(see `m:rebar3_sbom_cyclonedx`). +""". + -define(SPDX_ID, #{ "0bsd" => "0BSD", "aal" => "AAL", @@ -408,6 +415,10 @@ "zlib-acknowledgement" => "zlib-acknowledgement" }). +-doc """ +Returns the SPDX license ID for a given string if known; otherwise the original +string. +""". spdx_id(Id) -> maps:get(normalize(Id), ?SPDX_ID, undefined). diff --git a/src/rebar3_sbom_prv.erl b/src/rebar3_sbom_prv.erl index dc08db0..5445e13 100644 --- a/src/rebar3_sbom_prv.erl +++ b/src/rebar3_sbom_prv.erl @@ -11,6 +11,17 @@ -include("rebar3_sbom.hrl"). +-moduledoc """ +Rebar3 provider that implements the `rebar3 sbom` command (generates a CycloneDX SBoM). + +Builds a CycloneDX SBoM from project and dependency metadata +(via `rebar3_sbom_cyclonedx:bom/5`), then encodes it with `m:rebar3_sbom_xml` +or `m:rebar3_sbom_json` and writes to the path given by `--output` +(default `./bom.xml` or `./bom.json`). PURL and CPE for components come from +`m:rebar3_sbom_purl` and `m:rebar3_sbom_cpe`; license IDs from +`m:rebar3_sbom_license`. +""". + %--- Macros -------------------------------------------------------------------- -define(CUSTOM_MAPPING, #{ "github" => "vcs", @@ -52,6 +63,10 @@ init(State) -> ]), {ok, rebar_state:add_provider(State, Provider)}. +-doc """ +Builds the SBoM via `rebar3_sbom_cyclonedx:bom/5`, encodes it (XML or JSON), +and writes the output file (see `m:rebar3_sbom_xml` or `m:rebar3_sbom_json`). +""". -spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}. do(State) -> {Args, _} = rebar_state:command_parsed_args(State), @@ -96,6 +111,7 @@ do(State) -> {error, {?MODULE, Message}} end. +-doc "Formats provider errors for display to the user.". -spec format_error(any()) -> iolist(). format_error(Message) -> io_lib:format("~s", [Message]). diff --git a/src/rebar3_sbom_purl.erl b/src/rebar3_sbom_purl.erl index 745b690..d8ea354 100644 --- a/src/rebar3_sbom_purl.erl +++ b/src/rebar3_sbom_purl.erl @@ -8,6 +8,13 @@ % https://github.com/package-url/purl-spec +-moduledoc """ +Builds [PURL](https://github.com/package-url/purl-spec) identifiers for SBoM +components. Supports Hex, GitHub, Bitbucket, and local/OTP apps. + +Used by the provider when building components (see `m:rebar3_sbom_prv`). +""". + -export([hex/2, git/3, github/2, bitbucket/2, local_otp_app/2, local/2]). hex(Name, Version) -> diff --git a/src/rebar3_sbom_xml.erl b/src/rebar3_sbom_xml.erl index 27374ae..a02ae0b 100644 --- a/src/rebar3_sbom_xml.erl +++ b/src/rebar3_sbom_xml.erl @@ -8,6 +8,8 @@ -include("rebar3_sbom.hrl"). -include_lib("xmerl/include/xmerl.hrl"). +-moduledoc "Encodes and decodes the SBoM to and from CycloneDX XML.". + -define(XMLNS, "http://cyclonedx.org/schema/bom/1.6"). -define(XMLNS_XSI, "http://www.w3.org/2001/XMLSchema-instance"). -define(XSI_SCHEMA_LOC,