defmodule SBoM do
@moduledoc """
Collect dependency information for use in a Software Bill-of-Materials (SBOM).
"""
alias SBoM.Purl
alias SBoM.Cpe
@doc """
Builds a SBoM for the current Mix project. The result can be exported to
CycloneDX XML format using the `SBoM.CycloneDX` module. Pass an environment
of `nil` to include dependencies across all environments.
Wrap the call to this function with `Mix.Project.in_project/3,4` to select a
Mix project by path.
"""
def components_for_project(environment \\ :prod) do
Mix.Project.get!()
{deps, not_ok} =
Mix.Dep.load_on_environment(env: environment)
|> Enum.split_with(&ok?/1)
case not_ok do
[] ->
components =
deps
|> Enum.map(&component_from_dep/1)
|> Enum.reject(&is_nil/1)
{:ok, components}
_ ->
{:error, :unresolved_dependency}
end
end
defp ok?(dep) do
Mix.Dep.ok?(dep) || Mix.Dep.compilable?(dep)
end
defp component_from_dep(%{opts: opts} = dep) do
case Map.new(opts) do
%{optional: true} ->
# If the dependency is optional at the top level, then we don't include
# it in the SBoM
nil
opts_map ->
component_from_dep(dep, opts_map)
end
end
defp component_from_dep(%{scm: Hex.SCM}, opts) do
%{hex: name, lock: lock, dest: dest} = opts
version = elem(lock, 2)
sha256 = elem(lock, 3)
hex_metadata_path = Path.expand("hex_metadata.config", dest)
metadata =
case :file.consult(hex_metadata_path) do
{:ok, metadata} -> metadata
_ -> []
end
{_, description} = List.keyfind(metadata, "description", 0, {"description", ""})
{_, licenses} = List.keyfind(metadata, "licenses", 0, {"licenses", []})
%{
type: "library",
name: name,
version: version,
purl: Purl.hex(name, version, opts[:repo]),
cpe: Cpe.hex(name, version, opts[:repo]),
hashes: %{
"SHA-256" => sha256
},
description: description,
licenses: licenses
}
end
defp component_from_dep(%{scm: Mix.SCM.Git, app: app}, opts) do
%{git: git, lock: lock, dest: _dest} = opts
version =
case opts[:tag] do
nil ->
elem(lock, 2)
tag ->
tag
end
%{
type: "library",
name: to_string(app),
version: version,
purl: Purl.git(to_string(app), git, version),
licenses: []
}
end
defp component_from_dep(_dep, _opts), do: nil
end