From 02e7707e5c16fce04196e003fa3b16c5357bd710 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 27 Mar 2019 13:05:40 +0000 Subject: [PATCH] Fixes #79 --- lib/cms_scanner.rb | 84 +------------------------------- lib/cms_scanner/scan.rb | 86 +++++++++++++++++++++++++++++++++ spec/lib/cms_scanner_spec.rb | 91 ----------------------------------- spec/lib/scan_spec.rb | 92 ++++++++++++++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 174 deletions(-) create mode 100644 lib/cms_scanner/scan.rb create mode 100644 spec/lib/scan_spec.rb diff --git a/lib/cms_scanner.rb b/lib/cms_scanner.rb index 4f1d1d47..216a27b5 100644 --- a/lib/cms_scanner.rb +++ b/lib/cms_scanner.rb @@ -23,6 +23,7 @@ require 'cms_scanner/public_suffix/domain' # Adds a Domain#match method and logic, used in scope stuff require 'cms_scanner/numeric' # Adds a Numeric#bytes_to_human # Custom Libs +require 'cms_scanner/scan' require 'cms_scanner/helper' require 'cms_scanner/exit_code' require 'cms_scanner/errors' @@ -122,89 +123,6 @@ def self.included(base) base.extend(ClassMethods) super(base) end - - # Scan - class Scan - attr_reader :run_error - - def initialize - controllers << NS::Controller::Core.new - - exit_hook - - yield self if block_given? - end - - # @return [ Controllers ] - def controllers - @controllers ||= NS::Controllers.new - end - - def run - controllers.run - rescue OptParseValidator::NoRequiredOption => e - @run_error = e - - formatter.output('@usage', msg: e.message) - rescue NoMemoryError, ScriptError, SecurityError, SignalException, StandardError, SystemStackError => e - @run_error = e - - formatter.output('@scan_aborted', - reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message, - trace: e.backtrace, - verbose: controllers.first.parsed_options[:verbose] || - run_error_exit_code == NS::ExitCode::EXCEPTION) - ensure - Browser.instance.hydra.abort - - formatter.beautify - end - - # Used for convenience - # @See Formatter - def formatter - controllers.first.formatter - end - - # @return [ Hash ] - def datastore - controllers.first.datastore - end - - # Hook to be able to have an exit code returned - # depending on the findings / errors - # :nocov: - def exit_hook - # Avoid hooking the exit when rspec is running, otherwise it will always return 0 - # and Travis won't detect failed builds. Couldn't find a better way, even though - # some people managed to https://github.com/rspec/rspec-core/pull/410 - return if defined?(RSpec) - - at_exit do - exit(run_error_exit_code) if run_error - - controller = controllers.first - - # The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given - exit(NS::ExitCode::VULNERABLE) if controller.parsed_options[:url] && controller.target.vulnerable? - exit(NS::ExitCode::OK) - end - end - # :nocov: - - # @return [ Integer ] The exit code related to the run_error - def run_error_exit_code - return NS::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) || - run_error.is_a?(OptionParser::ParseError) - - return NS::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt) - - return NS::ExitCode::ERROR if run_error.is_a?(NS::Error::Standard) || - run_error.is_a?(CMSScanner::Error::Standard) - - NS::ExitCode::EXCEPTION - end - end end require "#{CMSScanner::APP_DIR}/app" diff --git a/lib/cms_scanner/scan.rb b/lib/cms_scanner/scan.rb new file mode 100644 index 00000000..00ee9e52 --- /dev/null +++ b/lib/cms_scanner/scan.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module CMSScanner + # Scan + class Scan + attr_reader :run_error + + def initialize + controllers << NS::Controller::Core.new + + exit_hook + + yield self if block_given? + end + + # @return [ Controllers ] + def controllers + @controllers ||= NS::Controllers.new + end + + def run + controllers.run + rescue OptParseValidator::NoRequiredOption => e + @run_error = e + + formatter.output('@usage', msg: e.message) + rescue NoMemoryError, ScriptError, SecurityError, SignalException, StandardError, SystemStackError => e + @run_error = e + + formatter.output('@scan_aborted', + reason: e.is_a?(Interrupt) ? 'Canceled by User' : e.message, + trace: e.backtrace, + verbose: controllers.first.parsed_options[:verbose] || + run_error_exit_code == NS::ExitCode::EXCEPTION) + ensure + Browser.instance.hydra.abort + + formatter.beautify + end + + # Used for convenience + # @See Formatter + def formatter + controllers.first.formatter + end + + # @return [ Hash ] + def datastore + controllers.first.datastore + end + + # Hook to be able to have an exit code returned + # depending on the findings / errors + # :nocov: + def exit_hook + # Avoid hooking the exit when rspec is running, otherwise it will always return 0 + # and Travis won't detect failed builds. Couldn't find a better way, even though + # some people managed to https://github.com/rspec/rspec-core/pull/410 + return if defined?(RSpec) + + at_exit do + exit(run_error_exit_code) if run_error + + controller = controllers.first + + # The parsed_option[:url] must be checked to avoid raising erros when only -h/-v are given + exit(NS::ExitCode::VULNERABLE) if controller.parsed_options[:url] && controller.target.vulnerable? + exit(NS::ExitCode::OK) + end + end + # :nocov: + + # @return [ Integer ] The exit code related to the run_error + def run_error_exit_code + return NS::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) || + run_error.is_a?(OptionParser::ParseError) + + return NS::ExitCode::INTERRUPTED if run_error.is_a?(Interrupt) + + return NS::ExitCode::ERROR if run_error.is_a?(NS::Error::Standard) || + run_error.is_a?(CMSScanner::Error::Standard) + + NS::ExitCode::EXCEPTION + end + end +end diff --git a/spec/lib/cms_scanner_spec.rb b/spec/lib/cms_scanner_spec.rb index 09d0dcb8..aa7214d5 100644 --- a/spec/lib/cms_scanner_spec.rb +++ b/spec/lib/cms_scanner_spec.rb @@ -56,94 +56,3 @@ end end end - -describe CMSScanner::Scan do - subject(:scanner) { described_class.new } - let(:controller) { CMSScanner::Controller } - - before do - Object.send(:remove_const, :ARGV) - Object.const_set(:ARGV, []) - end - - describe '#new, #controllers' do - its(:controllers) { should eq([controller::Core.new]) } - end - - describe '#run' do - after do - scanner.run - - if defined?(run_error) - expect(scanner.run_error).to be_a run_error.class - expect(scanner.run_error.message).to eql run_error.message - end - end - - it 'runs the controlllers and calls the formatter#beautify' do - hydra = CMSScanner::Browser.instance.hydra - - expect(scanner.controllers).to receive(:run).ordered - expect(hydra).to receive(:abort).ordered - expect(scanner.formatter).to receive(:beautify).ordered - end - - context 'when no required option supplied' do - it 'calls the formatter to display the usage view' do - expect(scanner.formatter).to receive(:output) - .with('@usage', msg: 'One of the following options is required: url, help, hh, version') - end - end - - context 'when an Interrupt is raised during the scan' do - it 'aborts the scan with the correct output' do - expect(scanner.controllers.option_parser).to receive(:results).and_return({}) - - expect(scanner.controllers.first) - .to receive(:before_scan) - .and_raise(Interrupt) - - expect(scanner.formatter).to receive(:output) - .with('@scan_aborted', hash_including(reason: 'Canceled by User', trace: anything, verbose: false)) - end - end - - { - NoMemoryError.new => true, - ScriptError.new => true, - SecurityError.new => true, - SignalException.new('SIGTERM') => true, - Interrupt.new('Canceled by User') => false, - RuntimeError.new('error spotted') => true, - CMSScanner::Error::Standard.new('aa') => false, - CMSScanner::Error::MaxScanDurationReached.new => false, - SystemStackError.new => true - }.each do |error, expected_verbose| - context "when an/a #{error.class} is raised during the scan" do - let(:run_error) { error } - - it 'aborts the scan with the associated output' do - expect(scanner.controllers.option_parser).to receive(:results).and_return({}) - - expect(scanner.controllers.first) - .to receive(:before_scan) - .and_raise(run_error.class, run_error.message) - - expect(scanner.formatter).to receive(:output) - .with('@scan_aborted', hash_including(reason: run_error.message, - trace: anything, - verbose: expected_verbose)) - end - end - end - end - - describe '#datastore' do - its(:datastore) { should eq({}) } - end - - describe '#exit_hook' do - # No idea how to test that, maybe with another at_exit hook ? oO - xit - end -end diff --git a/spec/lib/scan_spec.rb b/spec/lib/scan_spec.rb new file mode 100644 index 00000000..9779b2eb --- /dev/null +++ b/spec/lib/scan_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +describe CMSScanner::Scan do + subject(:scanner) { described_class.new } + let(:controller) { CMSScanner::Controller } + + before do + Object.send(:remove_const, :ARGV) + Object.const_set(:ARGV, []) + end + + describe '#new, #controllers' do + its(:controllers) { should eq([controller::Core.new]) } + end + + describe '#run' do + after do + scanner.run + + if defined?(run_error) + expect(scanner.run_error).to be_a run_error.class + expect(scanner.run_error.message).to eql run_error.message + end + end + + it 'runs the controlllers and calls the formatter#beautify' do + hydra = CMSScanner::Browser.instance.hydra + + expect(scanner.controllers).to receive(:run).ordered + expect(hydra).to receive(:abort).ordered + expect(scanner.formatter).to receive(:beautify).ordered + end + + context 'when no required option supplied' do + it 'calls the formatter to display the usage view' do + expect(scanner.formatter).to receive(:output) + .with('@usage', msg: 'One of the following options is required: url, help, hh, version') + end + end + + context 'when an Interrupt is raised during the scan' do + it 'aborts the scan with the correct output' do + expect(scanner.controllers.option_parser).to receive(:results).and_return({}) + + expect(scanner.controllers.first) + .to receive(:before_scan) + .and_raise(Interrupt) + + expect(scanner.formatter).to receive(:output) + .with('@scan_aborted', hash_including(reason: 'Canceled by User', trace: anything, verbose: false)) + end + end + + { + NoMemoryError.new => true, + ScriptError.new => true, + SecurityError.new => true, + SignalException.new('SIGTERM') => true, + Interrupt.new('Canceled by User') => false, + RuntimeError.new('error spotted') => true, + CMSScanner::Error::Standard.new('aa') => false, + CMSScanner::Error::MaxScanDurationReached.new => false, + SystemStackError.new => true + }.each do |error, expected_verbose| + context "when an/a #{error.class} is raised during the scan" do + let(:run_error) { error } + + it 'aborts the scan with the associated output' do + expect(scanner.controllers.option_parser).to receive(:results).and_return({}) + + expect(scanner.controllers.first) + .to receive(:before_scan) + .and_raise(run_error.class, run_error.message) + + expect(scanner.formatter).to receive(:output) + .with('@scan_aborted', hash_including(reason: run_error.message, + trace: anything, + verbose: expected_verbose)) + end + end + end + end + + describe '#datastore' do + its(:datastore) { should eq({}) } + end + + describe '#exit_hook' do + # No idea how to test that, maybe with another at_exit hook ? oO + xit + end +end