defmodule Mix.Tasks.Sobelow do use Mix.Task @moduledoc """ Sobelow is a static analysis tool for discovering vulnerabilities in Phoenix applications. This tool should be run in the root of the project directory with the following command: mix sobelow ## Command line options * `--root -r` - Specify application root directory * `--verbose -v` - Print vulnerable code snippets * `--ignore -i` - Ignore modules * `--ignore-files` - Ignore files * `--details -d` - Get module details * `--all-details` - Get all module details * `--private` - Skip update checks * `--strict` - Exit when bad syntax is encountered * `--mark-skip-all` - Mark all printed findings as skippable * `--clear-skip` - Clear configuration added by `--mark-skip-all` * `--skip` - Skip functions flagged with `#sobelow_skip` or tagged with `--mark-skip-all` * `--router` - Specify router location * `--exit` - Return non-zero exit status * `--threshold` - Only return findings at or above a given confidence level * `--format` - Specify findings output format * `--quiet` - Return no output if there are no findings * `--compact` - Minimal, single-line findings * `--save-config` - Generates a configuration file based on command line options * `--config` - Run Sobelow with configuration file * `--version` - Output current version of Sobelow ## Ignoring modules If specific modules, or classes of modules are not relevant to the scan, it is possible to ignore them with a comma-separated list. mix sobelow -i XSS.Raw,Traversal ## Supported modules * XSS * XSS.Raw * XSS.SendResp * XSS.ContentType * XSS.HTML * SQL * SQL.Query * SQL.Stream * Config * Config.CSRF * Config.Headers * Config.CSP * Config.HTTPS * Config.HSTS * Config.Secrets * Config.CSWH * Vuln * Vuln.CookieRCE * Vuln.HeaderInject * Vuln.PlugNull * Vuln.Redirect * Vuln.Coherence * Vuln.Ecto * Traversal * Traversal.SendFile * Traversal.FileModule * Traversal.SendDownload * Misc * Misc.BinToTerm * Misc.FilePath * RCE.EEx * RCE.CodeModule * CI * CI.System * CI.OS * DOS * DOS.StringToAtom * DOS.ListToAtom * DOS.BinToAtom """ @switches [ verbose: :boolean, root: :string, ignore: :string, ignore_files: :string, details: :string, all_details: :boolean, private: :boolean, strict: :boolean, diff: :string, skip: :boolean, mark_skip_all: :boolean, clear_skip: :boolean, router: :string, exit: :string, format: :string, config: :boolean, save_config: :boolean, quiet: :boolean, compact: :boolean, flycheck: :boolean, out: :string, threshold: :string, version: :boolean ] @aliases [v: :verbose, r: :root, i: :ignore, d: :details, f: :format] # For escript entry def main(argv) do run(argv) end def run(argv) do {opts, _, _} = OptionParser.parse(argv, aliases: @aliases, switches: @switches) root = Keyword.get(opts, :root, ".") config = Keyword.get(opts, :config, false) conf_file = root <> "/.sobelow-conf" conf_file? = config && File.exists?(conf_file) opts = if is_nil(Keyword.get(opts, :exit)) && Enum.member?(argv, "--exit") do [{:exit, "low"} | opts] else opts end opts = if conf_file? do {:ok, opts} = File.read!(conf_file) |> Code.string_to_quoted() opts else opts end {verbose, diff, details, private, strict, skip, mark_skip_all, clear_skip, router, exit_on, format, ignored, ignored_files, all_details, out, threshold, version} = get_opts(opts, root, conf_file?) set_env(:verbose, verbose) if with_code = Keyword.get(opts, :with_code) do Mix.Shell.IO.info("WARNING: --with-code is deprecated, please use --verbose instead.\n") set_env(:verbose, with_code) end set_env(:root, root) set_env(:details, details) set_env(:private, private) set_env(:strict, strict) set_env(:skip, skip) set_env(:mark_skip_all, mark_skip_all) set_env(:clear_skip, clear_skip) set_env(:router, router) set_env(:exit_on, exit_on) set_env(:format, format) set_env(:ignored, ignored) set_env(:ignored_files, ignored_files) set_env(:out, out) set_env(:threshold, threshold) set_env(:version, version) save_config = Keyword.get(opts, :save_config) if function_exported?(Mix, :ensure_application!, 1) do Mix.ensure_application!(:ssl) Mix.ensure_application!(:inets) end cond do diff -> run_diff(argv) !is_nil(save_config) -> Sobelow.save_config(conf_file) !is_nil(all_details) -> Sobelow.all_details() !is_nil(details) -> Sobelow.details() version -> Sobelow.version() true -> Sobelow.run() end end # This diff check is strictly used for testing/debugging and # isn't meant for general use. # # Useful for comapring the output of two different runs of Sobelow def run_diff(argv) do diff_idx = Enum.find_index(argv, fn i -> i === "--diff" end) {_, list} = List.pop_at(argv, diff_idx) {diff_target, list} = List.pop_at(list, diff_idx) args = Enum.join(list, " ") diff_target = to_string(diff_target) System.shell("mix sobelow #{args} > sobelow.tempdiff") {diff, _} = System.shell("diff sobelow.tempdiff #{diff_target}") IO.puts(diff) end def set_env(key, value) do Application.put_env(:sobelow, key, value) end defp get_opts(opts, root, conf_file?) do verbose = Keyword.get(opts, :verbose, false) details = Keyword.get(opts, :details, nil) all_details = Keyword.get(opts, :all_details) private = Keyword.get(opts, :private, false) strict = Keyword.get(opts, :strict, false) diff = Keyword.get(opts, :diff, false) skip = Keyword.get(opts, :skip, false) mark_skip_all = Keyword.get(opts, :mark_skip_all, false) clear_skip = Keyword.get(opts, :clear_skip, false) router = Keyword.get(opts, :router) out = Keyword.get(opts, :out) version = Keyword.get(opts, :version, false) exit_on = Keyword.get(opts, :exit, "None") |> to_string() |> String.downcase() |> case do "high" -> :high "medium" -> :medium "low" -> :low _ -> false end format = cond do Keyword.get(opts, :quiet) -> "quiet" Keyword.get(opts, :compact) -> "compact" Keyword.get(opts, :flycheck) -> "flycheck" true -> Keyword.get(opts, :format, "txt") |> String.downcase() end format = out_format(out, format) {ignored, ignored_files} = if conf_file? do {Keyword.get(opts, :ignore, []), Keyword.get(opts, :ignore_files, []) |> Enum.map(&Path.expand(&1, root))} else ignored = Keyword.get(opts, :ignore, "") |> String.split(",") ignored_files = Keyword.get(opts, :ignore_files, "") |> String.split(",") |> Enum.reject(fn file -> file == "" end) |> Enum.map(&Path.expand(&1, root)) {ignored, ignored_files} end threshold = Keyword.get(opts, :threshold, "low") |> to_string() |> String.downcase() |> case do "high" -> :high "medium" -> :medium _ -> :low end {verbose, diff, details, private, strict, skip, mark_skip_all, clear_skip, router, exit_on, format, ignored, ignored_files, all_details, out, threshold, version} end # Future updates will include format hinting based on the outfile name. Additional output # formats will also be added. defp out_format(nil, format), do: format defp out_format("", format), do: format defp out_format(_out, format) do if format in ["json", "quiet", "sarif"] do format else "json" end end end