require 'brakeman/scanner' require 'brakeman/util' require 'brakeman/differ' #Class for rescanning changed files after an initial scan class Brakeman::Rescanner < Brakeman::Scanner include Brakeman::Util KNOWN_TEMPLATE_EXTENSIONS = Brakeman::TemplateParser::KNOWN_TEMPLATE_EXTENSIONS SCAN_ORDER = [:config, :gemfile, :initializer, :lib, :routes, :template, :model, :controller] #Create new Rescanner to scan changed files def initialize options, processor, changed_files super(options, processor) @paths = changed_files.map {|f| tracker.app_tree.file_path(f) } @old_results = tracker.filtered_warnings #Old warnings from previous scan @changes = nil #True if files had to be rescanned @reindex = Set.new end #Runs checks. #Will rescan files if they have not already been scanned def recheck rescan if @changes.nil? tracker.run_checks if @changes Brakeman::RescanReport.new @old_results, tracker end #Rescans changed files def rescan tracker.template_cache.clear paths_by_type = {} SCAN_ORDER.each do |type| paths_by_type[type] = [] end @paths.each do |path| type = file_type(path) paths_by_type[type] << path unless type == :unknown end @changes = false SCAN_ORDER.each do |type| paths_by_type[type].each do |path| Brakeman.debug "Rescanning #{path} as #{type}" if rescan_file path, type @changes = true end end end if @changes and not @reindex.empty? tracker.reindex_call_sites @reindex end self end #Rescans a single file def rescan_file path, type = nil type ||= file_type path unless path.exists? return rescan_deleted_file path, type end case type when :controller rescan_controller path when :template rescan_template path when :model rescan_model path when :lib rescan_lib path when :config process_config when :initializer rescan_initializer path when :routes rescan_routes when :gemfile if tracker.config.has_gem? :rails_xss and tracker.config.escape_html? tracker.config.escape_html = false end process_gems else return false #Nothing to do, file hopefully does not need to be rescanned end true end def rescan_controller path controller = tracker.reset_controller path paths = controller.nil? ? [path] : controller.files parse_ruby_files(paths).each do |astfile| process_controller astfile end #Process data flow and template rendering #from the controller tracker.controllers.each do |name, controller| if controller.files.include?(path) tracker.templates.each do |template_name, template| next unless template.render_path if template.render_path.include_controller? name tracker.reset_template template_name end end controller.src.each do |file, src| @processor.process_controller_alias controller.name, src, nil, file end end end @reindex << :templates << :controllers end def rescan_template path return unless path.relative.match KNOWN_TEMPLATE_EXTENSIONS and path.exists? template_name = template_path_to_name(path) tracker.reset_template template_name fp = Brakeman::FileParser.new(tracker) template_parser = Brakeman::TemplateParser.new(tracker, fp) template_parser.parse_template path, path.read process_template fp.file_list[:templates].first @processor.process_template_alias tracker.templates[template_name] rescan = Set.new #Search for processed template and process it. #Search for rendered versions of template and re-render (if necessary) tracker.templates.each do |_name, template| if template.file == path or template.file.nil? next unless template.render_path and template.name.to_sym == template_name.to_sym template.render_path.each do |from| case from[:type] when :template rescan << [:template, from[:name]] when :controller rescan << [:controller, from[:class], from[:method]] end end end end rescan.each do |r| if r[0] == :controller controller = tracker.controllers[r[1]] controller.src.each do |file, src| unless @paths.include? file @processor.process_controller_alias controller.name, src, r[2], file end end elsif r[0] == :template template = tracker.templates[r[1]] rescan_template template.file end end @reindex << :templates end def rescan_model path num_models = tracker.models.length model = tracker.reset_model path paths = model.nil? ? [path] : model.files parse_ruby_files(paths).each do |astfile| process_model astfile.path, astfile.ast end #Only need to rescan other things if a model is added or removed if num_models != tracker.models.length process_template_data_flows process_controller_data_flows @reindex << :templates << :controllers end @reindex << :models end def rescan_lib path lib = tracker.reset_lib path paths = lib.nil? ? [path] : lib.files parse_ruby_files(paths).each do |astfile| process_lib astfile end lib = nil tracker.libs.each do |_name, library| if library.files.include?(path) lib = library break end end rescan_mixin lib if lib end def rescan_routes # Routes affect which controller methods are treated as actions # which affects which templates are rendered, so routes, controllers, # and templates rendered from controllers must be rescanned tracker.reset_routes tracker.reset_templates :only_rendered => true process_routes process_controller_data_flows @reindex << :controllers << :templates end def rescan_initializer path tracker.reset_initializer path parse_ruby_files([path]).each do |astfile| process_initializer astfile end @reindex << :initializers end #Handle rescanning when a file is deleted def rescan_deleted_file path, type case type when :controller rescan_controller path when :template rescan_deleted_template path when :model rescan_model path when :lib rescan_deleted_lib path when :initializer rescan_deleted_initializer path else if remove_deleted_file path return true else Brakeman.notify "Ignoring deleted file: #{path}" end end true end def rescan_deleted_template path return unless path.relative.match KNOWN_TEMPLATE_EXTENSIONS template_name = template_path_to_name(path) #Remove template tracker.reset_template template_name #Remove any rendered versions, or partials rendered from it tracker.templates.delete_if do |_name, template| template.file == path or template.name.to_sym == template_name.to_sym end end def rescan_deleted_lib path deleted_lib = nil tracker.libs.delete_if do |_name, lib| if lib.files.include?(path) deleted_lib = lib true end end rescan_mixin deleted_lib if deleted_lib end def rescan_deleted_initializer path tracker.initializers.delete Pathname.new(path).basename.to_s end #Check controllers, templates, models and libs for data from file #and delete it. def remove_deleted_file path deleted = false [:controllers, :models, :libs].each do |collection| tracker.send(collection).delete_if do |_name, data| if data.files.include?(path) deleted = true true end end end tracker.templates.delete_if do |_name, data| if data.file == path deleted = true true end end deleted end #Guess at what kind of file the path contains def file_type path case path when /\/app\/controllers/ :controller when /\/app\/views/ :template when /\/app\/models/ :model when /\/lib/ :lib when /\/config\/initializers/ :initializer when /config\/routes\.rb/ :routes when /\/config\/.+\.(rb|yml)/ :config when /Gemfile|gems\./ :gemfile else :unknown end end def rescan_mixin lib method_names = [] lib.each_method do |name, _meth| method_names << name end to_rescan = [] #Rescan controllers that mixed in library tracker.controllers.each do |_name, controller| if controller.includes.include? lib.name controller.files.each do |path| unless @paths.include? path to_rescan << path end end end end to_rescan.each do |controller| tracker.reset_controller controller rescan_file controller end to_rescan = [] #Check if a method from this mixin was used to render a template. #This is not precise, because a different controller might have the #same method... tracker.templates.each do |name, template| next unless template.render_path if template.render_path.include_any_method? method_names name.to_s.match(/^([^.]+)/) original = tracker.templates[$1.to_sym] if original to_rescan << [name, original.file] end end end to_rescan.each do |template| tracker.reset_template template[0] rescan_file template[1] end end def parse_ruby_files list paths = list.select(&:exists?) file_parser = Brakeman::FileParser.new(tracker) file_parser.parse_files paths, :rescan file_parser.file_list[:rescan] end end #Class to make reporting of rescan results simpler to deal with class Brakeman::RescanReport include Brakeman::Util attr_reader :old_results, :new_results def initialize old_results, tracker @tracker = tracker @old_results = old_results @all_warnings = nil @diff = nil end #Returns true if any warnings were found (new or old) def any_warnings? not all_warnings.empty? end #Returns an array of all warnings found def all_warnings @all_warnings ||= @tracker.filtered_warnings end #Returns an array of warnings which were in the old report but are not in the #new report after rescanning def fixed_warnings diff[:fixed] end #Returns an array of warnings which were in the new report but were not in #the old report def new_warnings diff[:new] end #Returns true if there are any new or fixed warnings def warnings_changed? not (diff[:new].empty? and diff[:fixed].empty?) end #Returns a hash of arrays for :new and :fixed warnings def diff @diff ||= Brakeman::Differ.new(all_warnings, @old_results).diff end #Returns an array of warnings which were in the old report and the new report def existing_warnings @old ||= all_warnings.select do |w| not new_warnings.include? w end end #Output total, fixed, and new warnings def to_s(verbose = false) Brakeman.load_brakeman_dependency 'terminal-table' if !verbose <<-OUTPUT Total warnings: #{all_warnings.length} Fixed warnings: #{fixed_warnings.length} New warnings: #{new_warnings.length} OUTPUT else #Eventually move this to different method, or make default to_s out = "" {:fixed => fixed_warnings, :new => new_warnings, :existing => existing_warnings}.each do |warning_type, warnings| if warnings.length > 0 out << "#{warning_type.to_s.titleize} warnings: #{warnings.length}\n" table = Terminal::Table.new(:headings => ["Confidence", "Class", "Method", "Warning Type", "Message"]) do |t| warnings.sort_by { |w| w.confidence}.each do |warning| w = warning.to_row w["Confidence"] = Brakeman::Report::TEXT_CONFIDENCE[w["Confidence"]] t << [w["Confidence"], w["Class"], w["Method"], w["Warning Type"], w["Message"]] end end out << truncate_table(table.to_s) end end out end end end