Static-analysis pass over a plugin's source tree.
Behind mix mob.audit_plugins (see MOB_PLUGIN_SECURITY.md). Walks the
plugin's Elixir sources (lib/**/*.ex{,s}) with an AST scanner and its C
NIF sources (priv/native/**/*.{c,h}) with a tighter regex pass, flagging
patterns Mob considers risky:
Code.eval_string/1,2,3andCode.compile_string/1,2— the prime arbitrary-code-execution vector. Severity:high.String.to_atom/1with a non-literal argument — atom-exhaustion risk. A literalString.to_atom("foo")is fine; flagged only when the argument is a variable or expression. Severity:medium.:erlang.binary_to_term/1— unbounded deserialization. The arity-2 form:erlang.binary_to_term(bin, [:safe])does not fire (caller has opted into the bounded variant). Severity:high.Application.put_env/3,4targeting:mob— would let a plugin silently retarget plugin activations, host_config keys, etc.get_envis fine. Severity:medium.- File / network I/O escape hatches outside the plugin's own
priv/:File.write{,!}/1,2,File.rm_rf{,!}/1,File.cp{,!}/2,:os.cmd/1,System.cmd/2,3,Path.expand/1with a~literal. We don't try to prove what they touch — flag and let the plugin author either remove or justify. Severity:medium. - In C NIF sources: calls to
system(3),popen(3),execve(2), and rawsocket(2)creation. Regex over comment-stripped text. Severities:medium(socket) and:high(system/popen/execve).
Swift / Kotlin sources are out of scope for this commit — the spec
(MOB_PLUGIN_SECURITY.md) calls for proper parsers there, and the task
surfaces a "not yet audited" summary line instead of guessing with regex.
All checks are pure given file inputs; the only I/O is reading source files off disk.
Summary
Functions
Audits a single C source file in isolation. Public for testing.
Audits a single Elixir source file in isolation. Public for testing.
Audits a single plugin checked out at plugin_dir.
Computes the exit code for one or more reports.
Tally findings into %{high: n, medium: n, low: n}. Public for testing.
Types
@type report() :: %{ plugin: atom() | nil, findings: [finding()], summary: %{ high: non_neg_integer(), medium: non_neg_integer(), low: non_neg_integer() }, kotlin_or_swift_skipped: boolean() }
@type severity() :: :high | :medium | :low
Functions
Audits a single C source file in isolation. Public for testing.
Audits a single Elixir source file in isolation. Public for testing.
Audits a single plugin checked out at plugin_dir.
manifest may be nil (a tier-0 plugin); only :name is read from it for
attribution in the resulting findings. Returns a report/0 map.
Computes the exit code for one or more reports.
- 0 — every finding is
:low(or there are none). - 1 — at least one
:medium, no:high. - 2 — at least one
:high.
When accept_medium? is true, mediums no longer count toward exit code 1
(highs still produce 2). Public for testing.
@spec tally([finding()]) :: %{ high: non_neg_integer(), medium: non_neg_integer(), low: non_neg_integer() }
Tally findings into %{high: n, medium: n, low: n}. Public for testing.