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`): overwrite 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 * `--schema` (`-s`): schema version to be used, defaults to "1.2". """ @doc false @impl Mix.Task def run(all_args) do {opts, _args} = OptionParser.parse!( all_args, aliases: [o: :output, f: :force, d: :dev, r: :recurse, s: :schema], strict: [ output: :string, force: :boolean, dev: :boolean, recurse: :boolean, schema: :string ] ) output_path = opts[:output] || @default_path valiate_schema(opts) environment = (!opts[:dev] && :prod) || nil 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) end end defp generate_bom(output_path, environment, opts) do case SBoM.components_for_project(environment) do {:ok, components} -> xml = SBoM.CycloneDX.bom(components, opts) create_file(output_path, xml, force: opts[: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 defp valiate_schema(opts) do schema_versions = ["1.2", "1.1"] if opts[:schema] && opts[:schema] not in schema_versions do shell = Mix.shell() shell.error( "invalid cyclonedx schema version, available versions are #{ schema_versions |> Enum.join(", ") }" ) Mix.raise("Give correct cyclonedx schema version to continue.") end end end