lib/brakeman/scanner.rb
begin
Brakeman.load_brakeman_dependency 'ruby_parser'
require 'ruby_parser/bm_sexp.rb'
require 'ruby_parser/bm_sexp_processor.rb'
require 'brakeman/processor'
require 'brakeman/app_tree'
require 'brakeman/file_parser'
require 'brakeman/parsers/template_parser'
require 'brakeman/processors/lib/file_type_detector'
rescue LoadError => e
$stderr.puts e.message
$stderr.puts "Please install the appropriate dependency."
exit(-1)
end
#Scans the Rails application.
class Brakeman::Scanner
attr_reader :options
#Pass in path to the root of the Rails application
def initialize options, processor = nil
@options = options
@app_tree = Brakeman::AppTree.from_options(options)
if (!@app_tree.root || !@app_tree.exists?("app")) && !options[:force_scan]
message = "Please supply the path to a Rails application (looking in #{@app_tree.root}).\n" <<
" Use `--force` to run a scan anyway."
raise Brakeman::NoApplication, message
end
@processor = processor || Brakeman::Processor.new(@app_tree, options)
@show_timing = tracker.options[:debug] || tracker.options[:show_timing]
end
#Returns the Tracker generated from the scan
def tracker
@processor.tracked_events
end
def process_step description
Brakeman.notify "#{description}...".ljust(40)
if @show_timing
start_t = Time.now
yield
duration = Time.now - start_t
Brakeman.notify "(#{description}) Duration: #{duration} seconds"
else
yield
end
end
def process_step_file description
if @show_timing
Brakeman.notify "Processing #{description}"
start_t = Time.now
yield
duration = Time.now - start_t
Brakeman.notify "(#{description}) Duration: #{duration} seconds"
else
yield
end
end
#Process everything in the Rails application
def process
process_step 'Processing gems' do
process_gems
end
process_step 'Processing configuration' do
guess_rails_version
process_config
end
process_step 'Parsing files' do
parse_files
end
process_step 'Detecting file types' do
detect_file_types
end
process_step 'Processing initializers' do
process_initializers
end
process_step 'Processing libs' do
process_libs
end
process_step 'Processing routes' do
process_routes
end
process_step 'Processing templates' do
process_templates
end
process_step 'Processing data flow in templates' do
process_template_data_flows
end
process_step 'Processing models' do
process_models
end
process_step 'Processing controllers' do
process_controllers
end
process_step 'Processing data flow in controllers' do
process_controller_data_flows
end
process_step 'Indexing call sites' do
index_call_sites
end
tracker
end
def parse_files
fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout], tracker.options[:parallel_checks])
fp.parse_files tracker.app_tree.ruby_file_paths
template_parser = Brakeman::TemplateParser.new(tracker, fp)
fp.read_files(@app_tree.template_paths) do |path, contents|
template_parser.parse_template path, contents
end
# Collect errors raised during parsing
tracker.add_errors(fp.errors)
@parsed_files = fp.file_list
end
def detect_file_types
@file_list = {
controllers: [],
initializers: [],
libs: [],
models: [],
templates: [],
}
detector = Brakeman::FileTypeDetector.new
@parsed_files.each do |file|
if file.is_a? Brakeman::TemplateParser::TemplateFile
@file_list[:templates] << file
else
type = detector.detect_type(file)
unless type == :skip
if @file_list[type].nil?
raise type.to_s
else
@file_list[type] << file
end
end
end
end
end
#Process config/environment.rb and config/gems.rb
#
#Stores parsed information in tracker.config
def process_config
# Sometimes folks like to put constants in environment.rb
# so let's always process it even for newer Rails versions
process_config_file "environment.rb"
if options[:rails3] or options[:rails4] or options[:rails5] or options[:rails6]
process_config_file "application.rb"
process_config_file "environments/production.rb"
else
process_config_file "gems.rb"
end
if @app_tree.exists?("vendor/plugins/rails_xss") or
options[:rails3] or options[:escape_html]
tracker.config.escape_html = true
Brakeman.notify "[Notice] Escaping HTML by default"
end
if @app_tree.exists? ".ruby-version"
if version = @app_tree.file_path(".ruby-version").read[/(\d\.\d.\d+)/]
tracker.config.set_ruby_version version, @app_tree.file_path(".ruby-version"), 1
end
end
tracker.config.load_rails_defaults
end
def process_config_file file
path = @app_tree.file_path("config/#{file}")
if path.exists?
@processor.process_config(parse_ruby_file(path), path)
end
rescue => e
Brakeman.notify "[Notice] Error while processing #{path}"
tracker.error e.exception(e.message + "\nwhile processing #{path}"), e.backtrace
end
private :process_config_file
#Process Gemfile
def process_gems
gem_files = {}
if @app_tree.exists? "Gemfile"
file = @app_tree.file_path("Gemfile")
gem_files[:gemfile] = { :src => parse_ruby_file(file), :file => file }
elsif @app_tree.exists? "gems.rb"
file = @app_tree.file_path("gems.rb")
gem_files[:gemfile] = { :src => parse_ruby_file(file), :file => file }
end
if @app_tree.exists? "Gemfile.lock"
file = @app_tree.file_path("Gemfile.lock")
gem_files[:gemlock] = { :src => file.read, :file => file }
elsif @app_tree.exists? "gems.locked"
file = @app_tree.file_path("gems.locked")
gem_files[:gemlock] = { :src => file.read, :file => file }
end
if @app_tree.gemspec
gem_files[:gemspec] = { :src => parse_ruby_file(@app_tree.gemspec), :file => @app_tree.gemspec }
end
if not gem_files.empty?
@processor.process_gems gem_files
end
rescue => e
Brakeman.notify "[Notice] Error while processing Gemfile."
tracker.error e.exception(e.message + "\nWhile processing Gemfile"), e.backtrace
end
#Set :rails3/:rails4 option if version was not determined from Gemfile
def guess_rails_version
unless tracker.options[:rails3] or tracker.options[:rails4]
if @app_tree.exists?("script/rails")
tracker.options[:rails3] = true
Brakeman.notify "[Notice] Detected Rails 3 application"
elsif @app_tree.exists?("app/channels")
tracker.options[:rails3] = true
tracker.options[:rails4] = true
tracker.options[:rails5] = true
Brakeman.notify "[Notice] Detected Rails 5 application"
elsif not @app_tree.exists?("script")
tracker.options[:rails3] = true
tracker.options[:rails4] = true
Brakeman.notify "[Notice] Detected Rails 4 application"
end
end
end
#Process all the .rb files in config/initializers/
#
#Adds parsed information to tracker.initializers
def process_initializers
track_progress @file_list[:initializers] do |init|
process_step_file init[:path] do
process_initializer init
end
end
end
#Process an initializer
def process_initializer init
@processor.process_initializer(init.path, init.ast)
end
#Process all .rb in lib/
#
#Adds parsed information to tracker.libs.
def process_libs
if options[:skip_libs]
Brakeman.notify '[Skipping]'
return
end
track_progress @file_list[:libs] do |lib|
process_step_file lib.path do
process_lib lib
end
end
end
#Process a library
def process_lib lib
@processor.process_lib lib.ast, lib.path
end
#Process config/routes.rb
#
#Adds parsed information to tracker.routes
def process_routes
if @app_tree.exists?("config/routes.rb")
file = @app_tree.file_path("config/routes.rb")
if routes_sexp = parse_ruby_file(file)
@processor.process_routes routes_sexp
else
Brakeman.notify "[Notice] Error while processing routes - assuming all public controller methods are actions."
options[:assume_all_routes] = true
end
else
Brakeman.notify "[Notice] No route information found"
end
end
#Process all .rb files in controllers/
#
#Adds processed controllers to tracker.controllers
def process_controllers
track_progress @file_list[:controllers] do |controller|
process_step_file controller.path do
process_controller controller
end
end
end
def process_controller_data_flows
controllers = tracker.controllers.sort_by { |name, _| name.to_s }
track_progress controllers, "controllers" do |name, controller|
process_step_file name do
controller.src.each do |file, src|
@processor.process_controller_alias name, src, nil, file
end
end
end
#No longer need these processed filter methods
tracker.filter_cache.clear
end
def process_controller astfile
begin
@processor.process_controller(astfile.ast, astfile.path)
rescue => e
tracker.error e.exception(e.message + "\nWhile processing #{astfile.path}"), e.backtrace
end
end
#Process all views and partials in views/
#
#Adds processed views to tracker.views
def process_templates
templates = @file_list[:templates].sort_by { |t| t[:path] }
track_progress templates, "templates" do |template|
process_step_file template[:path] do
process_template template
end
end
end
def process_template template
@processor.process_template(template.name, template.ast, template.type, nil, template.path)
end
def process_template_data_flows
templates = tracker.templates.sort_by { |name, _| name.to_s }
track_progress templates, "templates" do |name, template|
process_step_file name do
@processor.process_template_alias template
end
end
end
#Process all the .rb files in models/
#
#Adds the processed models to tracker.models
def process_models
track_progress @file_list[:models] do |model|
process_step_file model[:path] do
process_model model[:path], model[:ast]
end
end
end
def process_model path, ast
@processor.process_model(ast, path)
end
def track_progress list, type = "files"
total = list.length
current = 0
list.each do |item|
report_progress current, total, type
current += 1
yield item
end
end
def report_progress(current, total, type = "files")
return unless @options[:report_progress]
$stderr.print " #{current}/#{total} #{type} processed\r"
end
def index_call_sites
tracker.index_call_sites
end
def parse_ruby_file file
fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout])
fp.parse_ruby(file.read, file)
rescue Exception => e
tracker.error(e)
nil
end
end
# This is to allow operation without loading the Haml library
module Haml; class Error < StandardError; end; end