#!/usr/bin/env ruby require 'getoptlong' require 'json' require 'codesake-commons' require 'codesake-dawn' APPNAME = File.basename($0) LIST_KNOWN_FRAMEWORK = %w(rails sinatra padrino) VALID_OUTPUT_FORMAT = %w(console json csv html) $logger = Codesake::Commons::Logging.instance $logger.helo APPNAME, Codesake::Dawn::VERSION opts = GetoptLong.new( [ '--rails', '-r', GetoptLong::NO_ARGUMENT], [ '--sinatra', '-s', GetoptLong::NO_ARGUMENT], [ '--padrino', '-p', GetoptLong::NO_ARGUMENT], [ '--gem-lock', '-G', GetoptLong::OPTIONAL_ARGUMENT], [ '--list-known-framework', '-f', GetoptLong::NO_ARGUMENT], [ '--list-knowledgebase', '-k', GetoptLong::OPTIONAL_ARGUMENT], [ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT], [ '--verbose', '-V', GetoptLong::NO_ARGUMENT], [ '--debug', '-D', GetoptLong::NO_ARGUMENT], [ '--count-only', '-C', GetoptLong::NO_ARGUMENT], [ '--exit-on-warn', '-z', GetoptLong::NO_ARGUMENT], [ '--version', '-v', GetoptLong::NO_ARGUMENT], [ '--help', '-h', GetoptLong::NO_ARGUMENT] ) engine = nil options = {:verbose=>false, :output=>"console", :count_only=>false, :dump_kb=>false, :mvc=>"", :gemfile_scan=>false, :gemfile_name=>"", :debug=>false, :exit_on_warn => false} trap("INT") { $logger.die('[INTERRUPTED]') } check = "" guess = {:name=>"", :version=>"", :connected_gems=>[]} opts.each do |opt, val| case opt when '--version' puts "#{Codesake::Dawn::VERSION} [#{Codesake::Dawn::CODENAME}]" Kernel.exit(0) when '--rails' options[:mvc]=:rails when '--sinatra' options[:mvc]=:sinatra when '--padrino' options[:mvc]=:padrino when '--gem-lock' options[:gemfile_scan] = true options[:gemfile_name] = val unless val.nil? guess = Codesake::Dawn::Core.guess_mvc(val) $logger.log "Guessed MVC: #{guess[:name]} v#{guess[:version]}" when '--verbose' options[:verbose]=true when '--output' options[:output] = val unless VALID_OUTPUT_FORMAT.find_index(val).nil? when '--count-only' options[:count_only] = true when '--debug' options[:debug] = true when '--exit-on-warn' options[:exit_on_warn] = true when '--list-knowledgebase' options[:dump_kb]=true check = val unless val.nil? when '--list-known-framework' puts "Ruby MVC framework supported by #{APPNAME}:" LIST_KNOWN_FRAMEWORK.each do |mvc| puts "* #{mvc}" end Kernel.exit(0) when '--help' Kernel.exit(Codesake::Dawn::Core.help) end end if options[:dump_kb] puts Codesake::Dawn::Core.dump_knowledge_base(options[:verbose]) if check.empty? if ! check.empty? found = Codesake::Dawn::KnowledgeBase.find(nil, check) puts "#{check} found in knowledgebase." if found puts "#{check} not found in knowledgebase" if ! found end Kernel.exit(0) end target=ARGV.shift $logger.die("missing target") if target.nil? && options[:gemfile_name].nil? $logger.die("invalid directory (#{target})") if options[:gemfile_name].nil? &&! Codesake::Dawn::Core.is_good_target?(target) $logger.die("if scanning Gemfile.lock file you must not force target MVC using one from -r, -s or -p flag") if ! options[:mvc].empty? && options[:gemfile_scan] ## MVC auto detect. # Skipping MVC autodetect if it's already been done by guess_mvc when choosing Gemfile.lock scan unless options[:gemfile_scan] begin engine = Codesake::Dawn::Core.detect_mvc(target) if options[:mvc].empty? rescue ArgumentError => e $logger.die(e.message) end end engine = Codesake::Dawn::Rails.new(target) if options[:mvc] == :rails && options[:gemfile_scan].nil? engine = Codesake::Dawn::Sinatra.new(target) if options[:mvc] == :sinatra && options[:gemfile_scan].nil? engine = Codesake::Dawn::Padrino.new(target) if options[:mvc] == :padrino && options[:gemfile_scan].nil? engine = Codesake::Dawn::GemfileLock.new(target, options[:gemfile_name], options[:debug], guess) if options[:gemfile_scan] $logger.die("ruby framework auto detect failed. Please force if rails, sinatra or padrino with -r, -s or -p flags") if engine.nil? if options[:exit_on_warn] Kernel.at_exit do if engine.count_vulnerabilities != 0 Kernel.exit(engine.count_vulnerabilities) end end end if options[:count_only] ret = Codesake::Dawn::Core.dry_run(target, engine) puts (ret)? engine.vulnerabilities.count : "-1" unless options[:output] == "json" puts (ret)? {:status=>"OK", :vulnerabilities_count=>engine.count_vulnerabilities}.to_json : {:status=>"KO", :vulnerabilities_count=>-1}.to_json Kernel.exit(0) end if options[:output] == "json" puts Codesake::Dawn::Core.output_json_run(target, engine) Kernel.exit(0) end $logger.die "missing target framework option" if engine.nil? engine.load_knowledge_base $logger.die "nothing to do on #{target}" if ! options[:gemfile_scan] && ! engine.can_apply? $logger.log "scanning #{target}" $logger.log "#{engine.name} v#{engine.get_mvc_version} detected" unless engine.name == "Gemfile.lock" $logger.log "#{engine.force} v#{engine.get_mvc_version} detected" if engine.name == "Gemfile.lock" $logger.log "applying all security checks" if engine.apply_all $logger.log "#{engine.applied_checks} security checks applied - #{engine.skipped_checks} security checks skipped" else $logger.err "no security checks in the knowledge base" end if engine.count_vulnerabilities != 0 $logger.log "#{engine.count_vulnerabilities} vulnerabilities found" engine.vulnerabilities.each do |vuln| $logger.log "#{vuln[:name]} failed" $logger.log "Description: #{vuln[:message]}" $logger.log "Solution: #{vuln[:remediation]}" $logger.err "Evidence:" vuln[:evidences].each do |evidence| $logger.err evidence end end if engine.has_reflected_xss? $logger.log "#{engine.reflected_xss.count} reflected XSS found" engine.reflected_xss.each do |vuln| $logger.log "request parameter \"#{vuln[:sink_source]}\" is used without escaping in #{vuln[:sink_view]}. It was read here: #{vuln[:sink_file]}@#{vuln[:sink_line]}" $logger.err "evidence: #{vuln[:sink_evidence]}" end end else $logger.ok "no vulnerabilities found." end if engine.mitigated_issues.count != 0 $logger.log "#{engine.mitigated_issues.count} mitigated vulnerabilities found" engine.mitigated_issues.each do |vuln| $logger.ok "#{vuln[:name]} mitigated" vuln[:evidences].each do |evidence| $logger.err evidence end end end $logger.bye