Current section

Files

Jump to
sbom lib mix tasks sbom.cyclonedx.ex
Raw

lib/mix/tasks/sbom.cyclonedx.ex

defmodule Mix.Tasks.Sbom.Cyclonedx do
@shortdoc "Generates CycloneDX SBoM"
use Mix.Task
import Mix.Generator
@default_path "bom.xml"
@moduledoc """
Generates a Software Bill-of-Materials (SBoM) in CycloneDX format.
## Options
* `--output` (`-o`): the full path to the SBoM output file (default:
#{@default_path})
* `--force` (`-f`): overwite existing files without prompting for
confirmation
* `--dev` (`-d`): include dependencies for non-production environments
(including `dev`, `test` or `docs`); by default only dependencies for
MIX_ENV=prod are returned
* `--recurse` (`-r`): in an umbrella project, generate individual output
files for each application, rather than a single file for the entire
project
"""
@doc false
def run(all_args) do
{opts, _args} =
OptionParser.parse!(
all_args,
aliases: [o: :output, f: :force, d: :dev, r: :recurse],
strict: [output: :string, force: :boolean, dev: :boolean, recurse: :boolean]
)
output_path = opts[:output] || @default_path
environment = (!opts[:dev] && :prod) || nil
# Mix.Task.run("deps.loadpaths", ["--no-compile", "--no-load-deps"])
apps = Mix.Project.apps_paths()
if opts[:recurse] && apps do
Enum.each(apps, &generate_bom(&1, output_path, environment, opts[:force]))
else
generate_bom(output_path, environment, opts[:force])
end
end
defp generate_bom(output_path, environment, force) do
case SBoM.components_for_project(environment) do
{:ok, components} ->
xml = SBoM.CycloneDX.bom(components)
create_file(output_path, xml, force: force)
{:error, :unresolved_dependency} ->
dependency_error()
end
end
defp generate_bom({app, path}, output_path, environment, force) do
Mix.Project.in_project(app, path, fn _module ->
generate_bom(output_path, environment, force)
end)
end
defp dependency_error do
shell = Mix.shell()
shell.error("Unchecked dependencies; please run `mix deps.get`")
Mix.raise("Can't continue due to errors on dependencies")
end
end