Skip to content
10 changes: 7 additions & 3 deletions src/rebar3_sbom_cpe.erl
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@ cpe(<<"elixir">>, Version, _) ->
cpe(_Name, _Version, undefined) ->
undefined;
cpe(Name, Version, Url) ->
Organization = github_url(Url),
build_cpe(Organization, Name, Version).
case github_url(Url) of
undefined -> undefined;
Organization -> build_cpe(Organization, Name, Version)
end.

%--- Private -------------------------------------------------------------------

Expand All @@ -78,7 +80,9 @@ github_url(<<"https://github.com/", Rest/bitstring>>) ->
Organization;
github_url(<<"git@github.com:", Rest/bitstring>>) ->
[Organization | _] = string:split(Rest, "/"),
Organization.
Organization;
github_url(_) ->
undefined.

-spec build_cpe(Organization, Name, Version) -> CPE when
Organization :: bitstring(),
Expand Down
6 changes: 4 additions & 2 deletions src/rebar3_sbom_cyclonedx.erl
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,15 @@ component_field(external_references = Field, RawComponent) ->
component_field(Field, RawComponent) ->
case proplists:get_value(Field, RawComponent) of
Value when is_binary(Value) ->
binary:bin_to_list(Value);
unicode:characters_to_list(Value);
Value when is_list(Value) ->
unicode:characters_to_list(Value);
Else ->
Else
end.

license(Name) when is_binary(Name) ->
license(binary:bin_to_list(Name));
license(unicode:characters_to_list(Name));
license(Name) ->
case rebar3_sbom_license:spdx_id(Name) of
undefined ->
Expand Down
4 changes: 2 additions & 2 deletions src/rebar3_sbom_json.erl
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ dependency_to_json(D) ->
bin(undefined) ->
undefined;
bin(Value) when is_list(Value) ->
erlang:list_to_binary(Value);
unicode:characters_to_binary(Value);
bin(Value) ->
Value.

Expand Down Expand Up @@ -232,6 +232,6 @@ json_to_external_reference(#{<<"type">> := Type, <<"url">> := Url}) ->
str(undefined) ->
undefined;
str(Value) when is_binary(Value) ->
erlang:binary_to_list(Value);
unicode:characters_to_list(Value);
str(Value) ->
Value.
104 changes: 104 additions & 0 deletions src/rebar3_sbom_otp.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
%% SPDX-License-Identifier: BSD-3-Clause
%% SPDX-FileCopyrightText: 2026 Erlang Ecosystem Foundation

-module(rebar3_sbom_otp).

-export([otp_components/0, otp_components/1]).

-include("rebar3_sbom.hrl").

-define(OTP_GITHUB, <<"https://github.com/erlang/otp">>).

%% Returns OTP/ERTS component info in the same proplist format as dep_info.
-spec otp_components() -> [proplists:proplist()].
otp_components() ->
otp_components(otp_apps()).

-spec otp_components(Apps :: [atom()]) -> [proplists:proplist()].
otp_components(Apps) ->
OtpRelease = erlang:system_info(otp_release),
ErtsVersion = erlang:system_info(version),
OtpComponent = otp_component(OtpRelease),
ErtsComponent = erts_component(ErtsVersion, OtpRelease),
AppComponents = [app_component(App, OtpRelease) || App <- Apps],
[OtpComponent, ErtsComponent | AppComponents].

%% Top-level Erlang/OTP component
otp_component(OtpRelease) ->
Name = <<"erlang/otp">>,
Version = list_to_binary(OtpRelease),
[
{name, Name},
{version, Version},
{purl, rebar3_sbom_purl:otp_runtime(Name, Version)},
{cpe, rebar3_sbom_cpe:cpe(Name, Version, ?OTP_GITHUB)},
{authors, []},
{description, <<"Erlang/OTP">>},
{licenses, [<<"Apache-2.0">>]},
{external_references, #{"vcs" => binary_to_list(?OTP_GITHUB)}},
{dependencies, []},
{scope, required}
].

%% ERTS component
erts_component(ErtsVersion, OtpRelease) ->
Name = <<"erts">>,
Version = list_to_binary(ErtsVersion),
[
{name, Name},
{version, Version},
{purl, rebar3_sbom_purl:otp_runtime(Name, Version)},
{cpe, rebar3_sbom_cpe:cpe(<<"erlang/otp">>, list_to_binary(OtpRelease), ?OTP_GITHUB)},
{authors, []},
{description, <<"Erlang Runtime System">>},
{licenses, [<<"Apache-2.0">>]},
{external_references, #{"vcs" => binary_to_list(?OTP_GITHUB)}},
{dependencies, []},
{scope, required}
].

%% Individual OTP application component
app_component(App, OtpRelease) ->
Name = atom_to_binary(App),
Version =
case application:get_key(App, vsn) of
{ok, Vsn} -> list_to_binary(Vsn);
undefined -> list_to_binary(OtpRelease)
end,
Description =
case application:get_key(App, description) of
{ok, Desc} -> unicode:characters_to_binary(Desc);
undefined -> <<>>
end,
Licenses =
case application:get_key(App, licenses) of
{ok, L} -> [unicode:characters_to_binary(Li) || Li <- L];
undefined -> [<<"Apache-2.0">>]
end,
[
{name, Name},
{version, Version},
{purl, rebar3_sbom_purl:otp_runtime(Name, Version)},
{cpe, rebar3_sbom_cpe:cpe(<<"erlang/otp">>, list_to_binary(OtpRelease), ?OTP_GITHUB)},
{authors, []},
{description, Description},
{licenses, Licenses},
{external_references, #{"vcs" => binary_to_list(?OTP_GITHUB)}},
{dependencies, []},
{scope, required}
].

%% Returns OTP apps actually used by the project's dependency tree.
%% These are the standard OTP applications that come bundled with Erlang.
-spec otp_apps() -> [atom()].
otp_apps() ->
OtpLib = code:lib_dir(),
AllLoaded = [App || {App, _, _} <- application:loaded_applications()],
[App || App <- AllLoaded, is_otp_app(App, OtpLib)].

-spec is_otp_app(atom(), file:filename()) -> boolean().
is_otp_app(App, OtpLib) ->
case code:lib_dir(App) of
{error, _} -> false;
Dir -> lists:prefix(OtpLib, Dir)
end.
34 changes: 23 additions & 11 deletions src/rebar3_sbom_prv.erl
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ init(State) ->
"overwite existing files without prompting for confirmation"},
{strict_version, $V, "strict_version", {boolean, true},
"modify the version number of the BoM only when the content changes"},
{author, $a, "author", string, "the author of the SBoM"}
{author, $a, "author", string, "the author of the SBoM"},
{include_otp, undefined, "include-otp", {boolean, false},
"include OTP/ERTS runtime components in the SBoM"}
]},
{short_desc, "Generates CycloneDX SBoM"},
{desc, "Generates a Software Bill-of-Materials (SBoM) in CycloneDX format"}
Expand All @@ -71,15 +73,25 @@ do(State) ->
PluginInfo = dep_info(Plugin),
PluginDepsInfo = [dep_info(Dep) || Dep <- PluginDeps],

PluginOpts = rebar_state:get(State, rebar3_sbom, []),
IncludeOtp =
proplists:get_value(include_otp, Args) orelse
proplists:get_value(include_otp, PluginOpts, false),

FilePath = filepath(Output, Format),
DepsInfo = [dep_info(Dep) || Dep <- rebar_state:all_deps(State)],
OtpComponents =
case IncludeOtp of
true -> rebar3_sbom_otp:otp_components();
false -> []
end,
AppInfo = dep_info(App),
AppInfo2 = [{sha256, hash(AppInfo, rebar_dir:base_dir(State))} | AppInfo],
MetadataInfo = metadata(State),
SBoM = rebar3_sbom_cyclonedx:bom(
{FilePath, Format},
IsStrictVersion,
{AppInfo2, DepsInfo},
{AppInfo2, DepsInfo ++ OtpComponents},
{PluginInfo, PluginDepsInfo},
MetadataInfo
),
Expand Down Expand Up @@ -160,7 +172,7 @@ hex_metadata(Dep) ->

hex_metadata_licenses(HexMetadata) ->
HexMetadataLicenses = proplists:get_value(<<"licenses">>, HexMetadata, []),
[binary_to_list(HexMetadataLicense) || HexMetadataLicense <- HexMetadataLicenses].
[unicode:characters_to_list(HexMetadataLicense) || HexMetadataLicense <- HexMetadataLicenses].

-spec get_github_link(HexMetadata, Links) -> binary() when
HexMetadata :: [{binary(), binary()}],
Expand All @@ -170,7 +182,7 @@ get_github_link([], Links) ->
undefined ->
undefined;
Value ->
list_to_binary(Value)
unicode:characters_to_binary(Value)
end;
get_github_link(HexMetadata, _) ->
Links = proplists:get_value(<<"links">>, HexMetadata, []),
Expand Down Expand Up @@ -262,7 +274,7 @@ dep_info(_Name, _Version, {pkg, Name, Version, Sha256}, Common) ->
{version, Version},
{purl, rebar3_sbom_purl:hex(Name, Version)},
{sha256, string:lowercase(Sha256)},
{cpe, rebar3_sbom_cpe:cpe(Name, list_to_binary(Version), GitHubLink)}
{cpe, rebar3_sbom_cpe:cpe(Name, unicode:characters_to_binary(Version), GitHubLink)}
| Common
];
dep_info(_Name, _Version, {pkg, Name, Version, _InnerChecksum, OuterChecksum, _RepoConf}, Common) ->
Expand All @@ -279,15 +291,15 @@ dep_info(Name, _DepVersion, {git, Git, GitRef}, Common) ->
{Version, Purl, CPE} =
case GitRef of
{tag, Tag} ->
GeneratedCPE = rebar3_sbom_cpe:cpe(Name, list_to_binary(Tag), list_to_binary(Git)),
GeneratedCPE = rebar3_sbom_cpe:cpe(Name, unicode:characters_to_binary(Tag), unicode:characters_to_binary(Git)),
{Tag, rebar3_sbom_purl:git(Name, Git, Tag), GeneratedCPE};
{branch, Branch} ->
GeneratedCPE = rebar3_sbom_cpe:cpe(
Name, list_to_binary(Branch), list_to_binary(Git)
Name, unicode:characters_to_binary(Branch), unicode:characters_to_binary(Git)
),
{Branch, rebar3_sbom_purl:git(Name, Git, Branch), GeneratedCPE};
{ref, Ref} ->
GeneratedCPE = rebar3_sbom_cpe:cpe(Name, list_to_binary(Ref), list_to_binary(Git)),
GeneratedCPE = rebar3_sbom_cpe:cpe(Name, unicode:characters_to_binary(Ref), unicode:characters_to_binary(Git)),
{Ref, rebar3_sbom_purl:git(Name, Git, Ref), GeneratedCPE}
end,
[
Expand All @@ -305,7 +317,7 @@ dep_info(Name, Version, checkout, Common) ->
{name, Name},
{version, Version},
{purl, rebar3_sbom_purl:local_otp_app(Name, Version)},
{cpe, rebar3_sbom_cpe:cpe(Name, list_to_binary(Version), GitHubLink)}
{cpe, rebar3_sbom_cpe:cpe(Name, unicode:characters_to_binary(Version), GitHubLink)}
| Common
];
dep_info(Name, Version, root_app, Common) ->
Expand All @@ -315,7 +327,7 @@ dep_info(Name, Version, root_app, Common) ->
{name, Name},
{version, Version},
{purl, Purl},
{cpe, rebar3_sbom_cpe:cpe(Name, list_to_binary(Version), GitHubLink)}
{cpe, rebar3_sbom_cpe:cpe(Name, unicode:characters_to_binary(Version), GitHubLink)}
| Common
].

Expand All @@ -325,7 +337,7 @@ filepath(Path, _Format) ->
Path.

write_file(Filename, Contents, true) ->
file:write_file(Filename, Contents);
file:write_file(Filename, unicode:characters_to_binary(Contents));
write_file(Filename, Xml, false) ->
case file:read_file_info(Filename) of
{error, enoent} ->
Expand Down
5 changes: 4 additions & 1 deletion src/rebar3_sbom_purl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

% https://github.com/package-url/purl-spec

-export([hex/2, git/3, github/2, bitbucket/2, local_otp_app/2, local/2]).
-export([hex/2, git/3, github/2, bitbucket/2, local_otp_app/2, local/2, otp_runtime/2]).

hex(Name, Version) ->
purl(["hex", string:lowercase(Name)], Version).
Expand Down Expand Up @@ -49,6 +49,9 @@ local_otp_app(Name, Version) ->
local(Name, Version) ->
purl(["generic", string:lowercase(Name)], Version).

otp_runtime(Name, Version) ->
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use the otp purl type here:

package-url/purl-spec#472

We also use this in CVEs: https://cna.erlef.org/cves/CVE-2026-23941.json

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think I got it correct now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the wait @Taure.

There's two things we need to be careful of:

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ok, will try to see if I can make it better and see that these tools also work

purl(["generic", "erlang-" ++ string:lowercase(Name)], Version).

purl(PathSegments, Version) ->
Path = lists:join("/", [escape(Segment) || Segment <- PathSegments]),
unicode:characters_to_binary(io_lib:format("pkg:~s@~s", [Path, escape(Version)])).
Expand Down
Loading