Packages

Validate quoted Elixir AST against a function whitelist

Current section

6 Versions

Jump to

Compare versions

6 files changed
+139 additions
-20 deletions
  @@ -28,6 +28,9 @@ whitelist = Loppers.special_forms ++ [
28 28 the `Loppers.Walk` module)
29 29 * Returns the AST-Fragment (including the line number if your compiler provides it)
30 30 so you can add squiggly lines to the editor at the right place.
31 + * Whitelist a module with functions with `{Application.Callbacks, :__all__}`
32 + * Whitelist a module with all child-modules and functions with `{Application.Callbacks, :__submodules_all__}`
33 +
31 34
32 35 ## Installation
  @@ -4,12 +4,11 @@
4 4 <<"Validate quoted Elixir AST against a function whitelist">>}.
5 5 {<<"elixir">>,<<"~> 1.4">>}.
6 6 {<<"files">>,
7 - [<<"lib/list.ex">>,<<"lib/loppers.ex">>,<<"lib/match.ex">>,
8 - <<"lib/validate.ex">>,<<"lib/walk.ex">>,<<"mix.exs">>,<<"README.md">>,
7 + [<<"lib">>,<<"lib/walk.ex">>,<<"lib/match.ex">>,<<"lib/list.ex">>,
8 + <<"lib/validate.ex">>,<<"lib/loppers.ex">>,<<"mix.exs">>,<<"README.md">>,
9 9 <<"LICENSE">>]}.
10 10 {<<"licenses">>,[<<"MIT">>]}.
11 11 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/narrowtux/loppers">>}]}.
12 - {<<"maintainers">>,[<<"Moritz Schmale">>]}.
13 12 {<<"name">>,<<"loppers">>}.
14 13 {<<"requirements">>,[]}.
15 - {<<"version">>,<<"0.1.3">>}.
14 + {<<"version">>,<<"1.0.0">>}.
  @@ -4,9 +4,10 @@ defmodule Loppers do
4 4 @type error ::
5 5 {:not_allowed, ast :: term}
6 6 @type function_ref ::
7 - {module :: atom, :__all__} |
7 + {module :: atom, :__all__} |
8 + {module :: atom, :__submodules_all__} |
8 9 {module :: atom, function :: atom} |
9 - function :: atom
10 + (function :: atom)
10 11
11 12 @type validate_option ::
12 13 {:whitelist, [function_ref]} |
  @@ -42,7 +43,19 @@ defmodule Loppers do
42 43 iex> blacklist = [{Enum, :map_reduce}]
43 44 iex> quoted = quote do Enum.map_reduce([], nil, &({&1, nil})) end
44 45 iex> Loppers.validate(quoted, [whitelist: whitelist, blacklist: blacklist])
45 - {:error, [not_allowed: {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map_reduce]}, [], [[], nil, {:&, [], [{{:&, [], [1]}, nil}]}]}]}
46 + {:error, [
47 + not_allowed: {{:., [parent_modules: []],
48 + [
49 + {:__aliases__, [parent_modules: [], alias: false], [:Enum]},
50 + :map_reduce
51 + ]}, [parent_modules: []],
52 + [
53 + [],
54 + nil,
55 + {:&, [parent_modules: []],
56 + [{{:&, [parent_modules: []], [1]}, nil}]}
57 + ]}
58 + ]}
46 59
47 60 ## Options
48 61
  @@ -55,6 +68,7 @@ defmodule Loppers do
55 68 def validate(quoted, opts) do
56 69 {quoted, _acc} = Walk.walk(quoted, {%{}, [{Kernel, []}]}, &Walk.gen_meta/2)
57 70 {quoted, _acc} = Walk.walk(quoted, [], &Walk.module_functions/2)
71 + {quoted, _acc} = Walk.recurse(quoted, %{}, &Walk.track_parent_modules/2)
58 72 whitelist = Keyword.get(opts, :whitelist, nil)
59 73 blacklist = Keyword.get(opts, :blacklist, [])
60 74 acc = Validate.validate(quoted, [], fn {_, meta, _} = ast, acc ->
  @@ -100,7 +114,8 @@ defmodule Loppers do
100 114 [
101 115 {Kernel, :@},
102 116 {Kernel, :defmodule},
103 - {Kernel, :def}
117 + {Kernel, :def},
118 + :when
104 119 ]
105 120 end
106 121
  @@ -110,8 +125,15 @@ defmodule Loppers do
110 125 Without those it's going to be hard to write any elixir code.
111 126 """
112 127 def special_forms do
113 - [:functions, :macros]
114 - |> Enum.flat_map(&Kernel.SpecialForms.__info__/1)
115 - |> Keyword.keys()
128 + special_forms =
129 + [:functions, :macros]
130 + |> Enum.flat_map(&Kernel.SpecialForms.__info__/1)
131 + |> Keyword.keys()
132 + special_forms ++ [
133 + :{},
134 + :doc,
135 + :->,
136 + {Kernel, :is_binary}
137 + ]
116 138 end
117 139 end
  @@ -13,6 +13,23 @@ defmodule Loppers.Match do
13 13 # ABC.fun(1, 2, 3)
14 14 # {{:., [], [{:__aliases__, [alias: false], [:ABC]}, :fun]}, [], [1, 2, 3]}
15 15
16 + # Handling :__submodules_all__
17 + def matches?({{:., _dot_meta, [{:__aliases__, aliases, called_as}, _called_fn]}, fn_meta, _arguments}, {mod, :__submodules_all__}) do
18 +
19 + called_module = case Keyword.get(aliases, :alias, false) do
20 + false ->
21 + parent_modules = Keyword.get(fn_meta, :parent_modules, [])
22 + list_to_module(parent_modules ++ called_as)
23 +
24 + aliased_module ->
25 + aliased_module
26 + end
27 +
28 + # Check if "Module.Child" has the Prefix for "Module."
29 + String.starts_with?("#{called_module}", "#{mod}.")
30 + end
31 +
32 +
16 33 def matches?({{:., _dot_meta, [{:__aliases__, aliases, called_as}, called_fn]}, _fn_meta, _arguments}, {mod, list_fn})
17 34 when called_fn == list_fn or list_fn == :__all__ do
18 35 called_module = case Keyword.get(aliases, :alias, false) do
  @@ -40,7 +57,6 @@ defmodule Loppers.Match do
40 57 mod == called_module && (list_fn == called_fn || list_fn == :__all__)
41 58 end
42 59
43 -
44 60 def matches?(_, _) do
45 61 false
46 62 end
  @@ -1,6 +1,11 @@
1 1 defmodule Loppers.Walk do
2 2 import Loppers.Match, only: [list_to_module: 1]
3 3
4 + @doc """
5 + Walking a given ast filling the accumulator using a walker-function.
6 +
7 + Walker signature needs to be `walker.(ast, acc)`
8 + """
4 9 def walk({_, _, _} = ast, acc, walker) do
5 10 {{fun, meta, args}, acc} = walker.(ast, acc)
6 11 {args, _acc} = reduce_args(args, acc, walker)
  @@ -9,11 +14,6 @@ defmodule Loppers.Walk do
9 14 {ast, acc}
10 15 end
11 16
12 - def walk([{key, {_, _, _} = call}], acc, walker) do
13 - {call, acc} = walk(call, acc, walker)
14 - {[{key, call}], acc}
15 - end
16 -
17 17 def walk(primitive, acc, _walker)
18 18 when is_atom(primitive) or
19 19 is_number(primitive) or
  @@ -32,6 +32,7 @@ defmodule Loppers.Walk do
32 32 reduce_args(list, acc, walker)
33 33 end
34 34
35 + @doc "Helper to call walker callback for each args."
35 36 def reduce_args(nil, acc, _) do
36 37 {nil, acc}
37 38 end
  @@ -39,6 +40,76 @@ defmodule Loppers.Walk do
39 40 Enum.map_reduce(args, acc, &walk(&1, &2, walker))
40 41 end
41 42
43 +
44 + # Testing walk() or recurse()
45 + #def inspect(a, b), do: {a, b} |> IO.inspect(label: "inspect")
46 +
47 +
48 +
49 + def track_parent_modules({:defmodule, meta_defmodule, [{:__aliases__, aliases_meta, parent_modules} | rest]}, acc) do
50 +
51 + # Add defined module to parent_modules in acc for whole branch.
52 +
53 + ast = {:defmodule, meta_defmodule, [{:__aliases__, aliases_meta, parent_modules} | rest]}
54 + acc = Map.update(acc, :parent_modules, parent_modules, &(&1 ++ parent_modules))
55 +
56 + {ast, acc}
57 + end
58 +
59 + def track_parent_modules({fun, meta, args}, acc) do
60 +
61 + # Add parent_modules to meta of function call from acc.
62 + meta = Keyword.put(meta, :parent_modules, Map.get(acc, :parent_modules, []))
63 +
64 + {{fun, meta, args}, acc}
65 + end
66 +
67 + def track_parent_modules(ast, acc), do: {ast, acc}
68 +
69 +
70 +
71 +
72 + @doc """
73 + Recursing into the AST. Difference to walk() is that the accumulator will only be changed for current branch.
74 + """
75 + def recurse({_, _, _} = ast, acc, callback) do
76 + {{fun, meta, args}, acc} = callback.(ast, acc)
77 +
78 + {args, _acc} = recurse(args, acc, callback)
79 + {fun, _acc} = recurse(fun, acc, callback)
80 +
81 + {{fun, meta, args}, acc}
82 + end
83 +
84 +
85 + def recurse({key, value}, acc, callback) do
86 + {{key, value}, acc} = callback.({key, value}, acc)
87 + {key, _acc} = recurse(key, acc, callback)
88 + {value, _acc} = recurse(value, acc, callback)
89 + {{key, value}, acc}
90 + end
91 +
92 +
93 + def recurse(ast, acc, callback) when is_list(ast) do
94 + {Enum.map(ast, fn(ast_entry) ->
95 + ast_entry |> recurse(acc, callback) |> elem(0)
96 + end), acc}
97 + end
98 +
99 + def recurse(primitive, acc, callback) do
100 + callback.(primitive, acc)
101 + end
102 +
103 +
104 +
105 +
106 +
107 +
108 +
109 +
110 + @doc """
111 + Walk callback for collecting metadata like aliases and imports.
112 + """
42 113 def gen_meta({:alias, _, args} = ast, {aliases, imports}) do
43 114 {aka, opts} = case args do
44 115 [aka, opts] -> {aka, opts}
  @@ -87,6 +158,9 @@ defmodule Loppers.Walk do
87 158
88 159 @defs ~w[def defp defmacro defmacrop]a
89 160
161 + @doc """
162 + Walk callback for collecting metadata like aliases and imports.
163 + """
90 164 def module_functions({:defmodule, _, [_aliases, args]} = module, _current_functions) do
91 165 contents = case Keyword.get(args, :do, {:__block__, [], []}) do
92 166 {:__block__, _, contents} -> contents
  @@ -98,7 +172,10 @@ defmodule Loppers.Walk do
98 172 {defs, _, _} when defs in @defs -> true
99 173 _ -> false
100 174 end)
101 - |> Enum.map(fn {_, _, [{name, _, _} | _]} -> name end)
175 + |> Enum.map(fn
176 + {_def, _ctx1, [{:when, _ctx2, [{name, _ctx3, _args} | _]} | _]} -> name
177 + {_def, _ctx1, [{name, _ctx2, _args} | _]} -> name
178 + end)
102 179
103 180 {module, functions}
104 181 end
  @@ -116,6 +193,8 @@ defmodule Loppers.Walk do
116 193
117 194 def module_functions(ast, acc), do: {ast, acc}
118 195
196 +
197 + @doc "Helper to check if a function is imported in a given module."
119 198 def function_imported?({module, opts}, function, arity) do
120 199 excluded = {function, arity} in Keyword.get(opts, :except, [])
121 200 included = case Keyword.get(opts, :only) do
Loading more files…