Packages
sobelow
0.5.3
0.14.1
0.14.0
0.13.0
0.12.2
0.12.1
0.12.0
0.11.1
0.11.0
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
Security-focused static analysis for Elixir & the Phoenix framework
Current section
Files
Jump to
Current section
Files
lib/sobelow/utils.ex
defmodule Sobelow.Utils do
@operators [:+, :-, :!, :^, :not, :~~~, :*, :/, :++, :--, :.., :<>,
:<<<, :>>>, :~>>, :<<~, :>, :<, :>=, :<=, :==, :!=, :=~,
:===, :!==, :&&, :&&&, :and, :||, :|||, :or, :=, :|]
# General Utils
def ast(filepath) do
{:ok, ast} = Code.string_to_quoted(read_file(filepath))
ast
end
defp read_file(filepath) do
content = File.read!(filepath)
if Sobelow.get_env(:skip) do
String.replace(content, ~r/#\s?sobelow_skip (\[(\"[^"]+\"(,|, )?)+\])/, "@sobelow_skip \\g{1}")
else
content
end
end
# This should be about as big as it gets, and is still fairly simple
# to understand. If it gets much more convoluted, an alternate
# solution should be explored.
def print_code(fun, :highlight_all) do
IO.puts "\n"
IO.puts IO.ANSI.light_magenta() <> Macro.to_string(fun) <> IO.ANSI.reset()
end
def print_code(fun, var, call \\ nil, module \\ nil) do
acc = ""
func_string = Macro.to_string fun, fn ast, string ->
s = print_highlighted(string, ast, var, call, module)
acc <> s
end
IO.puts "\n"
IO.puts func_string
end
def print_file_path_code(fun, var) do
acc = ""
func_string = Macro.to_string fun, fn ast, string ->
s = case ast do
{:=, _, [{^var, _, nil}|_]} ->
maybe_highlight(string, ast, var)
{{:., _,[{:__aliases__, _, [:Path]}, _]}, _, _} ->
maybe_highlight(string, ast, var)
{{:., _,[{:__aliases__, _, [:File]}, _]}, _, _} ->
maybe_highlight(string, ast, var)
_ -> if is_nil(string), do: "", else: string
end
acc <> s
end
IO.puts "\n"
IO.puts func_string
end
def print_highlighted(string, ast, var, call, module) do
case ast do
{:|>, _, [_, {^call, _, _}]} ->
maybe_highlight(string, ast, var)
{:&, _,[{:/, _,[{^call, _, _}, idx]}]} ->
maybe_highlight(string, ast, var, idx, :fun_cap)
{^call, _, _} ->
maybe_highlight(string, ast, var)
{:&, _,[{:/, _,[{{:., _,[{:__aliases__, _, _mod}, ^call]}, _, _}, idx]}]} ->
maybe_highlight(string, ast, var, idx, :fun_cap)
{:|>, _, [_, {{:., _,[{:__aliases__, _, mod}, ^call]}, _, _}]} ->
maybe_highlight(string, ast, var, module, mod)
{{:., _,[{:__aliases__, _, mod}, ^call]}, _, _} ->
maybe_highlight(string, ast, var, module, mod)
{:|>, _, [_, {{:., _, [^module, ^call]}, _, _}]} ->
maybe_highlight(string, ast, var)
{{:., _, [^module, ^call]}, _, _} ->
maybe_highlight(string, ast, var)
_ -> if is_nil(string), do: "", else: string
end
end
defp maybe_highlight(string, ast, var, idx, :fun_cap) do
if "&#{idx}" == var && Macro.to_string(ast) == string do
IO.ANSI.light_magenta() <> string <> IO.ANSI.reset()
else
string
end
end
defp maybe_highlight(string, ast, var, module, mod) do
if module === mod || Enum.member?(mod, module) do
maybe_highlight(string, ast, var)
else
string
end
end
defp maybe_highlight(string, ast, var) do
if is_fun_with_var?(ast, var) do
IO.ANSI.light_magenta() <> string <> IO.ANSI.reset()
else
string
end
end
def is_fun_with_var?(fun, var) do
{_, acc} = Macro.prewalk(fun, [], &is_fun_var/2)
if Enum.member?(acc, var), do: true, else: false
end
defp is_fun_var({:__aliases__, _, aliases} = ast, acc) do
{ast, [Module.concat(aliases)|acc]}
end
defp is_fun_var({:render, _, [_, _, keylist]} = ast, acc) do
{ast, Keyword.keys(keylist) ++ acc}
end
defp is_fun_var({:render, _, [_, keylist]} = ast, acc) when is_list(keylist) do
{ast, Keyword.keys(keylist) ++ acc}
end
defp is_fun_var({:&, _, [idx]} = ast, acc), do: {ast, ["&#{idx}"|acc]}
defp is_fun_var({var, _, _} = ast, acc), do: {ast, [var|acc]}
defp is_fun_var(ast, acc), do: {ast, acc}
def find_call({call, _, _} = ast, acc, call) do
{ast, acc <> Macro.to_string(ast)}
end
def find_call(ast, acc, _call), do: {ast, acc <> Macro.to_string(ast)}
def add_finding(line_no, filename, fun, fun_name, var, severity, type, call, module \\ nil) do
case Sobelow.format() do
"json" ->
log_json_finding(line_no, filename, fun,
fun_name, var, severity,
type, call, module)
"txt" ->
Sobelow.log_finding(type, severity)
print_finding_metadata(line_no, filename, fun,
fun_name, var, severity,
type, call, module)
"compact" ->
log_compact_finding(type, filename, line_no, severity)
_ ->
Sobelow.log_finding(type, severity)
end
end
def print_finding_metadata(line_no, filename, fun, fun_name, var, severity, type, call, module \\ nil) do
IO.puts finding_header(type, severity)
IO.puts finding_file_metadata(filename, fun_name, line_no)
IO.puts finding_variable(var)
maybe_print_code(fun, var, call, module)
IO.puts finding_break()
end
def log_compact_finding(type, filename, line_no, severity) do
details = "#{type} - #{filename}:#{line_no}"
Sobelow.log_finding(details, severity)
print_compact_finding(details, severity)
end
def log_compact_finding(type, severity) do
Sobelow.log_finding(type, severity)
print_compact_finding(type, severity)
end
defp print_compact_finding(details, severity) do
sev = case severity do
:high -> IO.ANSI.red()
:medium -> IO.ANSI.yellow()
:low -> IO.ANSI.green()
end
IO.puts "#{sev}[+]#{IO.ANSI.reset()} #{details}"
end
def log_json_finding(line_no, filename, _fun, fun_name, var, severity, type, _call, _module \\ nil) do
finding = [
type: type,
file: filename,
function: "#{fun_name}:#{line_no}",
variable: var
]
Sobelow.log_finding(finding, severity)
end
def finding_header(type, severity) do
{color, confidence} = finding_confidence(severity)
color <> type <> " - #{confidence} Confidence" <> IO.ANSI.reset()
end
def finding_file_metadata(filename, {:unquote, _, _} = fun_name, line_no) do
finding_file_metadata(filename, Macro.to_string(fun_name), line_no)
end
def finding_file_metadata(filename, fun_name, line_no) do
"File: #{filename} - #{fun_name}:#{line_no}"
end
def finding_variable(var) do
"Variable: #{var}"
end
def finding_confidence(severity) do
case severity do
:high -> {IO.ANSI.red(), "High"}
:medium -> {IO.ANSI.yellow(), "Medium"}
:low -> {IO.ANSI.green(), "Low"}
end
end
def finding_break() do
"\n-----------------------------------------------\n"
end
def maybe_print_code(fun, var, call, module) do
if Sobelow.get_env(:with_code), do: print_code(fun, var, call, module)
end
def maybe_print_file_path_code(fun, var) do
if Sobelow.get_env(:with_code), do: print_file_path_code(fun, var)
end
def get_sev(params, var) do
do_get_sev(params, var, :medium, :low)
end
def get_sev(params, var, false) do
do_get_sev(params, var, :high, :medium)
end
def get_sev(_params, _var, severity) do
severity
end
defp do_get_sev(params, var, high, low) do
params = ["conn.params"|params]
case Enum.member?(params, var) do
true -> high
false -> low
end
end
## Function parsing
def get_def_funs(filepath) do
ast = ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &get_def_funs(&1, &2))
acc
end
def get_def_funs({:@, _, [{:sobelow_skip, _, _}]} = ast, acc) do
if Sobelow.get_env(:skip), do: {ast, [ast|acc]}, else: {ast, acc}
end
def get_def_funs({:def, _, nil} = ast, acc), do: {ast, acc}
def get_def_funs({:defp, _, nil} = ast, acc), do: {ast, acc}
def get_def_funs({:def, _, _} = ast, acc), do: {ast, [ast|acc]}
def get_def_funs({:defp, _, _} = ast, acc), do: {ast, [ast|acc]}
def get_def_funs(ast, acc), do: {ast, acc}
def get_fun_vars_and_meta(fun, idx, type, module \\ nil) do
{params, {fun_name, line_no}} = get_fun_declaration(fun)
pipefuns = get_funs_from_pipe(fun, type, module)
pipevars = get_pipefuns_vars(pipefuns, fun, idx)
vars = get_funs(fun, type, module) -- pipefuns
|> get_funs_vars(idx, type, module)
{vars ++ pipevars, params, {fun_name, line_no}}
end
def get_erlang_fun_vars_and_meta(fun, idx, type, module) do
{params, {fun_name, line_no}} = get_fun_declaration(fun)
pipefuns = get_erlang_funs_from_pipe(fun, type, module)
pipevars = get_pipefuns_vars(pipefuns, fun, idx)
vars = get_erlang_aliased_funs_of_type(fun, type, module) -- pipefuns
|> get_funs_vars(idx, type, module)
{vars ++ pipevars, params, {fun_name, line_no}}
end
defp get_funs(fun, type, nil) do
get_funs_of_type(fun, type)
end
defp get_funs(fun, type, module) do
get_aliased_funs_of_type(fun, type, module)
end
defp get_funs_from_pipe(fun, type, nil) do
get_pipe_funs(fun)
|> Enum.map(fn {_, _, opts} -> Enum.at(opts, 1) end)
|> Enum.flat_map(&get_funs_of_type(&1, type))
|> Enum.uniq
end
defp get_funs_from_pipe(fun, type, module) do
get_pipe_funs(fun)
|> Enum.map(fn {_, _, opts} -> Enum.at(opts, 1) end)
|> Enum.flat_map(&get_aliased_funs_of_type(&1, type, module))
|> Enum.uniq
end
def get_erlang_funs_from_pipe(fun, type, module) do
get_pipe_funs(fun)
|> Enum.map(fn {_, _, opts} -> Enum.at(opts, 1) end)
|> Enum.flat_map(&get_erlang_aliased_funs_of_type(&1, type, module))
|> Enum.uniq
end
defp get_funs_vars(funs, idx, _type, nil) do
funs
|> Enum.map(&extract_opts(&1, idx))
|> List.flatten
end
defp get_funs_vars(funs, idx, _type, _module) do
funs
|> Enum.map(&extract_opts(&1, idx))
|> List.flatten
end
defp get_pipefuns_vars(pipefuns, fun, 0) do
pipefuns
|> Enum.flat_map(&get_pipe_val(fun, &1))
|> List.flatten
end
defp get_pipefuns_vars(pipefuns, _fun, idx) do
idx = idx - 1
pipefuns
|> Enum.map(&extract_opts(&1, idx))
|> List.flatten
end
def get_erlang_funs_of_type(ast, type) do
{_, acc} = Macro.prewalk(ast, [], &get_erlang_funs_of_type(&1, &2, type, :erlang))
acc
end
def get_erlang_funs_of_type({{:., _, [module, type]}, _, _} = ast, acc, type, module) do
{ast, [ast|acc]}
end
def get_erlang_funs_of_type({:&, _,[{:/, _,[{fun, meta, _}, idx]}]}, acc, type, module) do
fun_cap = create_fun_cap(fun, meta, idx)
get_erlang_funs_of_type(fun_cap, acc, type, module)
end
def get_erlang_funs_of_type(ast, acc, _type, _module), do: {ast, acc}
def get_erlang_aliased_funs_of_type(ast, type, module) do
{_, acc} = Macro.prewalk(ast, [], &get_erlang_funs_of_type(&1, &2, type, module))
acc
end
def get_funs_by_module(ast, module) do
{_, acc} = Macro.prewalk(ast, [], &contains_module(&1, &2, module))
acc
end
def get_assigns_from(fun, module) when is_list(module) do
get_funs_of_type(fun, :=)
|> Enum.filter(&contains_module?(&1, module))
|> Enum.map(&get_assign/1)
end
defp contains_module?(ast, module) do
{_, acc} = Macro.prewalk(ast, [], &contains_module(&1, &2, module))
if length(acc) > 0, do: true, else: false
end
defp contains_module({{:., _, [{:__aliases__, _, module}, _]}, _, _} = ast, acc, module) do
{module, [ast|acc]}
end
defp contains_module(ast,acc,_), do: {ast, acc}
defp get_assign({_,_,[{val,_,_}|_]}), do: val
defp get_assign(_), do: ""
## This is used to get aliased function calls such as `File.read`
## or `Ecto.Adapters.SQL.query`.
##
## This splits the call between strict and and standard, because there
## are some instances where we can be more certain of the alias contents.
## For instance, when using stdlib features such as `File.read` the alias
## list will be [:File]. For functions like `Ecto.Adapters.SQL.query`, there is less
## certainty because the Module has likely been aliased. The alias list
## could be [:Ecto, :Adapters, :SQL], just [:SQL], or something else entirely.
##
## Will consider flagging strict/standard separately depending on how this
## works in practice.
def get_aliased_funs_of_type(ast, type, module) when is_list(module) do
{_, acc} = Macro.prewalk(ast, [], &get_strict_aliased_funs_of_type(&1, &2, type, module))
acc
end
def get_aliased_funs_of_type(ast, type, module) do
{_, acc} = Macro.prewalk(ast, [], &get_aliased_funs_of_type(&1, &2, type, module))
acc
end
def get_strict_aliased_funs_of_type({{:., _, [{:__aliases__, _, aliases}, type]}, _, _opts} = ast, acc, type, module) do
if aliases === module do
{ast, [ast|acc]}
else
{ast, acc}
end
end
def get_strict_aliased_funs_of_type({:&, _,[{:/, _,[{fun, meta, _}, idx]}]}, acc, type, module) do
fun_cap = create_fun_cap(fun, meta, idx)
get_strict_aliased_funs_of_type(fun_cap, acc, type, module)
end
def get_strict_aliased_funs_of_type(ast, acc, _type, _module) do
{ast, acc}
end
def get_aliased_funs_of_type({{:., _, [{:__aliases__, _, aliases}, type]}, _, _opts} = ast, acc, type, module) do
if List.last(aliases) === module do
{ast, [ast|acc]}
else
{ast, acc}
end
end
def get_aliased_funs_of_type({:&, _,[{:/, _,[{fun, meta, _}, idx]}]}, acc, type, module) do
fun_cap = create_fun_cap(fun, meta, idx)
get_aliased_funs_of_type(fun_cap, acc, type, module)
end
def get_aliased_funs_of_type(ast, acc, _type, _module) do
{ast, acc}
end
def get_funs_of_type(ast, type) do
{_, acc} = Macro.prewalk(ast, [], &get_funs_of_type(&1, &2, type))
acc
end
def get_funs_of_type({type, _, _} = ast, acc, types) when is_list(types) do
if Enum.member?(types, type) do
{ast, [ast|acc]}
else
{ast, acc}
end
end
def get_funs_of_type({:&, _,[{:/, _,[{fun, meta, _}, idx]}]}, acc, type) do
fun_cap = create_fun_cap(fun, meta, idx)
get_funs_of_type(fun_cap, acc, type)
end
def get_funs_of_type({type, _, _} = ast, acc, type) do
{ast, [ast|acc]}
end
def get_funs_of_type(ast, acc, _type), do: {ast, acc}
defp create_fun_cap(fun, meta, idx) do
opts = Enum.map(1..idx, fn i -> {:&, [], [i]} end)
{fun, meta, opts}
end
def get_pipe_funs(ast) do
all_pipes = get_funs_of_type(ast, :|>)
Enum.filter all_pipes, fn pipe ->
{_,acc} = Macro.prewalk(pipe, [], &get_do_block/2)
Enum.empty?(acc)
end
end
def get_do_block({:|>,_,[_,{_,_,[[do: _block]]}]} = ast, acc) do
{[], [ast|acc]}
end
def get_do_block([do: _block] = ast, acc), do: {[], [ast|acc]}
def get_do_block(ast, acc), do: {ast, acc}
def extract_opts({:send_resp, _, nil}), do: []
def extract_opts({:send_resp, _, opts}), do: parse_opts(List.last(opts))
def extract_opts({{:., _, _}, _, _opts} = fun) do
parse_opts(fun)
end
def extract_opts({:<<>>,_,opts}) do
opts
|> Enum.map(&parse_string_interpolation/1)
end
def extract_opts({val, _, nil}), do: [val]
def extract_opts({val, _, []}), do: [val]
def extract_opts({_, _, opts}) when is_list(opts) do
opts
|> Enum.map(&parse_opts/1)
end
def extract_opts(opts) when is_list(opts), do: Enum.map(opts, &parse_opts/1)
def extract_opts(_), do: []
# A more general extract_opts. May be able to replace some of the
# function specific extractions.
def extract_opts({_, _, nil}, _idx), do: []
def extract_opts({_, _, opts}, idx) do
parse_opts(Enum.at(opts, idx))
end
defp parse_opts({:@, _, _}), do: []
defp parse_opts({key, _, nil}), do: key
defp parse_opts({:<<>>, _, opts}) do
Enum.map(opts, &parse_string_interpolation/1)
|> List.flatten
end
defp parse_opts({{:., _, [Access, :get]}, _, [{{:., _, [{:conn, _, nil}, :params]}, _, _}, _]}) do
"conn.params"
end
defp parse_opts({{:., _, [Access, :get]}, _, opts}) do
[{val, _, _}|_] = opts
val
end
defp parse_opts({{:., _, [{:__aliases__, _, module}, _func]}, _, _}) do
Module.concat(module)
end
# This is what an accessor func looks like, eg conn.params
defp parse_opts({{:., _, [{val, _, nil}, _]}, _, _}), do: val
defp parse_opts({:., _, [{val, _, nil}, _]}), do: val
defp parse_opts({{:.,_,opts}, _, _} = _fun) do
parse_opts(opts)
end
defp parse_opts({:&, _, [i]} = cap) when is_integer(i), do: Macro.to_string(cap)
defp parse_opts({fun, _, opts}) when fun in @operators do
Enum.map(opts, &parse_opts/1)
end
# Sigils aren't ordinary function calls.
defp parse_opts({fun, _, _}) when fun in [:sigil_s, :sigil_e], do: []
defp parse_opts({fun, _, opts}) when is_list(opts), do: fun
defp parse_opts(opts) when is_tuple(opts), do: parse_opts(Tuple.to_list(opts))
defp parse_opts(opts) when is_list(opts), do: Enum.map(opts, &parse_opts/1)
defp parse_opts(_), do: []
def get_fun_declaration(fun) do
{_, _, fun_opts} = fun
[definition|_] = fun_opts
declaration = case definition do
{:when, _, [opts|_]} -> opts
opts -> opts
end
params = get_params(declaration)
{fun_name, line_no, _} = declaration
{params, {fun_name, line_no}}
end
## Get function parameters.
defp get_params({_, _, params}) when is_list(params) do
Enum.flat_map(params, &get_params/1)
end
defp get_params({_, params}) when is_tuple(params) do
get_params(params)
end
defp get_params({var, _, nil}), do: [var]
defp get_params(_), do: []
def get_pipe_val(ast, pipe_fun) do
{_,acc} = Macro.prewalk(ast, [], &get_pipe_val(&1, &2, pipe_fun))
acc
end
def get_pipe_val({:|>, _, [{:|>,_,opts},pipefun]}, acc, pipefun) do
key = extract_opts(List.last(opts))
{[], [key|acc]}
end
def get_pipe_val({:|>, _, [opts,pipefun]}, acc, pipefun) do
key = extract_opts(opts)
{[], [key|acc]}
end
def get_pipe_val({:|>, _, [{fun,_,funopts} = opts,maybe_pipe]} = ast, acc, pipe) when not fun in [:|>] do
{_,match_pipe} = Macro.prewalk(maybe_pipe, [], &get_match(&1, &2, pipe))
{_,match_opts} = Macro.prewalk(opts, [], &get_match(&1,&2,pipe))
cond do
!Enum.empty?(match_pipe) ->
{maybe_pipe, acc}
!Enum.empty?(match_opts) ->
key = extract_opts(funopts)
{[], [key|acc]}
true ->
{ast, acc}
end
end
def get_pipe_val(ast,acc,_pipe), do: {ast, acc}
defp get_match(match, acc, match), do: {[], [match|acc]}
defp get_match(ast, acc, _), do: {ast, acc}
defp parse_string_interpolation({key, _, nil}), do: key
defp parse_string_interpolation({:::, _, opts}) do
parse_string_interpolation(opts)
end
defp parse_string_interpolation([{{:., _, [Kernel, :to_string]}, _, vars}, _]) do
Enum.map vars, &parse_opts/1
end
defp parse_string_interpolation({{:., _, [Kernel, :to_string]}, _, opts}) do
Enum.map opts, &parse_opts/1
end
defp parse_string_interpolation({:<<>>, _, opts}) do
opts
|> Enum.map(&parse_string_interpolation/1)
end
defp parse_string_interpolation(_) do
[]
end
## File listing
## In normal situations, this shouldn't fail. However,
## if there is a failure in path or app_name, it could
## lead to an issue here. In these situations, the scan
## will now proceed normally, but print an error message
## for the user.
def all_files(filepath, directory \\ "") do
if String.ends_with?(filepath, "/mix/") do
[]
else
case File.ls(filepath) do
{:ok, files} ->
Enum.flat_map(files, &list_files(&1, filepath, directory))
{:error, _} ->
IO.puts :stderr, "WARNING: Could not read #{Path.expand(filepath, "")}\n"
[]
end
end
end
defp list_files(filename, filepath, directory) do
cond do
Path.extname(filename) === ".ex" ->
[directory <> "/" <> filename]
File.dir?(filepath <> filename) ->
all_files(filepath <> filename <> "/", directory <> "/" <> filename)
true ->
[]
end
end
# Setup Utils
def get_app_name(filepath) do
if File.exists?(filepath) do
ast = ast(filepath)
{_, project_block} = Macro.prewalk(ast, [], &extract_project_block/2)
{_, app_name} = Macro.prewalk(project_block, [], &extract_app_name/2)
if is_atom(app_name), do: Atom.to_string(app_name), else: app_name
end
end
defp extract_project_block({:def, _, [{:project, _, _}, [do: block]]} = ast, _) do
{ast, block}
end
defp extract_project_block(ast, acc) do
{ast, acc}
end
defp extract_app_name(ast, acc) do
if Keyword.keyword?(ast) && Keyword.get(ast, :app) do
{ast, Keyword.get(ast, :app)}
else
{ast, acc}
end
end
# Config utils
def get_pipelines(filepath) do
ast = ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &get_funs_of_type(&1, &2, :pipeline))
acc
end
def is_vuln_pipeline({:pipeline, _, [_name, [do: block]]}, type) do
fun_list = case block do
{:__block__, _, list} -> list
{_, _, _} = list -> [list]
_ -> []
end
plugs = fun_list
|> Enum.reject(fn {type, _, _} -> type !== :plug end)
accepts = Enum.find_value(plugs, &get_plug_accepts/1)
has_type? =
case type do
:csrf -> Enum.find_value(plugs, &get_plug_csrf/1)
:headers -> Enum.find_value(plugs, &get_plug_headers/1)
end
if is_list(accepts) && Enum.member?(accepts, "html") && !has_type?, do: true, else: false
end
def get_plug_accepts({:plug, _, [:accepts, {:sigil_w, _, opts}]}), do: parse_accepts(opts)
def get_plug_accepts({:plug, _, [:accepts, accepts]}), do: accepts
def get_plug_accepts(_), do: false
def get_plug_csrf({:plug, _, [:protect_from_forgery]}), do: true
def get_plug_csrf({:plug, _, [:protect_from_forgery, _]}), do: true
def get_plug_csrf(_), do: false
def get_plug_headers({:plug, _, [:put_secure_browser_headers]}), do: true
def get_plug_headers({:plug, _, [:put_secure_browser_headers, _]}), do: true
def get_plug_headers(_), do: false
def parse_accepts([{:<<>>, _, [accepts|_]}, []]), do: String.split(accepts, " ")
def get_fuzzy_configs(key, filepath) do
ast = ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &extract_fuzzy_configs(&1, &2, key))
acc
end
def get_configs(key, filepath) do
ast = ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &extract_configs(&1, &2, key))
acc
end
defp extract_fuzzy_configs({:config, _, opts} = ast, acc, key) do
opt = List.last(opts)
vals = if Keyword.keyword?(opt), do: fuzzy_keyword_get(opt, key), else: nil
if is_nil(vals) do
{ast, acc}
else
{ast, [{ast, vals}|acc]}
end
end
defp extract_fuzzy_configs(ast, acc, _key) do
{ast, acc}
end
defp extract_configs({:config, _, opts} = ast, acc, key) do
opt = List.last(opts)
val = if Keyword.keyword?(opt), do: Keyword.get(opt, key), else: nil
if is_nil(val) do
{ast, acc}
else
{ast, [{ast, key, val}|acc]}
end
end
defp extract_configs(ast, acc, _key) do
{ast, acc}
end
defp fuzzy_keyword_get(opt, key) do
keys = Keyword.keys(opt)
Enum.map(keys, fn k ->
if is_atom(k) && k != :secret_key_base do
s = Atom.to_string(k) |> String.downcase()
if String.contains?(s, key), do: {k, Keyword.get(opt, k)}
end
end) |> Enum.reject(&is_nil/1)
end
def get_version(filepath) do
ast = ast(filepath)
{_, acc} = Macro.prewalk(ast, [], &get_version(&1, &2))
acc
end
def get_version({:@, _, nil} = ast, acc), do: {ast, acc}
def get_version({:@, _, [{:version, _, [vsn]}]}, _acc) when is_binary(vsn), do: {vsn, vsn}
def get_version(ast, acc), do: {ast, acc}
# XSS Utils
def get_template_raw_vars(filepath) do
ast = EEx.compile_string(File.read!(filepath))
{_, acc} = Macro.prewalk(ast, [], &extract_raw_vars(&1, &2))
acc
end
defp extract_raw_vars({:raw, _, [{_, _, [_, raw]}]} = ast, acc) do
{ast, [raw|acc]}
end
defp extract_raw_vars(ast, acc), do: {ast, acc}
def is_content_type_html({:put_resp_content_type, _, opts}) do
Enum.filter(opts, &is_binary/1)
|> Enum.any?(&String.contains?(&1, "html"))
end
def parse_render_opts({:render, _, opts}, params, idx) do
{_, vars} = Macro.prewalk(opts, [], &extract_render_opts/2)
template = if is_nil(opts) || Enum.empty?(opts), do: "", else: Enum.at(opts, idx)
reflected_vars = Enum.filter(vars, fn var ->
(is_reflected_var?(var) && is_in_params?(var, params)) || is_conn_params?(var)
end)
var_keys =
Enum.map vars, fn {key, val} ->
case val do
{_,_,_} -> key
_ -> nil
end
end
reflected_var_keys = Keyword.keys(reflected_vars)
{template, reflected_var_keys, var_keys -- reflected_var_keys}
end
def extract_render_opts(ast, acc) do
if Keyword.keyword?(ast) do
{ast, ast}
else
{ast, acc}
end
end
defp is_reflected_var?({_, {_, _, nil}}), do: true
defp is_reflected_var?(_), do: false
defp is_in_params?({_, {var, _, _}}, params) do
Enum.member?(params, var)
end
def is_conn_params?({_, {{:., _, [Access, :get]}, _, access_opts}}), do: is_conn_params?(access_opts)
def is_conn_params?([{{:., _, [{:conn, _, nil}, :params]}, _, []}, _]), do: true
def is_conn_params?(_), do: false
end