Skip to content

Commit

Permalink
Upgrade to CycloneDX v1.4 (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
lafirest authored Dec 23, 2022
1 parent 3f9207e commit 4a664a2
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 21 deletions.
97 changes: 88 additions & 9 deletions src/rebar3_sbom_cyclonedx.erl
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
-module(rebar3_sbom_cyclonedx).

-export([bom/1, bom/2, uuid/0]).
-export([bom/3, bom/4, uuid/0]).

bom(Components) ->
bom(Components, uuid()).
-define(APP, "rebar3_sbom").
-define(DEFAULT_VERSION, "1").
-define(COMPONENT_FIELDS, [name, version, author, description, licenses, purl, sha256]).

bom(Components, Serial) ->
Bom = {bom, [{serialNumber, Serial}, {xmlns, "http://cyclonedx.org/schema/bom/1.1"}], [
{components, [], [component(Component) || Component <- Components, Component /= undefined]}
]},
-include_lib("xmerl/include/xmerl.hrl").

bom(File, Components, Opts) ->
bom(File, Components, Opts, uuid()).

bom(File, Components, Opts, Serial) ->
ValidComponents = lists:filter(fun(E) -> E =/= undefined end, Components),
Content = {bom, [{version, ?DEFAULT_VERSION},
{serialNumber, Serial},
{xmlns, "http://cyclonedx.org/schema/bom/1.4"}],
[{metadata, metadata()},
{components, [], [component(Component) || Component <- ValidComponents]},
{dependencies, [], [dependency(Component) || Component <- ValidComponents]}]},
Normalized = xmerl_lib:normalize_element(Content),
Bom = update_version(File, Normalized, Opts),
xmerl:export_simple([Bom], xmerl_xml).

metadata() ->
[{timestamp, [calendar:system_time_to_rfc3339(erlang:system_time(second))]},
{tools, [{tool, [{name, [?APP]}]}]}
].

component(Component) ->
{component, [{type, "library"}],
[component_field(Field, Value) || {Field, Value} <- Component, Value /= undefined]}.
{component, [{type, "library"}, {'bom-ref', bom_ref_of_component(Component)}],
[component_field(Field, Value)
|| {Field, Value} <- Component,
lists:member(Field, ?COMPONENT_FIELDS), Value /= undefined, Value /= []]}.

component_field(name, Name) -> {name, [], [[Name]]};
component_field(version, Version) -> {version, [], [[Version]]};
component_field(author, Author) -> {author, [], [[string:join(Author, ",")]]};
component_field(description, Description) -> {description, [], [[Description]]};
component_field(licenses, Licenses) -> {licenses, [], [license(License) || License <- Licenses]};
component_field(purl, Purl) -> {purl, [], [[Purl]]};
Expand All @@ -39,3 +59,62 @@ uuid() ->

hex(Bin) ->
string:lowercase(<< <<Hex>> || <<Nibble:4>> <= Bin, Hex <- integer_to_list(Nibble,16) >>).

update_version(File, #xmlElement{attributes = Attrs} = Bom, Opts) ->
Version = get_version(File, Bom, Opts),
Attr = lists:keyfind(version, #xmlAttribute.name, Attrs),
NewAttr = Attr#xmlAttribute{value = Version},
NewAttrs = lists:keyreplace(version, #xmlAttribute.name, Attrs, NewAttr),
Bom#xmlElement{attributes = NewAttrs}.

get_version(File, Bom, Opts) ->
try
case xmerl_scan:file(File) of
{#xmlElement{attributes = Attrs} = Old, _} ->
case lists:keyfind(version, #xmlAttribute.name, Attrs) of
false ->
?DEFAULT_VERSION;
#xmlAttribute{value = Value} ->
case is_strict_version(Opts) andalso is_bom_equal(Old, Bom) of
true ->
Value;
_ ->
Version = erlang:list_to_integer(Value),
erlang:integer_to_list(Version + 1)
end
end;
{error, enoent} ->
?DEFAULT_VERSION
end
catch _:Reason ->
logger:error("scan file:~ts failed, reason:~p, will use the default version number 1",
[File, Reason]),
?DEFAULT_VERSION
end.

is_strict_version(Opts) ->
proplists:get_value(strict_version, Opts, true).

is_bom_equal(#xmlElement{content = A}, #xmlElement{content = B}) ->
lists:all(fun(Key) ->
ValA = lists:keyfind(Key, #xmlElement.name, A),
ValB = lists:keyfind(Key, #xmlElement.name, B),
case {ValA, ValB} of
{false, false} -> true;
{false, _} -> false;
{_, false} -> false;
{_, _} ->
xmerl_lib:simplify_element(ValA) =:=
xmerl_lib:simplify_element(ValB)
end
end,
[components]).

dependency(Component) ->
Ref = bom_ref_of_component(Component),
Deps = proplists:get_value(dependencies, Component, []),
{dependency, [{ref, [Ref]}], [dependency([{name, Dep}]) || Dep <- Deps]}.

bom_ref_of_component(Component) ->
Name = proplists:get_value(name, Component),
lists:flatten(io_lib:format("ref_component_~ts", [Name])).
34 changes: 22 additions & 12 deletions src/rebar3_sbom_prv.erl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ init(State) ->
{example, "rebar3 sbom"}, % How to use the plugin
{opts, [ % list of options understood by the plugin
{output, $o, "output", {string, ?OUTPUT}, "the full path to the SBoM output file"},
{force, $f, "force", {boolean, false}, "overwite existing files without prompting for confirmation"}
{force, $f, "force", {boolean, false}, "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"}
]},
{short_desc, "Generates CycloneDX SBoM"},
{desc, "Generates a Software Bill-of-Materials (SBoM) in CycloneDX format"}
Expand All @@ -34,7 +35,7 @@ do(State) ->
Force = proplists:get_value(force, Args),
Deps = rebar_state:all_deps(State),
DepsInfo = [dep_info(Dep) || Dep <- Deps],
Xml = rebar3_sbom_cyclonedx:bom(DepsInfo),
Xml = rebar3_sbom_cyclonedx:bom(Output, DepsInfo, Args),
case write_file(Output, Xml, Force) of
ok ->
rebar_api:info("CycloneDX SBoM written to ~s", [Output]),
Expand All @@ -53,47 +54,56 @@ dep_info(Dep) ->
Source = rebar_app_info:source(Dep),
Dir = rebar_app_info:dir(Dep),
Details = rebar_app_info:app_details(Dep),
dep_info(Name, Version, Source, Dir, Details).
Deps = rebar_app_info:deps(Dep),
dep_info(Name, Version, Source, Dir, Details, Deps).

dep_info(_Name, _Version, {pkg, Name, Version, Sha256}, _Dir, Details) ->
dep_info(_Name, _Version, {pkg, Name, Version, Sha256}, _Dir, Details, Deps) ->
[
{name, Name},
{version, Version},
{author, proplists:get_value(maintainers, Details)},
{description, proplists:get_value(description, Details)},
{licenses, proplists:get_value(licenses, Details)},
{purl, rebar3_sbom_purl:hex(Name, Version)},
{sha256, string:lowercase(Sha256)}
{sha256, string:lowercase(Sha256)},
{dependencies, Deps}
];

dep_info(_Name, _Version, {pkg, Name, Version, _InnerChecksum, OuterChecksum, _RepoConfig}, _Dir, Details) ->
dep_info(_Name, _Version, {pkg, Name, Version, _InnerChecksum, OuterChecksum, _RepoConfig}, _Dir, Details, Deps) ->
[
{name, Name},
{version, Version},
{author, proplists:get_value(maintainers, Details)},
{description, proplists:get_value(description, Details)},
{licenses, proplists:get_value(licenses, Details)},
{purl, rebar3_sbom_purl:hex(Name, Version)},
{sha256, string:lowercase(OuterChecksum)}
{sha256, string:lowercase(OuterChecksum)},
{dependencies, Deps}
];

dep_info(Name, _Version, {git, Git, {tag, Tag}}, _Dir, Details) ->
dep_info(Name, _Version, {git, Git, {tag, Tag}}, _Dir, Details, Deps) ->
[
{name, Name},
{version, Tag},
{author, proplists:get_value(maintainers, Details)},
{description, proplists:get_value(description, Details)},
{licenses, proplists:get_value(licenses, Details)},
{purl, rebar3_sbom_purl:git(Name, Git, Tag)}
{purl, rebar3_sbom_purl:git(Name, Git, Tag)},
{dependencies, Deps}
];

dep_info(Name, Version, {git, Git, {ref, Ref}}, _Dir, Details) ->
dep_info(Name, Version, {git, Git, {ref, Ref}}, _Dir, Details, Deps) ->
[
{name, Name},
{version, Version},
{author, proplists:get_value(maintainers, Details)},
{description, proplists:get_value(description, Details)},
{licenses, proplists:get_value(licenses, Details)},
{purl, rebar3_sbom_purl:git(Name, Git, Ref)}
{purl, rebar3_sbom_purl:git(Name, Git, Ref)},
{dependencies, Deps}
];

dep_info(_Name, _Version, _Source, _Dir, _Details) ->
dep_info(_Name, _Version, _Source, _Dir, _Details, _Deps) ->
undefined.

write_file(Filename, Xml, true) ->
Expand Down

0 comments on commit 4a664a2

Please sign in to comment.