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'
  require 'brakeman/tracker/file_cache'
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]
    @per_file_timing = tracker.options[:debug] && tracker.options[:show_timing]
  end

  #Returns the Tracker generated from the scan
  def tracker
    @processor.tracked_events
  end

  def file_cache
    tracker.file_cache
  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 @per_file_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(ruby_paths: nil, template_paths: nil)
    process_step 'Processing gems' do
      process_gems
    end

    process_step 'Processing configuration' do
      guess_rails_version
      process_config
    end

    # -
    # If ruby_paths or template_paths are set,
    # only parse those files. The rest will be fetched
    # from the file cache.
    #
    # Otherwise, parse everything normally.
    #
    astfiles = nil
    process_step 'Finding files' do
      ruby_paths ||= tracker.app_tree.ruby_file_paths
      template_paths ||= tracker.app_tree.template_paths
    end

    process_step 'Parsing files' do
      astfiles = parse_files(ruby_paths: ruby_paths, template_paths: template_paths)
    end

    process_step 'Detecting file types' do
      detect_file_types(astfiles)
    end

    tracker.save_file_cache! if support_rescanning?
    # -

    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(ruby_paths:, template_paths:)
    fp = Brakeman::FileParser.new(tracker.app_tree, tracker.options[:parser_timeout], tracker.options[:parallel_checks], tracker.options[:use_prism])

    fp.parse_files ruby_paths

    template_parser = Brakeman::TemplateParser.new(tracker, fp)

    fp.read_files(template_paths) do |path, contents|
      template_parser.parse_template(path, contents)
    end

    # Collect errors raised during parsing
    tracker.add_errors(fp.errors)

    fp.file_list
  end

  def detect_file_types(astfiles)
    detector = Brakeman::FileTypeDetector.new

    astfiles.each do |file|
      if file.is_a? Brakeman::TemplateParser::TemplateFile
        file_cache.add_file file, :template
      else
        type = detector.detect_type(file)

        unless type == :skip
          if file_cache.valid_type? type
            file_cache.add_file(file, type)
          else
            raise "Unexpected file type: #{type.inspect}"
          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 = {}
    gem_file_names = ['Gemfile', 'gems.rb']
    lock_file_names = ['Gemfile.lock', 'gems.locked']

    if tracker.options[:gemfile]
      name = tracker.options[:gemfile]
      gem_file_names.unshift name
      lock_file_names.unshift "#{name}.lock"
    end

    gem_file_names.each do |name|
      if @app_tree.exists? name
        file = @app_tree.file_path(name)
        gem_files[:gemfile] = { :src => parse_ruby_file(file), :file => file }
        break
      end
    end

    lock_file_names.each do |name|
      if @app_tree.exists? name
        file = @app_tree.file_path(name)
        gem_files[:gemlock] = { :src => file.read, :file => file }
        break
      end
    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_cache.initializers do |path, init|
      process_step_file 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

    libs = file_cache.libs.sort_by { |path, _| path }

    track_progress libs do |path, lib|
      process_step_file 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
    controllers = file_cache.controllers.sort_by { |path, _| path }

    track_progress controllers do |path, controller|
      process_step_file path do
        process_controller controller
      end
    end
  end

  def process_controller_data_flows
    controllers = tracker.controllers.sort_by { |name, _| name }

    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_cache.templates.sort_by { |path, _| path }

    track_progress templates, "templates" do |path, template|
      process_step_file 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 }

    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
    models = file_cache.models.sort_by { |path, _| path }

    track_progress models do |path, model|
      process_step_file path do
        process_model model
      end
    end
  end

  def process_model astfile
    @processor.process_model(astfile.ast, astfile.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], false, tracker.options[:use_prism])
    fp.parse_ruby(file.read, file)
  rescue Exception => e
    tracker.error(e)
    nil
  end

  def support_rescanning?
    tracker.options[:support_rescanning]
  end
end

# This is to allow operation without loading the Haml library
module Haml; class Error < StandardError; end; end