lib/ui/web/server.rb in arachni-0.2.2.2 vs lib/ui/web/server.rb in arachni-0.2.3

- old
+ new

@@ -29,28 +29,29 @@ require Arachni::Options.instance.dir['lib'] + 'ui/cli/output' require Arachni::Options.instance.dir['lib'] + 'framework' require Arachni::Options.instance.dir['lib'] + 'rpc/xml/client/dispatcher' require Arachni::Options.instance.dir['lib'] + 'rpc/xml/client/instance' require Arachni::Options.instance.dir['lib'] + 'ui/web/report_manager' +require Arachni::Options.instance.dir['lib'] + 'ui/web/dispatcher_manager' require Arachni::Options.instance.dir['lib'] + 'ui/web/log' require Arachni::Options.instance.dir['lib'] + 'ui/web/output_stream' # # # Provides a web user interface for the Arachni Framework using Sinatra.<br/> # # @author: Tasos "Zapotek" Laskos # <tasos.laskos@gmail.com> # <zapotek@segfault.gr> -# @version: 0.1.1-pre +# @version: 0.1.3 # # @see Arachni::RPC::XML::Client::Instance # @see Arachni::RPC::XML::Client::Dispatcher # module Web - VERSION = '0.1.1-pre' + VERSION = '0.1.3' class Server < Sinatra::Base configure do use Rack::Flash @@ -65,10 +66,25 @@ end helpers do + def title + main = 'Arachni - Web Application Security Scanner Framework' + + sub = env['PATH_INFO'].split( '/' ).map { + |part| + normalize_section_name( part ) + }.reject { |part| part.empty? }.join( ' &rarr; ' ) + + return sub.empty? ? main : sub + ' :: ' + main + end + + def normalize_section_name( name ) + name.gsub( '_', ' ' ).capitalize + end + def report_count settings.reports.all.size end def plugin_has_required_file_option?( options ) @@ -129,11 +145,11 @@ @@plugins end def proc_mem( rss ) # we assume a page size of 4096 - (rss.to_i * 4096 / 1024 / 1024).to_s + 'MB' + rss.to_i * 4096 / 1024 / 1024 end def secs_to_hms( secs ) secs = secs.to_i return [secs/3600, secs/60 % 60, secs % 60].map { @@ -159,10 +175,11 @@ # HELPER_OWNER = "WebUI helper" set :log, Log.new( Arachni::Options.instance, settings ) set :reports, ReportManager.new( Arachni::Options.instance, settings ) + set :dispatchers, DispatcherManager.new( Arachni::Options.instance, settings ) enable :sessions configure do # shit's on! @@ -176,13 +193,13 @@ # erb :error, { :layout => true }, :error => 'Remote server has been shut down.' # end end def show( page, layout = true ) - if page == :dispatcher + if page == :dispatchers ensure_dispatcher - erb :dispatcher, { :layout => true }, :stats => dispatcher_stats + erb :dispatchers, { :layout => true }, :stats => dispatcher_stats else erb page.to_sym, { :layout => layout } end end @@ -204,54 +221,89 @@ # # Provides an easy way to connect to an instance by port # # @param [Integer] port # - def connect_to_instance( port ) + def connect_to_instance( url, token = nil ) prep_session + url = 'https://' + url if URI( url ).scheme != 'https' + + @@connections ||= {} + begin - return Arachni::RPC::XML::Client::Instance.new( options, port_to_url( port ) ) - rescue Exception - raise "Instance on port #{port} has shutdown." + if @@connections[url] && @@connections[url].alive? + return @@connections[url] + end + rescue end + + begin + + # + # Sync up the session authentication tokens with the ones in the + # class variables. + # + # This will allow users to still connect to instances even if they + # shutdown the WebUI or removed their cookies. + # + + @@tokens ||= {} + session['tokens'] ||= {} + @@tokens[url] = token if token + + session['tokens'].merge!( @@tokens ) + @@tokens.merge!( session['tokens'] ) + session['tokens'].merge!( @@tokens ) + + return @@connections[url] = + Arachni::RPC::XML::Client::Instance.new( options, url, session['tokens'][url] ) + rescue Exception => e + raise "Instance at #{url} has shutdown." + end end # # Converts a port to a URL instance. # # @param [Integer] port # - def port_to_url( port ) - uri = URI( session[:dispatcher_url] ) + def port_to_url( port, dispatcher_url, no_scheme = nil ) + uri = URI( dispatcher_url ) uri.port = port.to_i - uri.to_s + uri = uri.to_s + + uri = sanitize_url( uri ) if no_scheme + return uri end - # - # Provides easy access to the dispatcher and handles failure - # - def dispatcher - begin - @dispatcher ||= Arachni::RPC::XML::Client::Dispatcher.new( options, session[:dispatcher_url] ) - rescue Exception => e - redirect '/dispatcher_error' - end + def dispatchers + settings.dispatchers end # # Provides statistics about running jobs etc using the dispatcher # def dispatcher_stats - stats = dispatcher.stats - stats['running_jobs'].each { - |job| + + stats = {} + dispatchers.all.each { + |dispatcher| + begin - job['paused'] = connect_to_instance( job['port'] ).framework.paused? + stats[dispatcher['url']] = dispatchers.connect( dispatcher['url'] ).stats + stats[dispatcher['url']]['running_jobs'].each { + |job| + begin + job['paused'] = connect_to_instance( port_to_url( job['port'], dispatcher['url'] ) ).framework.paused? + rescue + end + } rescue end } + return stats end def options Arachni::Options.instance @@ -365,16 +417,21 @@ def helper_instance begin @@arachni ||= nil if !@@arachni - instance = dispatcher.dispatch( HELPER_OWNER ) - @@arachni = connect_to_instance( instance['port'] ) + + d_url = dispatchers.all.first['url'] + instance = dispatchers.connect( d_url ).dispatch( HELPER_OWNER ) + instance_url = port_to_url( instance['port'], d_url ) + + @@arachni = connect_to_instance( instance_url, instance['token'] ) end + return @@arachni rescue - redirect '/dispatcher/error' + redirect '/dispatchers/edit' end end def component_cache_filled? begin @@ -401,12 +458,10 @@ # # Makes sure that all systems are go and populates the session with default values # def prep_session - session[:dispatcher_url] ||= 'http://localhost:7331' - ensure_dispatcher session['opts'] ||= {} session['opts']['settings'] ||= { 'audit_links' => true, @@ -440,15 +495,11 @@ # an appropriate error page. # # @return [Bool] true if alive, redirect if not # def ensure_dispatcher - begin - dispatcher.alive? - rescue Exception => e - redirect '/dispatcher/error' - end + redirect '/dispatchers/edit' if dispatchers.all.empty? end # # Saves the report, shuts down the instance and returns the content as HTML # to be sent back to the user's browser. @@ -473,81 +524,101 @@ end # # Kills all running instances # - def shutdown_all - settings.log.dispatcher_global_shutdown( env ) - dispatcher.stats['running_jobs'].each { - |job| - begin - save_and_shutdown( connect_to_instance( job['port'] ) ) - rescue + def shutdown_all( url ) + settings.log.dispatcher_global_shutdown( env, url ) + + dispatcher_stats.each_pair { + |d_url, stats| + + next if sanitize_url( d_url.dup ) != url + + stats['running_jobs'].each { + |job| + + instance_url = port_to_url( job['port'], d_url ) begin - connect_to_instance( job['port'] ).service.shutdown! + save_and_shutdown( connect_to_instance( instance_url ) ) rescue - settings.log.instance_fucker_wont_die( env, port_to_url( job['port'] ) ) - next + begin + connect_to_instance( instance_url ).service.shutdown! + rescue + settings.log.instance_fucker_wont_die( env, instance_url ) + next + end end - end - settings.log.instance_shutdown( env, port_to_url( job['port'] ) ) + settings.log.instance_shutdown( env, instance_url ) + } } + end # # Kills all idle instances # # @return [Integer] the number of reaped instances # def shutdown_zombies i = 0 - dispatcher.stats['running_jobs'].each { - |job| - begin - arachni = connect_to_instance( job['port'] ) + dispatcher_stats.each_pair { + |url, stats| + stats['running_jobs'].each { + |job| + begin - if !arachni.framework.busy? && !job['owner'] != HELPER_OWNER - save_and_shutdown( arachni ) - settings.log.webui_zombie_cleanup( env, port_to_url( job['port'] ) ) - i+=1 + instance_url = port_to_url( job['port'], url ) + arachni = connect_to_instance( instance_url ) + + begin + if !arachni.framework.busy? && !job['owner'] != HELPER_OWNER + save_and_shutdown( arachni ) + settings.log.webui_zombie_cleanup( env, instance_url ) + i+=1 + end + rescue end - rescue + rescue end - - rescue - end + } } return i end + def sanitize_url( url ) + url.gsub!( 'http://', '' ) + escape( url.gsub( 'https://', '' ) ) + end + get "/" do prep_session ensure_welcomed show :home end get "/welcome" do erb :welcome, { :layout => true } end - get "/dispatcher" do - show :dispatcher + get "/dispatchers" do + show :dispatchers end # # sets the dispatcher URL # - post "/dispatcher" do + post "/dispatchers" do if !params['url'] || params['url'].empty? flash[:err] = "URL cannot be empty." - show :dispatcher_error + show :dispatchers_edit else session[:dispatcher_url] = params['url'] settings.log.dispatcher_selected( env, params['url'] ) begin @@ -555,26 +626,48 @@ settings.log.dispatcher_verified( env, params['url'] ) redirect '/' rescue settings.log.dispatcher_error( env, params['url'] ) flash[:err] = "Couldn't find a dispatcher at \"#{escape( params['url'] )}\"." - show :dispatcher_error + show :dispatchers_edit end end end + get '/dispatchers/edit' do + show :dispatchers_edit + end + + post '/dispatchers/add' do + + begin + dispatchers.alive?( params[:url] ) + dispatchers.new( params ) + rescue + flash[:err] = "Couldn't find a dispatcher at \"#{escape( params['url'] )}\"." + end + + show :dispatchers_edit + end + + post '/dispatchers/:url/delete' do + dispatchers.delete( params[:url] ) + show :dispatchers_edit + end + + # # shuts down all instances # - post "/dispatcher/shutdown" do - shutdown_all - redirect '/dispatcher' + post "/dispatchers/:url/shutdown" do + shutdown_all( params[:url] ) + redirect '/dispatchers' end - get '/dispatcher/error' do - show :dispatcher_error + get '/dispatchers/error' do + show :dispatchers_edit end # # starts a scan # @@ -593,15 +686,17 @@ elsif !valid flash[:err] = "Invalid URL." show :home else - instance = dispatcher.dispatch( params['url'] ) - settings.log.instance_dispatched( env, port_to_url( instance['port'] ) ) + instance = dispatchers.connect( params['dispatcher'] ).dispatch( params['url'] ) + instance_url = port_to_url( instance['port'], params['dispatcher'] ) + + settings.log.instance_dispatched( env, instance_url ) settings.log.instance_owner_assigned( env, params['url'] ) - arachni = connect_to_instance( instance['port'] ) + arachni = connect_to_instance( instance_url, instance['token'] ) session['opts']['settings']['url'] = params['url'] unescape_hash( session['opts'] ) @@ -616,11 +711,11 @@ arachni.plugins.load( YAML::load( session['opts']['plugins'] ) ) arachni.framework.run settings.log.scan_started( env, params['url'] ) - redirect '/instance/' + instance['port'].to_s + redirect '/instance/' + instance_url.to_s.gsub( 'https://', '' ) end end get "/modules" do @@ -675,117 +770,111 @@ flash.now[:ok] = "Settings updated." show :settings, true end - get "/instance/:port" do + get "/instance/:url" do begin - arachni = connect_to_instance( params[:port] ) + arachni = connect_to_instance( params[:url] ) erb :instance, { :layout => true }, :paused => arachni.framework.paused?, :shutdown => false - rescue - flash.now[:notice] = "Instance on port #{params[:port]} has been shutdown." + rescue Exception => e + flash.now[:notice] = "Instance at #{params[:url]} has been shutdown." erb :instance, { :layout => true }, :shutdown => true, :stats => dispatcher_stats end end - get "/instance/:port/output.json" do + get "/instance/:url/output.json" do content_type :json begin - arachni = connect_to_instance( params[:port] ) + arachni = connect_to_instance( params[:url] ) if arachni.framework.busy? { 'data' => OutputStream.new( arachni, 38 ).data }.to_json else - settings.log.instance_shutdown( env, port_to_url( params[:port] ) ) + settings.log.instance_shutdown( env, params[:url] ) save_and_shutdown( arachni ) - - # { - # 'report' => '"data:text/html;base64, ' + - # Base64.encode64( save_shutdown_and_show( arachni ) ) + '"' - # }.to_json - { 'status' => 'finished', 'data' => "The server has been shut down." }.to_json end rescue { 'status' => 'finished', 'data' => "The server has been shut down." }.to_json end end - get "/instance/:port/output_results.json" do + get "/instance/:url/output_results.json" do content_type :json begin - arachni = connect_to_instance( params[:port] ) + arachni = connect_to_instance( params[:url] ) if !arachni.framework.paused? && arachni.framework.busy? out = erb( :output_results, { :layout => false }, :issues => YAML.load( arachni.framework.auditstore ).issues) { 'data' => out }.to_json end rescue { 'data' => "The server has been shut down." }.to_json end end - get "/instance/:port/stats.json" do + get "/instance/:url/stats.json" do content_type :json begin - arachni = connect_to_instance( params[:port] ) + arachni = connect_to_instance( params[:url] ) stats = arachni.framework.stats stats['current_page'] = escape( stats['current_page'] ) { 'refresh' => true, 'stats' => stats }.to_json rescue { 'refresh' => false }.to_json end end - post "/*/:port/pause" do - arachni = connect_to_instance( params[:port] ) + post "/*/:url/pause" do + arachni = connect_to_instance( params[:url] ) begin arachni.framework.pause! - settings.log.instance_paused( env, port_to_url( params[:port] ) ) + settings.log.instance_paused( env, params[:url] ) - flash.now[:notice] = "Instance on port #{params[:port]} will pause as soon as the current page is audited." + flash.now[:notice] = "Instance at #{params[:url]} will pause as soon as the current page is audited." erb params[:splat][0].to_sym, { :layout => true }, :paused => arachni.framework.paused?, :shutdown => false, :stats => dispatcher_stats rescue - flash.now[:notice] = "Instance on port #{params[:port]} has been shutdown." + flash.now[:notice] = "Instance at #{params[:url]} has been shutdown." erb params[:splat][0].to_sym, { :layout => true }, :shutdown => true, :stats => dispatcher_stats end end - post "/*/:port/resume" do - arachni = connect_to_instance( params[:port] ) + post "/*/:url/resume" do + arachni = connect_to_instance( params[:url] ) begin arachni.framework.resume! - settings.log.instance_resumed( env, port_to_url( params[:port] ) ) + settings.log.instance_resumed( env, params[:url] ) - flash.now[:notice] = "Instance on port #{params[:port]} resumes." + flash.now[:notice] = "Instance at #{params[:url]} resumes." erb params[:splat][0].to_sym, { :layout => true }, :paused => arachni.framework.paused?, :shutdown => false, :stats => dispatcher_stats rescue - flash.now[:notice] = "Instance on port #{params[:port]} has been shutdown." + flash.now[:notice] = "Instance at #{params[:url]} has been shutdown." erb params[:splat][0].to_sym, { :layout => true }, :shutdown => true, :stats => dispatcher_stats end end - post "/*/:port/shutdown" do - arachni = connect_to_instance( params[:port] ) + post "/*/:url/shutdown" do + arachni = connect_to_instance( params[:url] ) begin arachni.framework.busy? - settings.log.instance_shutdown( env, port_to_url( params[:port] ) ) + settings.log.instance_shutdown( env, params[:url] ) begin save_shutdown_and_show( arachni ) rescue - flash.now[:notice] = "Instance on port #{params[:port]} has been shutdown." + flash.now[:notice] = "Instance at #{params[:url]} has been shutdown." show params[:splat][0].to_sym ensure arachni.service.shutdown! end rescue - flash.now[:notice] = "Instance on port #{params[:port]} has already been shutdown." + flash.now[:notice] = "Instance at #{params[:url]} has already been shutdown." erb params[:splat][0].to_sym, { :layout => true }, :shutdown => true, :stats => dispatcher_stats end end get "/reports" do