=begin Copyright 2010-2022 Ecsypno This file is part of the Arachni Framework project and is subject to redistribution and commercial restrictions. Please see the Arachni Framework web site for more information on licensing and terms of use. =end module Arachni class Framework module Parts # Provides access to {Arachni::State::Framework} and helpers. # # @author Tasos "Zapotek" Laskos module State def self.included( base ) base.extend ClassMethods end module ClassMethods # @param [String] afs # Path to an `.afs.` (Arachni Framework Snapshot) file created by # {#suspend}. # # @return [Framework] # Restored instance. def restore( afs, &block ) framework = new framework.restore( afs ) if block_given? begin block.call framework ensure framework.clean_up framework.reset end end framework end # @note You should first reset {Arachni::Options}. # # Resets everything and allows the framework environment to be re-used. def reset Arachni::State.clear Arachni::Data.clear Arachni::Platform::Manager.reset Arachni::Check::Auditor.reset ElementFilter.reset Element::Capabilities::Auditable.reset Element::Capabilities::Analyzable.reset Arachni::Check::Manager.reset Arachni::Plugin::Manager.reset Arachni::Reporter::Manager.reset HTTP::Client.reset end end def initialize super Element::Capabilities::Auditable.skip_like do |element| if pause? print_debug "Blocking on element audit: #{element.audit_id}" end wait_if_paused end state.status = :ready end # @return [String] # Provisioned {#suspend} dump file for this instance. def snapshot_path return @state_archive if @state_archive default_filename = "#{URI(options.url).host} #{Time.now.to_s.gsub( ':', '_' )} " << "#{generate_token}.afs" location = options.snapshot.save_path if !location location = default_filename elsif File.directory? location location += "/#{default_filename}" end @state_archive ||= File.expand_path( location ) end # Cleans up the framework; should be called after running the audit or # after canceling a running scan. # # It stops the clock and waits for the plugins to finish up. def clean_up( shutdown_browsers = true ) return if @cleaned_up @cleaned_up = true state.force_resume state.status = :cleanup if shutdown_browsers state.set_status_message :browser_cluster_shutdown shutdown_browser_cluster end state.set_status_message :clearing_queues page_queue.clear url_queue.clear @finish_datetime = Time.now @start_datetime ||= Time.now # Make sure this is disabled or it'll break reporter output. disable_only_positives state.running = false state.set_status_message :waiting_for_plugins @plugins.block # Plugins may need the session right till the very end so save it for last. @session.clean_up @session = nil true end # @private def reset_trainer @trainer = Trainer.new( self ) end # @note Prefer this from {.reset} if you already have an instance. # @note You should first reset {Arachni::Options}. # # Resets everything and allows the framework to be re-used. def reset @cleaned_up = false @browser_job = nil @failures.clear @retries.clear # This needs to happen before resetting the other components so they # will be able to put in their hooks. self.class.reset clear_observers reset_trainer reset_session @checks.clear @reporters.clear @plugins.clear end # @return [State::Framework] def state Arachni::State.framework end # @param [String] afs # Path to an `.afs.` (Arachni Framework Snapshot) file created by {#suspend}. # # @return [Framework] # Restored instance. def restore( afs ) Snapshot.load afs browser_job_update_skip_states state.browser_skip_states checks.load Options.checks plugins.load Options.plugins.keys nil end # @return [Array] # Messages providing more information about the current {#status} of # the framework. def status_messages state.status_messages end # @return [Symbol] # Status of the instance, possible values are (in order): # # * `:ready` -- {#initialize Initialised} and waiting for instructions. # * `:preparing` -- Getting ready to start (i.e. initializing plugins etc.). # * `:scanning` -- The instance is currently {#run auditing} the webapp. # * `:pausing` -- The instance is being {#pause paused} (if applicable). # * `:paused` -- The instance has been {#pause paused} (if applicable). # * `:suspending` -- The instance is being {#suspend suspended} (if applicable). # * `:suspended` -- The instance has being {#suspend suspended} (if applicable). # * `:cleanup` -- The scan has completed and the instance is # {Framework::Parts::State#clean_up cleaning up} after itself (i.e. waiting for # plugins to finish etc.). # * `:aborted` -- The scan has been {Framework::Parts::State#abort}, you can grab the # report and shutdown. # * `:done` -- The scan has completed, you can grab the report and shutdown. def status state.status end # @return [Bool] # `true` if the framework is running, `false` otherwise. This is `true` # even if the scan is {#paused?}. def running? state.running? end # @return [Bool] # `true` if the system is scanning, `false` otherwise. def scanning? state.scanning? end # @return [Bool] # `true` if the framework is paused, `false` otherwise. def paused? state.paused? end # @return [Bool] # `true` if the framework has been instructed to pause (i.e. is in the # process of being paused or has been paused), `false` otherwise. def pause? state.pause? end # @return [Bool] # `true` if the framework is in the process of pausing, `false` otherwise. def pausing? state.pausing? end # @return (see Arachni::State::Framework#done?) def done? state.done? end # @note Each call from a unique caller is counted as a pause request # and in order for the system to resume **all** pause callers need to # {#resume} it. # # Pauses the framework on a best effort basis. # # @param [Bool] wait # Wait until the system has been paused. # # @return [Integer] # ID identifying this pause request. def pause( wait = true ) id = generate_token.hash state.pause id, wait id end # @return [Bool] # `true` if the framework {#run} has been aborted, `false` otherwise. def aborted? state.aborted? end # @return [Bool] # `true` if the framework has been instructed to abort (i.e. is in the # process of being aborted or has been aborted), `false` otherwise. def abort? state.abort? end # @return [Bool] # `true` if the framework is in the process of aborting, `false` otherwise. def aborting? state.aborting? end # Aborts the framework {#run} on a best effort basis. # # @param [Bool] wait # Wait until the system has been aborted. def abort( wait = true ) state.abort wait end # @note Each call from a unique caller is counted as a pause request # and in order for the system to resume **all** pause callers need to # {#resume} it. # # Removes a {#pause} request for the current caller. # # @param [Integer] id # ID of the {#pause} request. def resume( id ) state.resume id end # Writes a {Snapshot.dump} to disk and aborts the scan. # # @param [Bool] wait # Wait for the system to write it state to disk. # # @return [String,nil] # Path to the state file `wait` is `true`, `nil` otherwise. def suspend( wait = true ) state.suspend( wait ) return snapshot_path if wait nil end # @return [Bool] # `true` if the system is in the process of being suspended, `false` # otherwise. def suspend? state.suspend? end # @return [Bool] # `true` if the system has been suspended, `false` otherwise. def suspended? state.suspended? end private # @note Must be called before calling any audit methods. # # Prepares the framework for the audit. # # * Sets the status to `:preparing`. # * Starts the clock. # * Runs the plugins. def prepare state.status = :preparing state.running = true @start_datetime = Time.now Snapshot.restored? ? @plugins.restore : @plugins.run end def reset_session @session.clean_up if @session @session = Session.new end # Small but (sometimes) important optimization: # # Keep track of page elements which have already been passed to checks, # in order to filter them out and hopefully even avoid running checks # against pages with no new elements. # # It's not like there were going to be redundant audits anyways, because # each layer of the audit performs its own redundancy checks, but those # redundancy checks can introduce significant latencies when dealing # with pages with lots of elements. def pre_audit_element_filter( page ) unique_elements = {} page.elements.each do |e| next if !Options.audit.element?( e.type ) next if e.is_a?( Cookie ) || e.is_a?( Header ) new_element = false unique_elements[e.type] ||= [] if !state.element_checked?( e ) state.element_checked e new_element = true end if page.dom.depth > 0 && e.respond_to?( :dom ) && e.dom if !state.element_checked?( e.dom ) state.element_checked e.dom new_element = true end end next if !new_element unique_elements[e.type] << e end # Remove redundant elements from the page cache, if there are thousands # of them then just skipping them during the audit will introduce latency. unique_elements.each do |type, elements| page.send( "#{type}s=", elements ) end page end def handle_signals wait_if_paused abort_if_signaled suspend_if_signaled end def wait_if_paused state.paused if pause? sleep 0.2 while pause? && !abort? end def abort_if_signaled return if !abort? clean_up state.aborted end def suspend_if_signaled return if !suspend? suspend_to_disk end def suspend_to_disk while wait_for_browser_cluster? last_pending_jobs ||= 0 pending_jobs = browser_cluster.pending_job_counter if pending_jobs != last_pending_jobs state.set_status_message :waiting_for_browser_cluster_jobs, pending_jobs print_info "Suspending: #{status_messages.first}" end last_pending_jobs = pending_jobs sleep 0.1 end # Make sure the component options are up to date with what's actually # happening. options.checks = checks.loaded options.plugins = plugins.loaded. inject({}) { |h, name| h[name.to_s] = Options.plugins[name.to_s] || {}; h } if browser_cluster_job_skip_states state.browser_skip_states.merge browser_cluster_job_skip_states end state.set_status_message :suspending_plugins @plugins.suspend state.set_status_message :saving_snapshot, snapshot_path Snapshot.dump( snapshot_path ) state.clear_status_messages clean_up state.set_status_message :snapshot_location, snapshot_path print_info status_messages.first state.suspended end end end end end