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