Bash.CommandPolicy (Bash v0.6.0)

Copy Markdown View Source

Policy engine for controlling command execution and filesystem access.

A %CommandPolicy{} struct is stored as a top-level field on Bash.Session state. It is immutable once the session is initialized — set, shopt, and other runtime mechanisms cannot modify it.

Fields

  • :commands — gates command execution across all categories: builtins, externals, shell functions, and Elixir interop calls.

  • :paths — gates all filesystem access. Every resolved absolute path is checked against these rules before any filesystem operation (cd, source, redirects, test operators, glob expansion).

Command Categories

Every command resolved by the interpreter falls into one of four categories:

  • :builtin — built-in shell commands (echo, cd, export, etc.)
  • :external — external OS commands (cat, grep, /usr/bin/sort, etc.)
  • :function — user-defined shell functions
  • :interop — Elixir interop commands registered via Bash.Interop

Command Policy Types

  • :unrestricted — all commands in all categories are allowed (default).
  • :no_external — block all external command execution; other categories allowed.
  • A list of rules — evaluated in order, first match wins.
  • A function (String.t(), command_category() -> boolean()) — dynamic evaluation per command and category.
  • A function (String.t() -> boolean()) — legacy dynamic evaluation, only applied to external commands.

Rule Evaluation

When commands is a list, each rule is tried in order:

  • {:allow, items} — if the command matches any item, it is allowed.
  • {:disallow, items} — if the command matches any item, it is denied.
  • {:allow, :all} — allows all commands unconditionally.
  • {:disallow, :all} — denies all commands unconditionally.
  • fun/2 — receives (name, category), if it returns true, the command is allowed; false continues to the next rule.
  • fun/1 — legacy; only evaluated for :external commands, skipped for others.

Items can be strings (exact or basename match), Regex patterns, or category atoms:

  • :builtins — matches all commands in the :builtin category
  • :externals — matches all commands in the :external category
  • :functions — matches all commands in the :function category
  • :interop — matches all commands in the :interop category

If no rule matches, the command is denied.

Common Recipes

The table below shows how to set up policies for common scenarios. Categories not listed in an {:allow, [...]} rule are denied by default (fail-closed).

GoalPolicy
Allow everything (default)commands: :unrestricted
Block all externalscommands: :no_external
Builtins onlycommands: [{:allow, [:builtins]}]
Externals onlycommands: [{:allow, [:externals]}]
Builtins + functionscommands: [{:allow, [:builtins, :functions]}]
Builtins + externalscommands: [{:allow, [:builtins, :externals]}]
Builtins + interopcommands: [{:allow, [:builtins, :interop]}]
Builtins + functions + interopcommands: [{:allow, [:builtins, :functions, :interop]}]
Everything except externalscommands: [{:disallow, [:externals]}, {:allow, :all}]
Everything except interopcommands: [{:disallow, [:interop]}, {:allow, :all}]
Block specific builtinscommands: [{:disallow, ["eval", "source"]}, {:allow, :all}]
Builtins + specific externalscommands: [{:allow, [:builtins, "cat", "grep"]}]
Block everythingcommands: [{:disallow, :all}]

Examples

# Block all external commands (shorthand)
%CommandPolicy{commands: :no_external}

# Allow only builtins and specific externals
%CommandPolicy{commands: [{:allow, [:builtins, "cat", "grep"]}]}

# Allow builtins and functions, block everything else
%CommandPolicy{commands: [{:allow, [:builtins, :functions]}]}

# Block eval and source, allow everything else
%CommandPolicy{commands: [{:disallow, ["eval", "source"]}, {:allow, :all}]}

# Regex-based rules
%CommandPolicy{commands: [{:allow, [~r/^git-/]}]}

# Category-aware function
%CommandPolicy{commands: fn _cmd, cat -> cat in [:builtin, :function] end}

Summary

Functions

Returns true if the policy allows any external commands at all.

Checks whether the given command is allowed under the policy.

Checks whether access to the given filesystem path is allowed under the policy.

Returns true if the specific command is allowed under the policy.

Extracts the command policy from session state.

Builds a %CommandPolicy{} from a keyword list, map, or existing struct.

Returns true if the given path is allowed under the policy.

Types

command_category()

@type command_category() :: :builtin | :external | :function | :interop

command_rule()

@type command_rule() ::
  :unrestricted
  | :no_external
  | [
      {:allow, :all | [rule_item()]}
      | {:disallow, :all | [rule_item()]}
      | (String.t(), command_category() -> boolean())
      | (String.t() -> boolean())
    ]
  | (String.t(), command_category() -> boolean())
  | (String.t() -> boolean())

path_rule()

@type path_rule() ::
  nil
  | [
      {:allow, :all | [rule_item()]}
      | {:disallow, [rule_item()]}
      | (String.t() -> boolean())
    ]
  | (String.t() -> boolean())

rule_item()

@type rule_item() ::
  String.t() | Regex.t() | :builtins | :externals | :functions | :interop

t()

@type t() :: %Bash.CommandPolicy{commands: command_rule(), paths: path_rule()}

Functions

allows_external?(command_policy)

@spec allows_external?(t()) :: boolean()

Returns true if the policy allows any external commands at all.

Used by pipeline optimization to decide if the streaming path is viable.

check_command(policy, command_name, category \\ :external)

@spec check_command(t(), String.t(), command_category()) :: :ok | {:error, String.t()}

Checks whether the given command is allowed under the policy.

The category argument identifies what kind of command is being checked: :builtin, :external, :function, or :interop. Defaults to :external for backwards compatibility.

Returns :ok or {:error, message} with a descriptive error string.

check_path(command_policy, path)

@spec check_path(t(), String.t()) :: :ok | {:error, String.t()}

Checks whether access to the given filesystem path is allowed under the policy.

Returns :ok when paths is nil (no restrictions) or the path passes the rules. Returns {:error, message} when the path is denied.

command_allowed?(policy, command_name, category \\ :external)

@spec command_allowed?(t(), String.t(), command_category()) :: boolean()

Returns true if the specific command is allowed under the policy.

from_state(arg1)

@spec from_state(map()) :: t()

Extracts the command policy from session state.

Returns the default (unrestricted) policy when none is configured.

new(policy)

@spec new(keyword() | map() | t()) :: t()

Builds a %CommandPolicy{} from a keyword list, map, or existing struct.

String lists inside {:allow, list} and {:disallow, list} are partitioned into a MapSet (for O(1) string lookup), a list of non-string matchers (regex, functions), and a MapSet of category atoms.

iex> CommandPolicy.new(commands: :no_external)
%CommandPolicy{commands: :no_external}

iex> CommandPolicy.new(commands: [{:allow, [:builtins, "cat", ~r/^git-/]}])
# categories -> MapSet, strings -> MapSet, regex kept separately

path_allowed?(policy, path)

@spec path_allowed?(t(), String.t()) :: boolean()

Returns true if the given path is allowed under the policy.