Packages
metastatic
0.10.2
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/analysis/taint.ex
defmodule Metastatic.Analysis.Taint do
@moduledoc """
Taint analysis at the MetaAST level.
Tracks data flow from untrusted sources (taint sources) to dangerous
operations (taint sinks), identifying potential security vulnerabilities.
Works across all supported languages.
## Taint Sources
- User input functions (input, gets, argv)
- File reads
- Network requests
- Environment variables
## Taint Sinks
- Code execution (eval, exec, system)
- SQL queries
- File operations
- Shell commands
## Usage
alias Metastatic.{Document, Analysis.Taint}
# Analyze for taint flows
ast = {:function_call, [name: "eval"], [{:function_call, [name: "input"], []}]}
doc = Document.new(ast, :python)
{:ok, result} = Taint.analyze(doc)
result.has_taint_flows? # => true
## Examples
# No taint flows
iex> ast = {:binary_op, [category: :arithmetic, operator: :+], [{:literal, [subtype: :integer], 1}, {:literal, [subtype: :integer], 2}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Taint.analyze(doc)
iex> result.has_taint_flows?
false
# Tainted data to sink
iex> ast = {:function_call, [name: "eval"], [{:function_call, [name: "input"], []}]}
iex> doc = Metastatic.Document.new(ast, :python)
iex> {:ok, result} = Metastatic.Analysis.Taint.analyze(doc)
iex> result.has_taint_flows?
true
"""
alias Metastatic.Analysis.Taint.Result
alias Metastatic.Document
# Taint sources by language (expanded for better CWE-78/77/94 coverage)
@taint_sources %{
python: [
"input",
"raw_input",
"sys.argv",
"request.args",
"request.form",
"request.data",
"request.json",
"request.files",
"request.cookies",
"os.environ",
"getenv",
"flask.request",
"django.request",
"sys.stdin",
"fileinput"
],
javascript: [
"req.body",
"req.query",
"req.params",
"req.headers",
"req.cookies",
"process.argv",
"process.env",
"readline",
"prompt"
],
elixir: [
"IO.gets",
"System.argv",
"System.get_env",
"conn.params",
"conn.body_params",
"conn.query_params",
"socket.assigns",
"Phoenix.LiveView"
],
ruby: [
"params",
"request.params",
"ARGV",
"ENV",
"gets",
"readline",
"request.body",
"request.headers"
],
erlang: ["io:get_line", "init:get_argument", "os:getenv"]
}
# Taint sinks by language (expanded for CWE-78/77/94)
# CWE-78: OS Command Injection
# CWE-77: Command Injection
# CWE-94: Code Injection
@taint_sinks %{
python: [
# CWE-94: Code Injection
{"eval", :critical},
{"exec", :critical},
{"compile", :high},
{"__import__", :high},
{"importlib.import_module", :high},
# CWE-78/77: Command Injection
{"os.system", :critical},
{"os.popen", :critical},
{"subprocess.call", :high},
{"subprocess.run", :high},
{"subprocess.Popen", :high},
{"commands.getoutput", :critical},
{"commands.getstatusoutput", :critical},
# Shell utilities
{"os.execl", :critical},
{"os.execle", :critical},
{"os.execlp", :critical},
{"os.execv", :critical},
{"os.execve", :critical},
{"os.spawnl", :high},
{"os.spawnle", :high}
],
javascript: [
# CWE-94: Code Injection
{"eval", :critical},
{"Function", :critical},
{"setTimeout", :high},
{"setInterval", :high},
{"new Function", :critical},
# CWE-78/77: Command Injection
{"child_process.exec", :critical},
{"child_process.execSync", :critical},
{"child_process.spawn", :high},
{"child_process.spawnSync", :high},
{"child_process.execFile", :high},
{"require('child_process')", :high}
],
elixir: [
# CWE-94: Code Injection
{"Code.eval_string", :critical},
{"Code.eval_quoted", :critical},
{"Code.eval_file", :critical},
{"Code.compile_string", :high},
# CWE-78/77: Command Injection
{":os.cmd", :critical},
{"System.cmd", :high},
{"System.shell", :critical},
{"Port.open", :high}
],
ruby: [
# CWE-94: Code Injection
{"eval", :critical},
{"instance_eval", :critical},
{"class_eval", :critical},
{"module_eval", :critical},
{"send", :high},
{"__send__", :high},
{"public_send", :high},
# CWE-78/77: Command Injection
{"system", :critical},
{"exec", :critical},
# backticks
{"`", :critical},
{"%x", :critical},
{"Open3.capture2", :high},
{"Open3.capture3", :high},
{"IO.popen", :high}
],
erlang: [
{"erl_eval:expr", :critical},
{"erl_eval:exprs", :critical},
{":os.cmd", :critical},
{"open_port", :high}
]
}
use Metastatic.Document.Analyzer,
doc: """
Analyzes a document for taint flows.
Returns `{:ok, result}` where result is a `Metastatic.Analysis.Taint.Result` struct.
## Examples
iex> ast = {:literal, [subtype: :integer], 42}
iex> doc = Metastatic.Document.new(ast, :elixir)
iex> {:ok, result} = Metastatic.Analysis.Taint.analyze(doc)
iex> result.has_taint_flows?
false
"""
@impl Metastatic.Document.Analyzer
def handle_analyze(%Document{ast: ast, language: language} = _doc, _opts \\ []) do
# Find taint sources
sources = find_taint_sources(ast, language)
# Find taint sinks
sinks = find_taint_sinks(ast, language)
# Detect flows from sources to sinks
flows = detect_taint_flows(ast, sources, sinks)
{:ok, Result.new(flows)}
end
# Private implementation
# 3-tuple format
defp find_taint_sources(ast, language) do
source_funcs = Map.get(@taint_sources, language, [])
walk_ast(ast, [], fn node, acc ->
case node do
{:function_call, meta, _args} when is_list(meta) ->
name = Keyword.get(meta, :name, "")
if name in source_funcs do
[name | acc]
else
acc
end
{:variable, _meta, "argv"} ->
["argv" | acc]
_ ->
acc
end
end)
end
defp find_taint_sinks(ast, language) do
sink_patterns = Map.get(@taint_sinks, language, [])
walk_ast(ast, [], fn node, acc ->
case node do
{:function_call, meta, _args} when is_list(meta) ->
name = Keyword.get(meta, :name, "")
case Enum.find(sink_patterns, fn {n, _} -> n == name end) do
{func, risk} -> [{func, risk} | acc]
nil -> acc
end
_ ->
acc
end
end)
end
# Simplified taint flow detection
# Checks if any sink has a taint source in its arguments
# 3-tuple format
defp detect_taint_flows(ast, sources, sinks) do
if Enum.empty?(sources) or Enum.empty?(sinks) do
[]
else
walk_ast(ast, [], fn node, acc ->
with {:function_call, meta, args} when is_list(meta) <- node,
name <- Keyword.get(meta, :name, ""),
{sink_name, risk} when is_list(args) <-
Enum.find(sinks, fn {sink_name, _} -> sink_name == name end),
true <- has_taint_source?(args, sources) do
flow = %{
source: Enum.join(sources, ", "),
sink: sink_name,
risk: risk,
path: [sink_name],
recommendation: get_recommendation(risk)
}
[flow | acc]
else
_ -> acc
end
end)
end
end
defp has_taint_source?(args, sources) when is_list(args) do
Enum.any?(args, fn arg -> check_taint_in_ast(arg, sources) end)
end
defp check_taint_in_ast(ast, sources) do
case ast do
{:function_call, meta, _args} when is_list(meta) ->
name = Keyword.get(meta, :name, "")
name in sources
{:variable, _meta, name} ->
name in sources or name == "argv"
{:binary_op, _meta, [left, right]} ->
check_taint_in_ast(left, sources) or check_taint_in_ast(right, sources)
{:block, _meta, statements} when is_list(statements) ->
Enum.any?(statements, &check_taint_in_ast(&1, sources))
_ ->
false
end
end
defp walk_ast(ast, acc, func) do
acc = func.(ast, acc)
case ast do
{:block, _meta, statements} when is_list(statements) ->
Enum.reduce(statements, acc, fn stmt, a -> walk_ast(stmt, a, func) end)
{:conditional, _meta, [cond_expr, then_br, else_br]} ->
acc
|> walk_ast(cond_expr, func)
|> walk_ast(then_br, func)
|> walk_ast(else_br, func)
{:binary_op, _meta, [left, right]} ->
acc |> walk_ast(left, func) |> walk_ast(right, func)
{:function_call, _meta, args} when is_list(args) ->
Enum.reduce(args, acc, fn arg, a -> walk_ast(arg, a, func) end)
{:assignment, _meta, [target, value]} ->
acc |> walk_ast(target, func) |> walk_ast(value, func)
{:loop, meta, children} when is_list(meta) and is_list(children) ->
Enum.reduce(children, acc, fn child, a -> walk_ast(child, a, func) end)
nil ->
acc
_ ->
acc
end
end
defp get_recommendation(:critical) do
"Never pass untrusted input to code execution functions. Validate and sanitize all input."
end
defp get_recommendation(:high) do
"Sanitize and validate all user input before use. Use parameterized queries or safe alternatives."
end
defp get_recommendation(_) do
"Review data flow and add appropriate input validation."
end
end