require 'net/http' require 'json' require 'socket' require 'fileutils' # Statistics stuff # require 'code_metrics/statistics' module Dawn module Engine attr_reader :target attr_reader :name attr_reader :scan_start attr_reader :scan_stop # This attribute is used when @name == "Gemfile.lock" to force the # loading of specific MVC checks attr_reader :force attr_reader :gemfile_lock attr_reader :mvc_version attr_reader :connected_gems attr_reader :checks attr_reader :vulnerabilities attr_reader :mitigated_issues attr_reader :ruby_version attr_reader :engine_error attr_reader :stats attr_reader :reflected_xss # Typical MVC elements here # Each view will be something like {:filename=>"target/views/index.haml", :language=>:haml} attr_reader :views # Each controller will be a little bit more complex. Of course for # Sinatra, the controller filename will be the sole web application ruby # file. # {:filename=>"target/controllers/this_controller.rb", :actions=>[{:name=>"index", :method=>:get, :map=>"/"]} attr_reader :controllers # Models I don't know right now. Let them initialized as Array... we # will see later attr_reader :models attr_accessor :debug attr_reader :applied_checks attr_reader :skipped_checks attr_reader :output_dir_name def initialize(dir=nil, name="", options={}) @name = name @scan_start = Time.now @scan_stop = @scan_start @mvc_version = "" @gemfile_lock = "" @force = "" @connected_gems = [] @checks = [] @vulnerabilities = [] @mitigated_issues = [] @applied = [] @reflected_xss = [] @engine_error = false @applied_checks = 0 @skipped_checks = 0 @gemfile_lock_sudo = false set_target(dir) unless dir.nil? @ruby_version = get_ruby_version if dir.nil? @gemfile_lock = options[:gemfile_name] unless options[:gemfile_name].nil? # @stats = gather_statistics @views = detect_views @controllers = detect_controllers @models = detect_models @output_dir_name = output_dir if $logger.nil? require 'logger' $logger = Logger.new(STDOUT) $logger.helo "dawn-engine", Dawn::VERSION end $logger.warn "pattern matching security checks are disabled for Gemfile.lock scan" if @name == "Gemfile.lock" $logger.warn "combo security checks are disabled for Gemfile.lock scan" if @name == "Gemfile.lock" debug_me "engine is in debug mode" if @name == "Gemfile.lock" && ! options[:guessed_mvc].nil? # since all checks relies on @name a Gemfile.lock engine must # impersonificate the engine for the mvc it was detected debug_me "now I'm switching my name from #{@name} to #{options[:guessed_mvc][:name]}" $logger.err "there are no connected gems... it seems Gemfile.lock parsing failed" if options[:guessed_mvc][:connected_gems].empty? @name = options[:guessed_mvc][:name] @mvc_version = options[:guessed_mvc][:version] @connected_gems = options[:guessed_mvc][:connected_gems] @gemfile_lock_sudo = true end # FIXME.20140325 # # I comment out this call. knowledge base must be called explicitly # since it's now possible to pass an array saying check families to be # loaded. # # load_knowledge_base end def detect_views [] end def error! @error = true end def error? @error end def build_view_array(dir) return [] unless File.exist?(dir) and File.directory?(dir) ret = [] Dir.glob(File.join("#{dir}", "*")).each do |filename| ret << {:filename=>filename, :language=>:haml} if File.extname(filename) == ".haml" end ret end def detect_controllers [] end def detect_models [] end def get_ruby_version unless @target.nil? # does target use rbenv? ver = get_rbenv_ruby_ver # does the target use rvm? ver = get_rvm_ruby_ver if ver[:version].empty? && ver[:patchlevel].empty? # take the running ruby otherwise ver = {:engine=>RUBY_ENGINE, :version=>RUBY_VERSION, :patchlevel=>"p#{RUBY_PATCHLEVEL}"} if ver[:version].empty? && ver[:patchlevel].empty? else ver = {:engine=>RUBY_ENGINE, :version=>RUBY_VERSION, :patchlevel=>"p#{RUBY_PATCHLEVEL}"} end ver end def set_target(dir) @target = dir @gemfile_lock = File.join(@target, "Gemfile.lock") @mvc_version = set_mvc_version @ruby_version = get_ruby_version end def target_is_dir? File.directory?(@target) end def load_knowledge_base(enabled_checks=[]) debug_me("load_knowledge_base called. Enabled checks are: #{enabled_checks}") Dawn::KnowledgeBase.enabled_checks=[:bulletin, :generic_check] kb = Dawn::KnowledgeBase.instance @checks=kb.load debug_me("#{@checks.count} checks loaded") @checks end def set_mvc_version ver = "" return ver unless target_is_dir? return ver unless has_gemfile_lock? my_dir = Dir.pwd Dir.chdir(@target) lockfile = Bundler::LockfileParser.new(Bundler.read_file("Gemfile.lock")) lockfile.specs.each do |s| # detecting MVC version using @name in case of sinatra, padrino or rails engine ver= s.version.to_s if s.name == @name && @name != "Gemfile.lock" # detecting MVC version using @force in case of Gemfile.lock engine ver= s.version.to_s if s.name == @force.to_s && @name == "Gemfile.lock" @connected_gems << {:name=>s.name, :version=>s.version.to_s} end Dir.chdir(my_dir) return ver end def has_gemfile_lock? File.exist?(@gemfile_lock) end def is_good_mvc? (@mvc_version != "") end def can_apply? target_is_dir? && is_good_mvc? end def get_mvc_version "#{@mvc_version}" if is_good_mvc? end ######################################################################## ## Output stuff - START ######################################################################## # Creates the directory name where dawnscanner results will be saved # # Examples # engine.create_output_dir # # => /Users/thesp0nge/dawnscanner/results/railsgoat/20151123 # # => /Users/thesp0nge/dawnscanner/results/railsgoat/20151123_1 (if # previous directory name exists) def output_dir @output_dir_name = File.join(Dir.home, 'dawnscanner', 'results', File.basename(@target), Time.now.strftime('%Y%m%d')) if Dir.exist?(@output_dir_name) i=1 while (Dir.exist?(@output_dir_name)) do @output_dir_name = File.join(Dir.home, 'dawnscanner', 'results', File.basename(@target), "#{Time.now.strftime('%Y%m%d')}_#{i}") i+=1 end end @output_dir_name end # Creates the directory def create_output_dir FileUtils.mkdir_p(@output_dir_name) @output_dir_name end ######################################################################## ## Output stuff - END ######################################################################## ## Security stuff applies here # # Public it applies a single security check given by its name # # name - the security check to be applied # # Examples # # engine.apply("CVE-2013-1800") # # => boolean # # Returns a true value if the security check was successfully applied or false # otherwise def apply(name) # FIXME.20140325 # Now if no checks are loaded because knowledge base was not previously called, apply and apply_all proudly refuse to run. # Reason is simple, load_knowledge_base now needs enabled check array # and I don't want to pollute engine API to propagate this value. It's # a param to load_knowledge_base and then bin/dawn calls it # accordingly. # load_knowledge_base if @checks.nil? if @checks.nil? $logger.err "you must load knowledge base before trying to apply security checks" return false end return false if @checks.empty? @checks.each do |check| _do_apply(check) if check.name == name end false end def apply_all(checks_to_be_skipped=[]) @scan_start = Time.now debug_me("I'm asked to skip those checks #{checks_to_be_skipped}") debug_me("SCAN STARTED: #{@scan_start}") if @checks.nil? $logger.error "you must load knowledge base before trying to apply security checks" @scan_stop = Time.now debug_me("SCAN STOPPED: #{@scan_stop}") return false end if @checks.empty? $logger.warn "no security checks found. This is strange" @scan_stop = Time.now debug_me("SCAN STOPPED: #{@scan_stop}") return false end @checks.each do |check| if checks_to_be_skipped.include?(check.name) $logger.info("skipping security check #{check.name}") else _do_apply(check) end end @scan_stop = Time.now debug_me("SCAN STOPPED: #{@scan_stop}") true end def scan_time @scan_stop - @scan_start end def is_applied?(name) @applied.each do |a| return true if a[:name] == name end return false end def vulnerabilities apply_all if @applied.empty? @vulnerabilities end def find_vulnerability_by_name(name) apply(name) unless is_applied?(name) @vulnerabilities.each do |v| return v if v[:name] == name end nil end def is_vulnerable_to?(name) return (find_vulnerability_by_name(name) != nil) end def has_reflected_xss? (@reflected_xss.count != 0) unless @reflected_xss.nil? end def count_vulnerabilities ret = 0 ret = @vulnerabilities.count unless @vulnerabilities.nil? ret += @reflected_xss.count unless @reflected_xss.nil? ret end private def get_rbenv_ruby_ver return {:version=>"", :patchlevel=>""} unless File.exist?(File.join(@target, ".rbenv-version")) hash = File.read(File.join(@target, '.rbenv-version')).split('-') return {:version=>hash[0], :patchlevel=>hash[1]} end def get_rvm_ruby_ver return {:version=>"", :patchlevel=>""} unless File.exist?(File.join(@target, ".ruby-version")) hash = File.read(File.join(@target, '.ruby-version')).split('-') return {:version=>hash[0].chop, :patchlevel=>hash[1]} end def _do_apply(check) unless ((check.kind == Dawn::KnowledgeBase::PATTERN_MATCH_CHECK || check.kind == Dawn::KnowledgeBase::COMBO_CHECK ) && @gemfile_lock_sudo) @applied << { :name => name } debug_me "applying check #{check.name} - #{check.kind}" @applied_checks += 1 check.ruby_version = @ruby_version[:version] check.detected_ruby = @ruby_version if check.kind == Dawn::KnowledgeBase::RUBY_VERSION_CHECK check.dependencies = self.connected_gems if check.kind == Dawn::KnowledgeBase::DEPENDENCY_CHECK or check.kind == Dawn::KnowledgeBase::UNSAFE_DEPENDENCY_CHECK check.root_dir = self.target if check.kind == Dawn::KnowledgeBase::PATTERN_MATCH_CHECK check.options = {:detected_ruby => self.ruby_version, :dependencies => self.connected_gems, :root_dir => self.target } if check.kind == Dawn::KnowledgeBase::COMBO_CHECK check_vuln = check.vuln? if check_vuln vc = nil vc = check.vulnerable_checks if check.kind == Dawn::KnowledgeBase::COMBO_CHECK @vulnerabilities << {:name=> check.name || "CVE-XXXX-YYYY", :severity=>check.severity || "Unknown severity", :priority=>check.priority || "Unknown priority", :kind=>check.check_family || "Unknown kind", :message=>check.message || "", :remediation=>check.remediation || "", :evidences=>check.evidences || [], :cve_link=>check.cve_link || "No link", :cvss_score=>check.cvss_score || "No score", :vulnerable_checks=>vc} end @mitigated_issues << {:name=> check.name, :severity=>check.severity, :priority=>check.priority, :kind=>check.check_family, :message=>check.message, :remediation=>check.remediation, :evidences=>check.evidences, :vulnerable_checks=>nil} if check.mitigated? else debug_me "skipping check #{check.name}" @skipped_checks += 1 end true end # def gather_statistics # dirs = CodeMetrics::StatsDirectories.new # puts target # dirs.add_directories("#{target}/**/*.rb", "#{target}") # puts CodeMetrics::Statistics.new(*dirs).to_s # end end end