Current section
Files
Jump to
Current section
Files
lib/reach/trace/flow.ex
defmodule Reach.Trace.Flow do
@moduledoc "Taint and variable flow tracing through data dependence edges."
alias Reach.IR
alias Reach.Project.Query
alias Reach.Trace.Flow.{Path, Result}
alias Reach.Trace.Pattern
@max_intermediate_nodes 10
def analyze_taint(project, from_pattern, to_pattern, max_paths) do
sources = find_nodes(project, Pattern.compile(from_pattern, project.plugins))
sinks = find_nodes(project, Pattern.compile(to_pattern, project.plugins))
paths = find_taint_paths(project, sources, sinks, max_paths)
Result.new(type: :taint, from: from_pattern, to: to_pattern, paths: paths)
end
def analyze_variable(project, var_name, scope) do
scope_nodes = resolve_scope_nodes(project, scope)
definitions =
scope_nodes
|> Enum.filter(fn node ->
node.type == :var and node.meta[:binding_role] == :definition and
to_string(node.meta[:name]) == var_name
end)
|> Enum.sort_by(&location_key/1)
uses =
scope_nodes
|> Enum.filter(fn node ->
node.type == :var and node.meta[:binding_role] != :definition and
to_string(node.meta[:name]) == var_name
end)
|> Enum.sort_by(&location_key/1)
Result.new(type: :variable, variable: var_name, definitions: definitions, uses: uses)
end
defp resolve_scope_nodes(project, nil), do: Map.values(project.nodes)
defp resolve_scope_nodes(project, func_name) do
nodes = Map.values(project.nodes)
case Query.resolve_function(project, func_name) do
nil ->
nodes
{mod, fun, arity} ->
func_node =
Enum.find(nodes, fn node ->
node.type == :function_def and
{node.meta[:module], node.meta[:name], node.meta[:arity]} == {mod, fun, arity}
end)
if func_node, do: IR.all_nodes(func_node), else: nodes
end
end
defp find_nodes(project, filter) do
for {_id, node} <- project.nodes, filter.(node), do: node
end
defp find_taint_paths(project, sources, sinks, max_paths) do
graph = project.graph
sink_by_id = Map.new(sinks, &{&1.id, &1})
sink_ids = MapSet.new(Map.keys(sink_by_id))
stream =
sources
|> Stream.flat_map(fn source -> reachable_sinks(graph, source, sink_ids, sink_by_id) end)
|> Stream.map(fn {source, sink} -> build_path(project, source, sink) end)
if max_paths == :all, do: Enum.to_list(stream), else: Enum.take(stream, max_paths)
end
defp reachable_sinks(graph, source, sink_ids, sink_by_id) do
if Graph.has_vertex?(graph, source.id) do
graph
|> Graph.reachable([source.id])
|> MapSet.new()
|> MapSet.intersection(sink_ids)
|> Enum.map(&{source, Map.fetch!(sink_by_id, &1)})
else
[]
end
end
defp build_path(project, source, sink) do
graph = project.graph
if Graph.has_vertex?(graph, source.id) and Graph.has_vertex?(graph, sink.id) do
fwd = Graph.reachable(graph, [source.id]) |> MapSet.new()
bwd = Graph.reaching(graph, [sink.id]) |> MapSet.new()
path_ids = MapSet.intersection(fwd, bwd) |> MapSet.to_list()
path_nodes =
path_ids
|> Enum.map(fn id -> Map.get(project.nodes, id) end)
|> Enum.reject(&is_nil/1)
|> Enum.filter(& &1.source_span)
|> Enum.sort_by(fn node -> {node.source_span[:file], node.source_span[:start_line]} end)
|> Enum.uniq_by(fn node -> {node.source_span[:file], node.source_span[:start_line]} end)
|> Enum.take(@max_intermediate_nodes)
Path.new(source: source, sink: sink, intermediate: path_nodes)
else
Path.new(source: source, sink: sink, intermediate: [])
end
end
defp location_key(node) do
span = node.source_span || %{}
{span[:file] || "", span[:start_line] || 0, node.id}
end
end