# `MobDev.Plugin.Audit`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/plugin/audit.ex#L1)

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,3` and `Code.compile_string/1,2`
  — the prime arbitrary-code-execution vector. Severity `:high`.
- `String.to_atom/1` with a non-literal argument — atom-exhaustion risk.
  A literal `String.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,4` targeting `:mob` — would let a plugin silently
  retarget plugin activations, host_config keys, etc. `get_env` is 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/1` with 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 raw
  `socket(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.

# `finding`

```elixir
@type finding() :: %{
  severity: severity(),
  rule: atom(),
  plugin: atom() | nil,
  file: String.t(),
  line: pos_integer() | nil,
  snippet: String.t(),
  hint: String.t()
}
```

# `report`

```elixir
@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()
}
```

# `severity`

```elixir
@type severity() :: :high | :medium | :low
```

# `audit_c_file`

```elixir
@spec audit_c_file(Path.t(), Path.t(), atom() | nil) :: [finding()]
```

Audits a single C source file in isolation. Public for testing.

# `audit_elixir_file`

```elixir
@spec audit_elixir_file(Path.t(), Path.t(), atom() | nil) :: [finding()]
```

Audits a single Elixir source file in isolation. Public for testing.

# `audit_plugin`

```elixir
@spec audit_plugin(Path.t(), map() | nil) :: report()
```

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 `t:report/0` map.

# `exit_code`

```elixir
@spec exit_code([report()], boolean()) :: 0 | 1 | 2
```

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.

# `tally`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
