diff --git a/.rubocop.yml b/.rubocop.yml index 805020bd..198628e9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,12 +8,12 @@ ClassVars: Enabled: false LineLength: Max: 120 +Lint/UriEscapeUnescape: + Enabled: false MethodLength: Max: 20 Exclude: - 'app/controllers/enumeration/cli_options.rb' -Lint/UriEscapeUnescape: - Enabled: false Metrics/AbcSize: Max: 25 Metrics/BlockLength: @@ -29,3 +29,6 @@ Style/Documentation: Enabled: false Style/FormatStringToken: Enabled: false +Style/NumericPredicate: + Exclude: + - 'app/controllers/vuln_api.rb' diff --git a/app/controllers.rb b/app/controllers.rb index cd1ff486..ee162ae6 100644 --- a/app/controllers.rb +++ b/app/controllers.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'controllers/core' +require_relative 'controllers/vuln_api' require_relative 'controllers/custom_directories' require_relative 'controllers/wp_version' require_relative 'controllers/main_theme' diff --git a/app/controllers/vuln_api.rb b/app/controllers/vuln_api.rb new file mode 100644 index 00000000..a656504c --- /dev/null +++ b/app/controllers/vuln_api.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module WPScan + module Controller + # Controller to handle the API token + class VulnApi < CMSScanner::Controller::Base + def cli_options + [ + OptString.new(['--api-token TOKEN', 'The WPVulnDB API Token to display vulnerability data']) + ] + end + + def before_scan + return unless ParsedCli.api_token + + DB::VulnApi.token = ParsedCli.api_token + + api_status = DB::VulnApi.status + + raise Error::InvalidApiToken if api_status['error'] + raise Error::ApiLimitReached if api_status['requests_remaining'] == 0 + raise api_status['http_error'] if api_status['http_error'] + end + + def after_scan + output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests) + end + end + end +end diff --git a/app/models/plugin.rb b/app/models/plugin.rb index 3d0195aa..33db83e7 100644 --- a/app/models/plugin.rb +++ b/app/models/plugin.rb @@ -15,9 +15,16 @@ module WPScan @uri = Addressable::URI.parse(blog.url(path_from_blog)) end - # @return [ JSON ] + # Retrieve the metadata from the vuln API if available (and a valid token is given), + # or the local metadata db otherwise + # @return [ Hash ] + def metadata + @metadata ||= db_data.empty? ? DB::Plugin.metadata_at(slug) : db_data + end + + # @return [ Hash ] def db_data - @db_data ||= DB::Plugin.db_data(slug) + @db_data ||= DB::VulnApi.plugin_data(slug) end # @param [ Hash ] opts diff --git a/app/models/theme.rb b/app/models/theme.rb index 1079e211..79635de6 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -21,9 +21,16 @@ module WPScan parse_style end + # Retrieve the metadata from the vuln API if available (and a valid token is given), + # or the local metadata db otherwise # @return [ JSON ] + def metadata + @metadata ||= db_data.empty? ? DB::Theme.metadata_at(slug) : db_data + end + + # @return [ Hash ] def db_data - @db_data ||= DB::Theme.db_data(slug) + @db_data ||= DB::VulnApi.theme_data(slug) end # @param [ Hash ] opts diff --git a/app/models/wp_item.rb b/app/models/wp_item.rb index c4dab331..b43b6a59 100644 --- a/app/models/wp_item.rb +++ b/app/models/wp_item.rb @@ -60,18 +60,18 @@ module WPScan # @return [ String ] def latest_version - @latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil + @latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil end # Not used anywhere ATM # @return [ Boolean ] def popular? - @popular ||= db_data['popular'] + @popular ||= metadata['popular'] ? true : false end # @return [ String ] def last_updated - @last_updated ||= db_data['last_updated'] + @last_updated ||= metadata['last_updated'] end # @return [ Boolean ] diff --git a/app/models/wp_version.rb b/app/models/wp_version.rb index c1fed313..f08b635d 100644 --- a/app/models/wp_version.rb +++ b/app/models/wp_version.rb @@ -35,9 +35,16 @@ module WPScan @all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) } end - # @return [ JSON ] + # Retrieve the metadata from the vuln API if available (and a valid token is given), + # or the local metadata db otherwise + # @return [ Hash ] + def metadata + @metadata ||= db_data.empty? ? DB::Version.metadata_at(number) : db_data + end + + # @return [ Hash ] def db_data - @db_data ||= DB::Version.db_data(number) + @db_data ||= DB::VulnApi.wordpress_data(number) end # @return [ Array ] @@ -55,12 +62,12 @@ module WPScan # @return [ String ] def release_date - @release_date ||= db_data['release_date'] || 'Unknown' + @release_date ||= metadata['release_date'] || 'Unknown' end # @return [ String ] def status - @status ||= db_data['status'] || 'Unknown' + @status ||= metadata['status'] || 'Unknown' end end end diff --git a/app/views/cli/vuln_api/status.erb b/app/views/cli/vuln_api/status.erb new file mode 100644 index 00000000..17655092 --- /dev/null +++ b/app/views/cli/vuln_api/status.erb @@ -0,0 +1,13 @@ +<% unless @status.empty? -%> +<% if @status['http_error'] -%> +<%= critical_icon %> WPVulnDB API, <%= @status['http_error'].to_s %> +<% else -%> +<%= info_icon %> WPVulnDB API OK + | Plan: <%= @status['plan'] %> + | Requests Done (during the scan): <%= @api_requests %> + | Requests Remaining: <%= @status['requests_remaining'] %> +<% end -%> +<% else -%> +<%= warning_icon %> No WPVulnDB API Token given, as a result vulnerability data has not been output. +<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register. +<% end -%> diff --git a/app/views/json/vuln_api/status.erb b/app/views/json/vuln_api/status.erb new file mode 100644 index 00000000..9b550d5d --- /dev/null +++ b/app/views/json/vuln_api/status.erb @@ -0,0 +1,13 @@ +"vuln_api": { +<% unless @status.empty? -%> +<% if @status['http_error'] -%> +"http_error": <%= @status['http_error'].to_s.to_json %> +<% else -%> +"plan": <%= @status['plan'].to_json %>, +"requests_done_during_scan": <%= @api_requests.to_json %>, +"requests_remaining": <%= @status['requests_remaining'].to_json %> +<% end -%> +<% else -%> +"error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register." +<% end -%> +}, \ No newline at end of file diff --git a/bin/wpscan b/bin/wpscan index b4f2d680..063924dc 100755 --- a/bin/wpscan +++ b/bin/wpscan @@ -5,6 +5,7 @@ require 'wpscan' WPScan::Scan.new do |s| s.controllers << + WPScan::Controller::VulnApi.new << WPScan::Controller::CustomDirectories.new << WPScan::Controller::InterestingFindings.new << WPScan::Controller::WpVersion.new << diff --git a/bin/wpscan-memprof b/bin/wpscan-memprof index 9b2e5202..7efe4ba9 100755 --- a/bin/wpscan-memprof +++ b/bin/wpscan-memprof @@ -7,6 +7,7 @@ require 'wpscan' report = MemoryProfiler.report(top: 15) do WPScan::Scan.new do |s| s.controllers << + WPScan::Controller::VulnApi.new << WPScan::Controller::CustomDirectories.new << WPScan::Controller::InterestingFindings.new << WPScan::Controller::WpVersion.new << diff --git a/bin/wpscan-stackprof b/bin/wpscan-stackprof index 07f2f05c..ab8f51e1 100755 --- a/bin/wpscan-stackprof +++ b/bin/wpscan-stackprof @@ -12,6 +12,7 @@ StackProf.run(mode: :cpu, out: '/tmp/stackprof-cpu.dump', interval: 500) do # require_relative 'wpscan' doesn't work WPScan::Scan.new do |s| s.controllers << + WPScan::Controller::VulnApi.new << WPScan::Controller::CustomDirectories.new << WPScan::Controller::InterestingFindings.new << WPScan::Controller::WpVersion.new << diff --git a/lib/wpscan.rb b/lib/wpscan.rb index 869119e2..e8e035dd 100644 --- a/lib/wpscan.rb +++ b/lib/wpscan.rb @@ -13,7 +13,8 @@ require 'uri' require 'time' require 'readline' require 'securerandom' - +# Monkey Patches/Fixes/Override +require 'wpscan/typhoeus/response' # Adds a from_vuln_api? method # Custom Libs require 'wpscan/helper' require 'wpscan/db' @@ -38,12 +39,28 @@ module WPScan APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db') + Typhoeus.on_complete do |response| + next if response.cached? || !response.from_vuln_api? + + self.api_requests += 1 + end + # Override, otherwise it would be returned as 'wp_scan' # # @return [ String ] def self.app_name 'wpscan' end + + # @return [ Integer ] + def self.api_requests + @@api_requests ||= 0 + end + + # @param [ Integer ] value + def self.api_requests=(value) + @@api_requests = value + end end require "#{WPScan::APP_DIR}/app" diff --git a/lib/wpscan/browser.rb b/lib/wpscan/browser.rb index 1f738e80..41628c74 100644 --- a/lib/wpscan/browser.rb +++ b/lib/wpscan/browser.rb @@ -7,7 +7,7 @@ module WPScan # @return [ String ] def default_user_agent - "WPScan v#{VERSION} (https://wpscan.org/)" + @default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.org/)" end end end diff --git a/lib/wpscan/db.rb b/lib/wpscan/db.rb index e0e82783..3dbd5fb0 100644 --- a/lib/wpscan/db.rb +++ b/lib/wpscan/db.rb @@ -10,6 +10,8 @@ require_relative 'db/theme' require_relative 'db/wp_version' require_relative 'db/fingerprints' +require_relative 'db/vuln_api' + require_relative 'db/dynamic_finders/base' require_relative 'db/dynamic_finders/plugin' require_relative 'db/dynamic_finders/theme' diff --git a/lib/wpscan/db/plugin.rb b/lib/wpscan/db/plugin.rb index 62cd3628..fc98949d 100644 --- a/lib/wpscan/db/plugin.rb +++ b/lib/wpscan/db/plugin.rb @@ -4,9 +4,9 @@ module WPScan module DB # Plugin DB class Plugin < WpItem - # @return [ String ] - def self.db_file - @db_file ||= DB_DIR.join('plugins.json').to_s + # @return [ Hash ] + def self.metadata + @metadata ||= super['plugins'] || {} end end end diff --git a/lib/wpscan/db/plugins.rb b/lib/wpscan/db/plugins.rb index f8472ca3..ad404a32 100644 --- a/lib/wpscan/db/plugins.rb +++ b/lib/wpscan/db/plugins.rb @@ -5,8 +5,8 @@ module WPScan # WP Plugins class Plugins < WpItems # @return [ JSON ] - def self.db - Plugin.db + def self.metadata + Plugin.metadata end end end diff --git a/lib/wpscan/db/theme.rb b/lib/wpscan/db/theme.rb index 9d919414..23b8b006 100644 --- a/lib/wpscan/db/theme.rb +++ b/lib/wpscan/db/theme.rb @@ -4,9 +4,9 @@ module WPScan module DB # Theme DB class Theme < WpItem - # @return [ String ] - def self.db_file - @db_file ||= DB_DIR.join('themes.json').to_s + # @return [ Hash ] + def self.metadata + @metadata ||= super['themes'] || {} end end end diff --git a/lib/wpscan/db/themes.rb b/lib/wpscan/db/themes.rb index 1eeb4aef..dbe273d3 100644 --- a/lib/wpscan/db/themes.rb +++ b/lib/wpscan/db/themes.rb @@ -5,8 +5,8 @@ module WPScan # WP Themes class Themes < WpItems # @return [ JSON ] - def self.db - Theme.db + def self.metadata + Theme.metadata end end end diff --git a/lib/wpscan/db/updater.rb b/lib/wpscan/db/updater.rb index ce431b02..e177ac01 100644 --- a/lib/wpscan/db/updater.rb +++ b/lib/wpscan/db/updater.rb @@ -7,12 +7,15 @@ module WPScan class Updater # /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here FILES = %w[ - plugins.json themes.json wordpresses.json + metadata.json wp_fingerprints.json timthumbs-v3.txt config_backups.txt db_exports.txt - dynamic_finders.yml wp_fingerprints.json LICENSE + dynamic_finders.yml LICENSE ].freeze - OLD_FILES = %w[wordpress.db user-agents.txt dynamic_finders_01.yml].freeze + OLD_FILES = %w[ + wordpress.db user-agents.txt dynamic_finders_01.yml + wordpresses.json plugins.json themes.json + ].freeze attr_reader :repo_directory diff --git a/lib/wpscan/db/vuln_api.rb b/lib/wpscan/db/vuln_api.rb new file mode 100644 index 00000000..a4b56466 --- /dev/null +++ b/lib/wpscan/db/vuln_api.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module WPScan + module DB + # WPVulnDB API + class VulnApi + NON_ERROR_CODES = [200, 401, 404].freeze + + class << self + attr_accessor :token + end + + # @return [ Addressable::URI ] + def self.uri + @uri ||= Addressable::URI.parse('https://wpvulndb.com/api/v3/') + end + + # @param [ String ] path + # @param [ Hash ] params + # + # @return [ Hash ] + def self.get(path, params = {}) + return {} unless token + + res = Browser.get(uri.join(path), params.merge(request_params)) + + return JSON.parse(res.body) if NON_ERROR_CODES.include?(res.code) + + raise Error::HTTP, res + rescue Error::HTTP => e + retries ||= 0 + + if (retries += 1) <= 3 + sleep(1) + retry + end + + { 'http_error' => e } + end + + # @return [ Hash ] + def self.plugin_data(slug) + get("plugins/#{slug}")&.dig(slug) || {} + end + + # @return [ Hash ] + def self.theme_data(slug) + get("themes/#{slug}")&.dig(slug) || {} + end + + # @return [ Hash ] + def self.wordpress_data(version_number) + get("wordpresses/#{version_number.tr('.', '')}")&.dig(version_number) || {} + end + + # @return [ Hash ] + def self.status + json = get('status', params: { version: WPScan::VERSION }, cache_ttl: 0) + + json['requests_remaining'] = 'Unlimited' if json['requests_remaining'] == -1 + + json + end + + # @return [ Hash ] + def self.request_params + { + headers: { + 'Host' => uri.host, # Reset in case user provided a --vhost for the target + 'Referer' => nil, # Removes referer set by the cmsscanner to the target url + 'User-Agent' => Browser.instance.default_user_agent, + 'Authorization' => "Token token=#{token}" + } + } + end + end + end +end diff --git a/lib/wpscan/db/wp_item.rb b/lib/wpscan/db/wp_item.rb index 3ac34ac6..01da5743 100644 --- a/lib/wpscan/db/wp_item.rb +++ b/lib/wpscan/db/wp_item.rb @@ -6,14 +6,19 @@ module WPScan class WpItem # @param [ String ] identifier The plugin/theme slug or version number # - # @return [ Hash ] The JSON data from the DB associated to the identifier - def self.db_data(identifier) - db[identifier] || {} + # @return [ Hash ] The JSON data from the metadata associated to the identifier + def self.metadata_at(identifier) + metadata[identifier] || {} end # @return [ JSON ] - def self.db - @db ||= read_json_file(db_file) + def self.metadata + @metadata ||= read_json_file(metadata_file) + end + + # @return [ String ] + def self.metadata_file + @metadata_file ||= DB_DIR.join('metadata.json').to_s end end end diff --git a/lib/wpscan/db/wp_items.rb b/lib/wpscan/db/wp_items.rb index 0cf4984c..432c926e 100644 --- a/lib/wpscan/db/wp_items.rb +++ b/lib/wpscan/db/wp_items.rb @@ -6,17 +6,17 @@ module WPScan class WpItems # @return [ Array ] The slug of all items def self.all_slugs - db.keys + metadata.keys end # @return [ Array ] The slug of all popular items def self.popular_slugs - db.select { |_key, item| item['popular'] == true }.keys + metadata.select { |_key, item| item['popular'] == true }.keys end # @return [ Array ] The slug of all vulnerable items def self.vulnerable_slugs - db.reject { |_key, item| item['vulnerabilities'].empty? }.keys + metadata.select { |_key, item| item['vulnerabilities'] == true }.keys end end end diff --git a/lib/wpscan/db/wp_version.rb b/lib/wpscan/db/wp_version.rb index c03a74b4..85405c31 100644 --- a/lib/wpscan/db/wp_version.rb +++ b/lib/wpscan/db/wp_version.rb @@ -4,9 +4,9 @@ module WPScan module DB # WP Version class Version < WpItem - # @return [ String ] - def self.db_file - @db_file ||= DB_DIR.join('wordpresses.json').to_s + # @return [ Hash ] + def self.metadata + @metadata ||= super['wordpress'] || {} end end end diff --git a/lib/wpscan/errors.rb b/lib/wpscan/errors.rb index 53e0599d..c1152354 100644 --- a/lib/wpscan/errors.rb +++ b/lib/wpscan/errors.rb @@ -12,5 +12,6 @@ end require_relative 'errors/enumeration' require_relative 'errors/http' require_relative 'errors/update' +require_relative 'errors/vuln_api' require_relative 'errors/wordpress' require_relative 'errors/xmlrpc' diff --git a/lib/wpscan/errors/vuln_api.rb b/lib/wpscan/errors/vuln_api.rb new file mode 100644 index 00000000..a6fb6f82 --- /dev/null +++ b/lib/wpscan/errors/vuln_api.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module WPScan + module Error + # Error raised when the token given via --api-token is invalid + class InvalidApiToken < Standard + def to_s + 'The API token provided is invalid' + end + end + + # Error raised when the number of API requests has been reached + # currently not implemented on the API side + class ApiLimitReached < Standard + def to_s + 'Your API limit has been reached' + end + end + end +end diff --git a/lib/wpscan/typhoeus/response.rb b/lib/wpscan/typhoeus/response.rb new file mode 100644 index 00000000..6ef17c8b --- /dev/null +++ b/lib/wpscan/typhoeus/response.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Typhoeus + # Custom Response class + class Response + # @note: Ignores requests done to the /status endpoint of the API + # + # @return [ Boolean ] + def from_vuln_api? + effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) && !effective_url.include?('/status') + end + end +end diff --git a/lib/wpscan/version.rb b/lib/wpscan/version.rb index 011d0667..419c8d68 100644 --- a/lib/wpscan/version.rb +++ b/lib/wpscan/version.rb @@ -2,5 +2,5 @@ # Version module WPScan - VERSION = '3.6.3' + VERSION = '3.7.0-dev' end diff --git a/spec/app/controllers/vuln_api_spec.rb b/spec/app/controllers/vuln_api_spec.rb new file mode 100644 index 00000000..f2714c49 --- /dev/null +++ b/spec/app/controllers/vuln_api_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +describe WPScan::Controller::VulnApi do + subject(:controller) { described_class.new } + let(:target_url) { 'http://ex.lo/' } + let(:cli_args) { "--url #{target_url}" } + + before do + WPScan::ParsedCli.options = rspec_parsed_options(cli_args) + end + + describe '#cli_options' do + its(:cli_options) { should_not be_empty } + its(:cli_options) { should be_a Array } + + it 'contains to correct options' do + expect(controller.cli_options.map(&:to_sym)).to eq %i[api_token] + end + end + + describe '#before_scan' do + context 'when no --api-token provided' do + its(:before_scan) { should be nil } + end + + context 'when --api-token given' do + let(:cli_args) { "#{super()} --api-token token" } + + context 'when the token is invalid' do + before { expect(WPScan::DB::VulnApi).to receive(:status).and_return('error' => 'HTTP Token: Access denied.') } + + it 'raise an InvalidApiToken error' do + expect { controller.before_scan }.to raise_error(WPScan::Error::InvalidApiToken) + end + end + + context 'when the token is valid' do + context 'when the limit has been reached' do + before do + expect(WPScan::DB::VulnApi) + .to receive(:status) + .and_return('success' => true, 'plan' => 'free', 'requests_remaining' => 0) + end + + it 'raises an ApiLimitReached error' do + expect { controller.before_scan }.to raise_error(WPScan::Error::ApiLimitReached) + end + end + + context 'when a HTTP error, like a timeout' do + before do + expect(WPScan::DB::VulnApi) + .to receive(:status) + .and_return( + 'http_error' => WPScan::Error::HTTP.new( + Typhoeus::Response.new(effective_url: 'mock-url', return_code: 28) + ) + ) + end + + it 'raises an HTTP error' do + expect { controller.before_scan } + .to raise_error(WPScan::Error::HTTP, 'HTTP Error: mock-url (Timeout was reached)') + end + end + + context 'when the token is valid and no HTTP error' do + before do + expect(WPScan::DB::VulnApi) + .to receive(:status) + .and_return('success' => true, 'plan' => 'free', 'requests_remaining' => requests) + end + + context 'when limited requests' do + let(:requests) { 100 } + + it 'does not raise an error' do + expect { controller.before_scan }.to_not raise_error + end + + context 'when unlimited requests' do + let(:requests) { 'Unlimited' } + + it 'does not raise an error' do + expect { controller.before_scan }.to_not raise_error + end + end + end + end + end + end + end +end diff --git a/spec/app/models/plugin_spec.rb b/spec/app/models/plugin_spec.rb index 8075d4a2..aa92ef0f 100644 --- a/spec/app/models/plugin_spec.rb +++ b/spec/app/models/plugin_spec.rb @@ -81,24 +81,39 @@ describe WPScan::Model::Plugin do end describe '#latest_version, #last_updated, #popular' do - context 'when none' do - let(:slug) { 'vulnerable-not-popular' } + before { allow(plugin).to receive(:db_data).and_return(db_data) } + + context 'when no db_data and no metadata' do + let(:slug) { 'not-known' } + let(:db_data) { {} } its(:latest_version) { should be_nil } its(:last_updated) { should be_nil } its(:popular?) { should be false } end - context 'when values' do + context 'when no db_data but metadata' do let(:slug) { 'no-vulns-popular' } + let(:db_data) { {} } its(:latest_version) { should eql WPScan::Model::Version.new('2.0') } its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' } its(:popular?) { should be true } end + + context 'when db_data' do + let(:slug) { 'no-vulns-popular' } + let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') } + + its(:latest_version) { should eql WPScan::Model::Version.new('2.1') } + its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' } + its(:popular?) { should be true } + end end describe '#outdated?' do + before { allow(plugin).to receive(:db_data).and_return({}) } + context 'when last_version' do let(:slug) { 'no-vulns-popular' } @@ -116,13 +131,13 @@ describe WPScan::Model::Plugin do .and_return(WPScan::Model::Version.new(version_number)) end - context 'when version < last_version' do + context 'when version < latest_version' do let(:version_number) { '1.2' } its(:outdated?) { should eql true } end - context 'when version >= last_version' do + context 'when version >= latest_version' do let(:version_number) { '3.0' } its(:outdated?) { should eql false } @@ -130,7 +145,7 @@ describe WPScan::Model::Plugin do end end - context 'when no last_version' do + context 'when no latest_version' do let(:slug) { 'vulnerable-not-popular' } context 'when no version' do @@ -153,13 +168,16 @@ describe WPScan::Model::Plugin do end describe '#vulnerabilities' do + before { allow(plugin).to receive(:db_data).and_return(db_data) } + after do expect(plugin.vulnerabilities).to eq @expected expect(plugin.vulnerable?).to eql @expected.empty? ? false : true end context 'when plugin not in the DB' do - let(:slug) { 'not-in-db' } + let(:slug) { 'not-in-db' } + let(:db_data) { {} } it 'returns an empty array' do @expected = [] @@ -168,7 +186,8 @@ describe WPScan::Model::Plugin do context 'when in the DB' do context 'when no vulnerabilities' do - let(:slug) { 'no-vulns-popular' } + let(:slug) { 'no-vulns-popular' } + let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') } it 'returns an empty array' do @expected = [] @@ -176,11 +195,13 @@ describe WPScan::Model::Plugin do end context 'when vulnerabilities' do - let(:slug) { 'vulnerable-not-popular' } + let(:slug) { 'vulnerable-not-popular' } + let(:db_data) { vuln_api_data_for('plugins/vulnerable-not-popular') } + let(:all_vulns) do [ WPScan::Vulnerability.new( - 'First Vuln', + 'First Vuln <= 6.3.10 - LFI', { wpvulndb: '1' }, 'LFI', '6.3.10' diff --git a/spec/app/models/theme_spec.rb b/spec/app/models/theme_spec.rb index 4d573bd1..c2d631cc 100644 --- a/spec/app/models/theme_spec.rb +++ b/spec/app/models/theme_spec.rb @@ -86,8 +86,179 @@ describe WPScan::Model::Theme do end end + describe '#latest_version, #last_updated, #popular' do + before do + stub_request(:get, /.*\.css\z/) + allow(theme).to receive(:db_data).and_return(db_data) + end + + context 'when no db_data and no metadata' do + let(:slug) { 'not-known' } + let(:db_data) { {} } + + its(:latest_version) { should be_nil } + its(:last_updated) { should be_nil } + its(:popular?) { should be false } + end + + context 'when no db_data but metadata' do + let(:slug) { 'no-vulns-popular' } + let(:db_data) { {} } + + its(:latest_version) { should eql WPScan::Model::Version.new('2.0') } + its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' } + its(:popular?) { should be true } + end + + context 'when db_data' do + let(:slug) { 'no-vulns-popular' } + let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') } + + its(:latest_version) { should eql WPScan::Model::Version.new('2.2') } + its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' } + its(:popular?) { should be true } + end + end + + describe '#outdated?' do + before do + stub_request(:get, /.*\.css\z/) + allow(theme).to receive(:db_data).and_return({}) + end + + context 'when last_version' do + let(:slug) { 'no-vulns-popular' } + + context 'when no version' do + before { expect(theme).to receive(:version).at_least(1).and_return(nil) } + + its(:outdated?) { should eql false } + end + + context 'when version' do + before do + expect(theme) + .to receive(:version) + .at_least(1) + .and_return(WPScan::Model::Version.new(version_number)) + end + + context 'when version < latest_version' do + let(:version_number) { '1.2' } + + its(:outdated?) { should eql true } + end + + context 'when version >= latest_version' do + let(:version_number) { '3.0' } + + its(:outdated?) { should eql false } + end + end + end + + context 'when no latest_version' do + let(:slug) { 'vulnerable-not-popular' } + + context 'when no version' do + before { expect(theme).to receive(:version).at_least(1).and_return(nil) } + + its(:outdated?) { should eql false } + end + + context 'when version' do + before do + expect(theme) + .to receive(:version) + .at_least(1) + .and_return(WPScan::Model::Version.new('1.0')) + end + + its(:outdated?) { should eql false } + end + end + end + describe '#vulnerabilities' do - xit + before do + stub_request(:get, /.*\.css\z/) + allow(theme).to receive(:db_data).and_return(db_data) + end + + after do + expect(theme.vulnerabilities).to eq @expected + expect(theme.vulnerable?).to eql @expected.empty? ? false : true + end + + context 'when theme not in the DB' do + let(:slug) { 'not-in-db' } + let(:db_data) { {} } + + it 'returns an empty array' do + @expected = [] + end + end + + context 'when in the DB' do + context 'when no vulnerabilities' do + let(:slug) { 'no-vulns-popular' } + let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') } + + it 'returns an empty array' do + @expected = [] + end + end + + context 'when vulnerabilities' do + let(:slug) { 'vulnerable-not-popular' } + let(:db_data) { vuln_api_data_for('themes/vulnerable-not-popular') } + + let(:all_vulns) do + [ + WPScan::Vulnerability.new( + 'First Vuln', + { wpvulndb: '1' }, + 'LFI', + '6.3.10' + ), + WPScan::Vulnerability.new('No Fixed In', wpvulndb: '2') + ] + end + + context 'when no theme version' do + before { expect(theme).to receive(:version).at_least(1).and_return(false) } + + it 'returns all the vulnerabilities' do + @expected = all_vulns + end + end + + context 'when theme version' do + before do + expect(theme) + .to receive(:version) + .at_least(1) + .and_return(WPScan::Model::Version.new(number)) + end + + context 'when < to a fixed_in' do + let(:number) { '5.0' } + + it 'returns it' do + @expected = all_vulns + end + end + + context 'when >= to a fixed_in' do + let(:number) { '6.3.10' } + + it 'does not return it ' do + @expected = [all_vulns.last] + end + end + end + end + end end describe '#parent_theme' do diff --git a/spec/app/models/wp_version_spec.rb b/spec/app/models/wp_version_spec.rb index 65bfbda5..e1f8ecd7 100644 --- a/spec/app/models/wp_version_spec.rb +++ b/spec/app/models/wp_version_spec.rb @@ -40,11 +40,13 @@ describe WPScan::Model::WpVersion do describe '#vulnerabilities' do subject(:version) { described_class.new(number) } + before { allow(version).to receive(:db_data).and_return(db_data) } context 'when no vulns' do let(:number) { '4.4' } + let(:db_data) { { 'vulnerabilities' => [] } } - its(:vulnerabilities) { should eql([]) } + its(:vulnerabilities) { should be_empty } end context 'when vulnerable' do @@ -53,8 +55,25 @@ describe WPScan::Model::WpVersion do expect(version).to be_vulnerable end + let(:all_vulns) do + [ + WPScan::Vulnerability.new( + 'WP 3.8.1 - Vuln 1', + { wpvulndb: '1' }, + 'SQLI' + ), + WPScan::Vulnerability.new( + 'WP 3.8.1 - Vuln 2', + { url: %w[url-2 url-3], osvdb: %w[10], cve: %w[2014-0166], wpvulndb: '2' }, + nil, + '3.8.2' + ) + ] + end + context 'when a signle vuln' do - let(:number) { '3.8' } + let(:number) { '3.8.1' } + let(:db_data) { vuln_api_data_for('wordpresses/38') } it 'returns the expected result' do @expected = [WPScan::Vulnerability.new( @@ -67,6 +86,7 @@ describe WPScan::Model::WpVersion do context 'when multiple vulns' do let(:number) { '3.8.1' } + let(:db_data) { vuln_api_data_for('wordpresses/381') } it 'returns the expected results' do @expected = [ @@ -87,27 +107,30 @@ describe WPScan::Model::WpVersion do end end - describe '#release_date' do + describe '#metadata, #release_date, #status' do subject(:version) { described_class.new('3.8.1') } - its(:release_date) { should eql '2014-01-23' } + before { allow(version).to receive(:db_data).and_return(db_data) } - context 'when the version is not in the DB' do - subject(:version) { described_class.new('3.8.2') } + context 'when no db_data' do + let(:db_data) { {} } - its(:release_date) { should eql 'Unknown' } + its(:release_date) { should eql '2014-01-23' } + its(:status) { should eql 'outdated' } + + context 'when the version is not in the metadata' do + subject(:version) { described_class.new('3.8.2') } + + its(:release_date) { should eql 'Unknown' } + its(:status) { should eql 'Unknown' } + end end - end - describe '#status' do - subject(:version) { described_class.new('3.8.1') } + context 'when db_data' do + let(:db_data) { vuln_api_data_for('wordpresses/381') } - its(:status) { should eql 'outdated' } - - context 'when the version is not in the DB' do - subject(:version) { described_class.new('3.8.2') } - - its(:release_date) { should eql 'Unknown' } + its(:release_date) { should eql '2014-01-23-via-api' } + its(:status) { should eql 'outdated-via-api' } end end end diff --git a/spec/app/views_spec.rb b/spec/app/views_spec.rb index 82a7b4fb..2f214f91 100644 --- a/spec/app/views_spec.rb +++ b/spec/app/views_spec.rb @@ -9,6 +9,7 @@ describe 'App::Views' do # in the expected output. %i[JSON CliNoColour].each do |formatter| context "when #{formatter}" do + it_behaves_like 'App::Views::VulnApi' it_behaves_like 'App::Views::WpVersion' it_behaves_like 'App::Views::MainTheme' it_behaves_like 'App::Views::Enumeration' diff --git a/spec/fixtures/db/metadata.json b/spec/fixtures/db/metadata.json new file mode 100644 index 00000000..875aeb38 --- /dev/null +++ b/spec/fixtures/db/metadata.json @@ -0,0 +1,56 @@ +{ + "wordpress": { + "4.0": { + "release_date": "2014-09-04", + "status": "latest" + }, + "3.8.1": { + "release_date": "2014-01-23", + "status": "outdated" + }, + "3.8": { + "release_date": "2013-12-12", + "status": "insecure" + } + }, + "plugins": { + "no-vulns-popular": { + "vulnerabilities": false, + "popular": true, + "latest_version": "2.0", + "last_updated": "2015-05-16T00:00:00.000Z" + }, + "vulnerable-not-popular": { + "latest_version": null, + "last_updated": null, + "popular": false, + "vulnerabilities": true + } + }, + "themes": { + "no-vulns-popular": { + "popular": true, + "latest_version": "2.0", + "last_updated": "2015-05-16T00:00:00.000Z", + "vulnerabilities": false + }, + "vulnerable-not-popular": { + "latest_version": null, + "last_updated": null, + "popular": false, + "vulnerabilities": true + }, + "dignitas-themes": { + "popular": true, + "latest_version": null, + "last_updated": null, + "vulnerabilities" : true + }, + "yaaburnee-themes": { + "popular": false, + "latest_version": null, + "last_updated": null, + "vulnerabilities" : true + } + } +} \ No newline at end of file diff --git a/spec/fixtures/db/plugins.json b/spec/fixtures/db/plugins.json deleted file mode 100644 index 6d359c89..00000000 --- a/spec/fixtures/db/plugins.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "no-vulns-popular": { - "vulnerabilities": [], - "popular": true, - "latest_version": "2.0", - "last_updated": "2015-05-16T00:00:00.000Z" - }, - "vulnerable-not-popular": { - "latest_version": null, - "last_updated": null, - "popular": false, - "vulnerabilities" : [ - { - "title" : "First Vuln", - "fixed_in" : "6.3.10", - "id" : 1, - "vuln_type": "LFI" - }, - { - "title": "No Fixed In", - "id": 2 - } - ] - } -} \ No newline at end of file diff --git a/spec/fixtures/db/themes.json b/spec/fixtures/db/themes.json deleted file mode 100644 index 4c332c0d..00000000 --- a/spec/fixtures/db/themes.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "no-vulns-popular": { - "popular": true, - "latest_version": "2.0", - "last_updated": "2015-05-16T00:00:00.000Z", - "vulnerabilities": [] - }, - "dignitas-themes": { - "popular": true, - "latest_version": null, - "last_updated": null, - "vulnerabilities" : [ - { - "created_at" : "2015-03-05T19:25:59.000Z", - "updated_at" : "2015-03-05T19:37:47.000Z", - "references": { - "url" : [ - "http://research.evex.pw/?vuln=6" - ], - "packetstorm": [ - "130652" - ] - }, - "title" : "Dignitas 1.1.9 - Privilage Escalation", - "id" : 7825, - "vuln_type" : "AUTHBYPASS" - } - ] - }, - "yaaburnee-themes": { - "popular": false, - "latest_version": null, - "last_updated": null, - "vulnerabilities" : [ - { - "created_at" : "2015-03-05T19:25:44.000Z", - "updated_at" : "2015-03-05T19:41:14.000Z", - "references": { - "url" : [ - "http://research.evex.pw/?vuln=6" - ], - "packetstorm": [ - "130652" - ] - }, - "title" : "Ya'aburnee 1.0.7 - Privilage Escalation", - "id" : 7824, - "vuln_type" : "AUTHBYPASS" - } - ] - } -} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/plugins/no-vulns-popular.json b/spec/fixtures/db/vuln_api/plugins/no-vulns-popular.json new file mode 100644 index 00000000..f542be1e --- /dev/null +++ b/spec/fixtures/db/vuln_api/plugins/no-vulns-popular.json @@ -0,0 +1,6 @@ +{ + "vulnerabilities": [], + "popular": true, + "latest_version": "2.1", + "last_updated": "2015-05-16T00:00:00.000Z-via-api" +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/plugins/vulnerable-not-popular.json b/spec/fixtures/db/vuln_api/plugins/vulnerable-not-popular.json new file mode 100644 index 00000000..5d12466f --- /dev/null +++ b/spec/fixtures/db/vuln_api/plugins/vulnerable-not-popular.json @@ -0,0 +1,17 @@ +{ + "latest_version": null, + "last_updated": null, + "popular": false, + "vulnerabilities" : [ + { + "title" : "First Vuln \u003c= 6.3.10 - LFI", + "fixed_in" : "6.3.10", + "id" : 1, + "vuln_type": "LFI" + }, + { + "title": "No Fixed In", + "id": 2 + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/themes/dignitas-themes.json b/spec/fixtures/db/vuln_api/themes/dignitas-themes.json new file mode 100644 index 00000000..cad00135 --- /dev/null +++ b/spec/fixtures/db/vuln_api/themes/dignitas-themes.json @@ -0,0 +1,20 @@ +{ + "popular": true, + "latest_version": null, + "last_updated": null, + "vulnerabilities" : [ + { + "created_at" : "2015-03-05T19:25:59.000Z", + "updated_at" : "2015-03-05T19:37:47.000Z", + "references": { + "url" : [ + "http://research.evex.pw/?vuln=6", + "http://packetstormsecurity.com/files/130652/" + ] + }, + "title" : "Dignitas 1.1.9 - Privilage Escalation", + "id" : 7825, + "vuln_type" : "AUTHBYPASS" + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/themes/no-vulns-popular.json b/spec/fixtures/db/vuln_api/themes/no-vulns-popular.json new file mode 100644 index 00000000..82bcc56e --- /dev/null +++ b/spec/fixtures/db/vuln_api/themes/no-vulns-popular.json @@ -0,0 +1,6 @@ +{ + "popular": true, + "latest_version": "2.2", + "last_updated": "2015-05-16T00:00:00.000Z-via-api", + "vulnerabilities": [] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/themes/vulnerable-not-popular.json b/spec/fixtures/db/vuln_api/themes/vulnerable-not-popular.json new file mode 100644 index 00000000..213fa925 --- /dev/null +++ b/spec/fixtures/db/vuln_api/themes/vulnerable-not-popular.json @@ -0,0 +1,17 @@ +{ + "latest_version": null, + "last_updated": null, + "popular": false, + "vulnerabilities" : [ + { + "title" : "First Vuln", + "fixed_in" : "6.3.10", + "id" : 1, + "vuln_type": "LFI" + }, + { + "title": "No Fixed In", + "id": 2 + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/themes/yaaburnee-themes.json b/spec/fixtures/db/vuln_api/themes/yaaburnee-themes.json new file mode 100644 index 00000000..18762bea --- /dev/null +++ b/spec/fixtures/db/vuln_api/themes/yaaburnee-themes.json @@ -0,0 +1,20 @@ +{ + "popular": false, + "latest_version": null, + "last_updated": null, + "vulnerabilities" : [ + { + "created_at" : "2015-03-05T19:25:44.000Z", + "updated_at" : "2015-03-05T19:41:14.000Z", + "references": { + "url" : [ + "http://research.evex.pw/?vuln=6", + "http://packetstormsecurity.com/files/130652/" + ] + }, + "title" : "Ya'aburnee 1.0.7 - Privilage Escalation", + "id" : 7824, + "vuln_type" : "AUTHBYPASS" + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/wordpresses/38.json b/spec/fixtures/db/vuln_api/wordpresses/38.json new file mode 100644 index 00000000..9a80048a --- /dev/null +++ b/spec/fixtures/db/vuln_api/wordpresses/38.json @@ -0,0 +1,17 @@ +{ + "release_date" : "2013-12-12", + "status": "insecure", + "vulnerabilities" : [ + { + "references": { + "url" : ["url-4"], + "osvdb" : ["11"] + }, + "created_at" : "2014-08-01T10:58:19.000Z", + "updated_at" : "2014-09-16T15:45:26.000Z", + "title" : "WP 3.8 - Vuln 1", + "id" : 3, + "vuln_type" : "AUTHBYPASS" + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/wordpresses/381.json b/spec/fixtures/db/vuln_api/wordpresses/381.json new file mode 100644 index 00000000..aee93345 --- /dev/null +++ b/spec/fixtures/db/vuln_api/wordpresses/381.json @@ -0,0 +1,27 @@ +{ + "release_date" : "2014-01-23-via-api", + "status": "outdated-via-api", + "vulnerabilities" : [ + { + "created_at" : "2014-08-01T10:58:19.000Z", + "updated_at" : "2014-09-16T13:52:17.000Z", + "title" : "WP 3.8.1 - Vuln 1", + "id" : 1, + "vuln_type" : "SQLI", + "published_date" : null, + "fixed_in" : null + }, + { + "references" : { + "cve" : ["2014-0166"], + "osvdb" : ["10"], + "url" : ["url-2","url-3"] + }, + "fixed_in" : "3.8.2", + "created_at" : "2014-08-01T10:58:19.000Z", + "updated_at" : "2014-09-16T13:53:11.000Z", + "id" : 2, + "title" : "WP 3.8.1 - Vuln 2" + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/db/vuln_api/wordpresses/40.json b/spec/fixtures/db/vuln_api/wordpresses/40.json new file mode 100644 index 00000000..c809d0ed --- /dev/null +++ b/spec/fixtures/db/vuln_api/wordpresses/40.json @@ -0,0 +1,4 @@ +{ + "release_date" : "2014-09-04", + "status": "latest" +} \ No newline at end of file diff --git a/spec/fixtures/db/wordpresses.json b/spec/fixtures/db/wordpresses.json deleted file mode 100644 index 82eb4419..00000000 --- a/spec/fixtures/db/wordpresses.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "4.0": { - "release_date" : "2014-09-04", - "status": "latest" - }, - "3.8.1": { - "release_date" : "2014-01-23", - "status": "outdated", - "vulnerabilities" : [ - { - "created_at" : "2014-08-01T10:58:19.000Z", - "updated_at" : "2014-09-16T13:52:17.000Z", - "title" : "WP 3.8.1 - Vuln 1", - "id" : 1, - "vuln_type" : "SQLI", - "published_date" : null, - "fixed_in" : null - }, - { - "references" : { - "cve" : ["2014-0166"], - "url" : ["url-2","url-3"] - }, - "fixed_in" : "3.8.2", - "created_at" : "2014-08-01T10:58:19.000Z", - "updated_at" : "2014-09-16T13:53:11.000Z", - "id" : 2, - "title" : "WP 3.8.1 - Vuln 2" - } - ] - }, - "3.8": { - "release_date" : "2013-12-12", - "status": "insecure", - "vulnerabilities" : [ - { - "references": { - "url" : ["url-4"] - }, - "created_at" : "2014-08-01T10:58:19.000Z", - "updated_at" : "2014-09-16T15:45:26.000Z", - "title" : "WP 3.8 - Vuln 1", - "id" : 3, - "vuln_type" : "AUTHBYPASS" - } - ] - } -} diff --git a/spec/lib/db/themes_spec.rb b/spec/lib/db/themes_spec.rb index d97c5c2a..38be7baf 100644 --- a/spec/lib/db/themes_spec.rb +++ b/spec/lib/db/themes_spec.rb @@ -4,7 +4,7 @@ describe WPScan::DB::Themes do subject(:themes) { described_class } describe '#all_slugs' do - its(:all_slugs) { should eql %w[no-vulns-popular dignitas-themes yaaburnee-themes] } + its(:all_slugs) { should eql %w[no-vulns-popular vulnerable-not-popular dignitas-themes yaaburnee-themes] } end describe '#popular_slugs' do @@ -12,6 +12,6 @@ describe WPScan::DB::Themes do end describe '#vulnerable_slugs' do - its(:vulnerable_slugs) { should eql %w[dignitas-themes yaaburnee-themes] } + its(:vulnerable_slugs) { should eql %w[vulnerable-not-popular dignitas-themes yaaburnee-themes] } end end diff --git a/spec/lib/db/vuln_api_spec.rb b/spec/lib/db/vuln_api_spec.rb new file mode 100644 index 00000000..a784241f --- /dev/null +++ b/spec/lib/db/vuln_api_spec.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +describe WPScan::DB::VulnApi do + subject(:api) { described_class } + + describe '#uri' do + its(:uri) { should be_a Addressable::URI } + end + + describe '#token, @token=' do + context 'when no token set' do + before { api.token = nil } # In case it was set by a previous spec + + its(:token) { should be nil } + end + + context 'when token set' do + it 'returns it' do + api.token = 's3cRet' + + expect(api.token).to eql 's3cRet' + end + end + end + + describe '#get' do + context 'when no token' do + before { api.token = nil } + + it 'returns an empty hash' do + expect(api.get('test')).to eql({}) + end + end + + context 'when a token' do + before { api.token = 's3cRet' } + + context 'when no timeouts' do + before do + stub_request(:get, api.uri.join('path')) + .with(headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil, + 'User-Agent' => WPScan::Browser.instance.default_user_agent, + 'Authorization' => 'Token token=s3cRet' }) + .to_return(status: code, body: body) + end + + context 'when 200' do + let(:code) { 200 } + let(:body) { { data: 'something' }.to_json } + + it 'returns the expected hash' do + result = api.get('path') + + expect(result).to eql('data' => 'something') + end + end + + context 'when 401' do + let(:code) { 401 } + let(:body) { { error: 'HTTP Token: Access denied.' }.to_json } + + it 'returns the expected hash' do + result = api.get('path') + + expect(result).to eql('error' => 'HTTP Token: Access denied.') + end + end + + context 'when 404' do + let(:code) { 404 } + let(:body) { { error: 'Not found' }.to_json } + + it 'returns an empty hash' do + result = api.get('path') + + expect(result).to eql('error' => 'Not found') + end + end + end + + context 'when timeouts' do + context 'when all requests timeout' do + before do + stub_request(:get, api.uri.join('path')) + .with(headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil, + 'User-Agent' => WPScan::Browser.instance.default_user_agent, + 'Authorization' => 'Token token=s3cRet' }) + .to_return(status: 0) + end + + it 'tries 3 times and returns the hash with the error' do + expect(api).to receive(:sleep).with(1).exactly(3).times + + result = api.get('path') + + expect(result['http_error']).to be_a WPScan::Error::HTTP + end + end + + context 'when only the first request timeout' do + before do + stub_request(:get, api.uri.join('path')) + .with(headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil, + 'User-Agent' => WPScan::Browser.instance.default_user_agent, + 'Authorization' => 'Token token=s3cRet' }) + .to_return(status: 0).then + .to_return(status: 200, body: { data: 'test' }.to_json) + end + + it 'tries 1 time and returns expected data' do + expect(api).to receive(:sleep).with(1).exactly(1).times + + result = api.get('path') + + expect(result).to eql('data' => 'test') + end + end + end + end + end + + describe '#plugin_data' do + before { api.token = api_token } + + context 'when no --api-token' do + let(:api_token) { nil } + + it 'returns an empty hash' do + expect(api.plugin_data('slug')).to eql({}) + end + end + + context 'when valid --api-token' do + let(:api_token) { 's3cRet' } + + context 'when the slug exist' do + it 'calls the correct URL' do + stub_request(:get, api.uri.join('plugins/slug')) + .to_return(status: 200, body: { slug: { p: 'aa' } }.to_json) + + expect(api.plugin_data('slug')).to eql('p' => 'aa') + end + end + + context 'when the slug does not exist' do + it 'returns an empty hash' do + stub_request(:get, api.uri.join('plugins/slug-404')).to_return(status: 404, body: '{}') + + expect(api.plugin_data('slug-404')).to eql({}) + end + end + end + end + + describe '#theme_data' do + before { api.token = api_token } + + context 'when no --api-token' do + let(:api_token) { nil } + + it 'returns an empty hash' do + expect(api.theme_data('slug')).to eql({}) + end + end + + context 'when valid --api-token' do + let(:api_token) { 's3cRet' } + + context 'when the slug exist' do + it 'calls the correct URL' do + stub_request(:get, api.uri.join('themes/slug')) + .to_return(status: 200, body: { slug: { t: 'aa' } }.to_json) + + expect(api.theme_data('slug')).to eql('t' => 'aa') + end + end + + context 'when the slug does not exist' do + it 'returns an empty hash' do + stub_request(:get, api.uri.join('themes/slug-404')).to_return(status: 404, body: '{}') + + expect(api.theme_data('slug-404')).to eql({}) + end + end + end + end + + describe '#wordpress_data' do + before { api.token = api_token } + + context 'when no --api-token' do + let(:api_token) { nil } + + it 'returns an empty hash' do + expect(api.wordpress_data('1.2')).to eql({}) + end + end + + context 'when valid --api-token' do + let(:api_token) { 's3cRet' } + + context 'when the version exist' do + it 'calls the correct URL' do + stub_request(:get, api.uri.join('wordpresses/522')) + .to_return(status: 200, body: { '5.2.2' => { w: 'aa' } }.to_json) + + expect(api.wordpress_data('5.2.2')).to eql('w' => 'aa') + end + end + + context 'when the version does not exist' do + it 'returns an empty hash' do + stub_request(:get, api.uri.join('wordpresses/11')).to_return(status: 404, body: '{}') + + expect(api.wordpress_data('1.1')).to eql({}) + end + end + end + end + + describe '#status' do + before do + api.token = 's3cRet' + + stub_request(:get, api.uri.join('status')) + .with(query: { version: WPScan::VERSION }, + headers: { 'Host' => api.uri.host, 'Expect' => nil, 'Referer' => nil, + 'User-Agent' => WPScan::Browser.instance.default_user_agent, + 'Authorization' => 'Token token=s3cRet' }) + .to_return(status: code, body: return_body.to_json) + end + + let(:code) { 200 } + let(:return_body) { {} } + + context 'when 200' do + let(:return_body) { { success: true, plan: 'free', requests_remaining: 100 } } + + it 'returns the expected hash' do + status = api.status + + expect(status['success']).to be true + expect(status['plan']).to eql 'free' + expect(status['requests_remaining']).to eql 100 + end + + context 'when unlimited requests' do + let(:return_body) { super().merge(plan: 'enterprise', requests_remaining: -1) } + + it 'returns the expected hash, witht he correct requests_remaining' do + status = api.status + + expect(status['success']).to be true + expect(status['plan']).to eql 'enterprise' + expect(status['requests_remaining']).to eql 'Unlimited' + end + end + end + + context 'when 401' do + let(:code) { 401 } + let(:return_body) { { error: 'HTTP Token: Access denied.' } } + + it 'returns the expected hash' do + status = api.status + + expect(status['error']).to eql 'HTTP Token: Access denied.' + end + end + + context 'otherwise' do + let(:code) { 0 } + + it 'returns the expected hash with the response' do + status = api.status + + expect(status['http_error']).to be_a WPScan::Error::HTTP + end + end + end +end diff --git a/spec/lib/target_spec.rb b/spec/lib/target_spec.rb index df79d600..9f6fd9a5 100644 --- a/spec/lib/target_spec.rb +++ b/spec/lib/target_spec.rb @@ -83,14 +83,22 @@ describe WPScan::Target do end context 'when wp_version found' do + before do + expect(wp_version) + .to receive(:db_data) + .and_return(vuln_api_data_for("wordpresses/#{wp_version.number.tr('.', '')}")) + + target.instance_variable_set(:@wp_version, wp_version) + end + context 'when not vulnerable' do - before { target.instance_variable_set(:@wp_version, WPScan::Model::WpVersion.new('4.4')) } + let(:wp_version) { WPScan::Model::WpVersion.new('4.0') } it { should_not be_vulnerable } end context 'when vulnerable' do - before { target.instance_variable_set(:@wp_version, WPScan::Model::WpVersion.new('3.8.1')) } + let(:wp_version) { WPScan::Model::WpVersion.new('3.8.1') } it { should be_vulnerable } end diff --git a/spec/output/vuln_api/all_ok.cli_no_colour b/spec/output/vuln_api/all_ok.cli_no_colour new file mode 100644 index 00000000..f95873d1 --- /dev/null +++ b/spec/output/vuln_api/all_ok.cli_no_colour @@ -0,0 +1,4 @@ +[+] WPVulnDB API OK + | Plan: paid + | Requests Done (during the scan): 3 + | Requests Remaining: 120 diff --git a/spec/output/vuln_api/all_ok.json b/spec/output/vuln_api/all_ok.json new file mode 100644 index 00000000..445fd787 --- /dev/null +++ b/spec/output/vuln_api/all_ok.json @@ -0,0 +1,7 @@ +{ + "vuln_api": { + "plan": "paid", + "requests_done_during_scan": 3, + "requests_remaining": 120 + } +} \ No newline at end of file diff --git a/spec/output/vuln_api/http_error.cli_no_colour b/spec/output/vuln_api/http_error.cli_no_colour new file mode 100644 index 00000000..5fd40fa3 --- /dev/null +++ b/spec/output/vuln_api/http_error.cli_no_colour @@ -0,0 +1 @@ +[!] WPVulnDB API, HTTP Error: url (Timeout was reached) diff --git a/spec/output/vuln_api/http_error.json b/spec/output/vuln_api/http_error.json new file mode 100644 index 00000000..b0c6dacb --- /dev/null +++ b/spec/output/vuln_api/http_error.json @@ -0,0 +1,5 @@ +{ + "vuln_api": { + "http_error": "HTTP Error: url (Timeout was reached)" + } +} \ No newline at end of file diff --git a/spec/output/vuln_api/no_more_requests.cli_no_colour b/spec/output/vuln_api/no_more_requests.cli_no_colour new file mode 100644 index 00000000..a0cdc0b9 --- /dev/null +++ b/spec/output/vuln_api/no_more_requests.cli_no_colour @@ -0,0 +1,4 @@ +[+] WPVulnDB API OK + | Plan: free + | Requests Done (during the scan): 3 + | Requests Remaining: 0 diff --git a/spec/output/vuln_api/no_more_requests.json b/spec/output/vuln_api/no_more_requests.json new file mode 100644 index 00000000..71145329 --- /dev/null +++ b/spec/output/vuln_api/no_more_requests.json @@ -0,0 +1,7 @@ +{ + "vuln_api": { + "plan": "free", + "requests_done_during_scan": 3, + "requests_remaining": 0 + } +} \ No newline at end of file diff --git a/spec/output/vuln_api/no_token.cli_no_colour b/spec/output/vuln_api/no_token.cli_no_colour new file mode 100644 index 00000000..aa626461 --- /dev/null +++ b/spec/output/vuln_api/no_token.cli_no_colour @@ -0,0 +1,2 @@ +[!] No WPVulnDB API Token given, as a result vulnerability data has not been output. +[!] You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register. diff --git a/spec/output/vuln_api/no_token.json b/spec/output/vuln_api/no_token.json new file mode 100644 index 00000000..d30384d9 --- /dev/null +++ b/spec/output/vuln_api/no_token.json @@ -0,0 +1,5 @@ +{ + "vuln_api": { + "error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/register." + } +} \ No newline at end of file diff --git a/spec/output/vuln_api/unlimited_requests.cli_no_colour b/spec/output/vuln_api/unlimited_requests.cli_no_colour new file mode 100644 index 00000000..adf71437 --- /dev/null +++ b/spec/output/vuln_api/unlimited_requests.cli_no_colour @@ -0,0 +1,4 @@ +[+] WPVulnDB API OK + | Plan: enterprise + | Requests Done (during the scan): 3 + | Requests Remaining: Unlimited diff --git a/spec/output/vuln_api/unlimited_requests.json b/spec/output/vuln_api/unlimited_requests.json new file mode 100644 index 00000000..5a2f8d8e --- /dev/null +++ b/spec/output/vuln_api/unlimited_requests.json @@ -0,0 +1,7 @@ +{ + "vuln_api": { + "plan": "enterprise", + "requests_done_during_scan": 3, + "requests_remaining": "Unlimited" + } +} \ No newline at end of file diff --git a/spec/output/wp_version/with_vulns.cli_no_colour b/spec/output/wp_version/with_vulns.cli_no_colour index bf68dd18..ff096336 100644 --- a/spec/output/wp_version/with_vulns.cli_no_colour +++ b/spec/output/wp_version/with_vulns.cli_no_colour @@ -1,4 +1,4 @@ -[+] WordPress version 3.8.1 identified (Outdated, released on 2014-01-23). +[+] WordPress version 3.8.1 identified (Outdated-via-api, released on 2014-01-23-via-api). | Detected By: rspec | | [!] 2 vulnerabilities identified: diff --git a/spec/output/wp_version/with_vulns.json b/spec/output/wp_version/with_vulns.json index 726c133b..8dd8430e 100644 --- a/spec/output/wp_version/with_vulns.json +++ b/spec/output/wp_version/with_vulns.json @@ -1,8 +1,8 @@ { "version": { "number": "3.8.1", - "release_date": "2014-01-23", - "status": "outdated", + "release_date": "2014-01-23-via-api", + "status": "outdated-via-api", "found_by": "rspec", "confidence": 0, "interesting_entries": [ diff --git a/spec/shared_examples.rb b/spec/shared_examples.rb index 8c8636ea..526acb76 100644 --- a/spec/shared_examples.rb +++ b/spec/shared_examples.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'shared_examples/views/vuln_api' require 'shared_examples/views/wp_version' require 'shared_examples/views/main_theme' require 'shared_examples/views/enumeration' diff --git a/spec/shared_examples/views/main_theme.rb b/spec/shared_examples/views/main_theme.rb index 96556d71..3710ea48 100644 --- a/spec/shared_examples/views/main_theme.rb +++ b/spec/shared_examples/views/main_theme.rb @@ -60,6 +60,8 @@ shared_examples 'App::Views::MainTheme' do it 'outputs the expected string' do expect(theme).to receive(:version).at_least(1) + allow(theme).to receive(:db_data).and_return(vuln_api_data_for('themes/dignitas-themes')) + @tpl_vars = tpl_vars.merge(theme: theme, verbose: true) end end diff --git a/spec/shared_examples/views/vuln_api.rb b/spec/shared_examples/views/vuln_api.rb new file mode 100644 index 00000000..4150708d --- /dev/null +++ b/spec/shared_examples/views/vuln_api.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +shared_examples 'App::Views::VulnApi' do + let(:controller) { WPScan::Controller::VulnApi.new } + let(:tpl_vars) { { url: target_url } } + + describe 'status' do + let(:view) { 'status' } + + context 'when no api token is given' do + let(:expected_view) { 'no_token' } + + it 'outputs the expected string' do + @tpl_vars = tpl_vars.merge(status: {}) + end + end + + context 'when http error' do + let(:expected_view) { 'http_error' } + + it 'outputs the expected string' do + @tpl_vars = tpl_vars.merge( + status: { + 'http_error' => WPScan::Error::HTTP.new(Typhoeus::Response.new(effective_url: 'url', return_code: 28)) + } + ) + end + end + + context 'when no more remaining requests' do + let(:expected_view) { 'no_more_requests' } + + it 'outputs the expected string' do + @tpl_vars = tpl_vars.merge( + status: { 'success': true, 'plan' => 'free', 'requests_remaining' => 0 }, + api_requests: 3 + ) + end + end + + context 'when everything is fine' do + let(:expected_view) { 'all_ok' } + + it 'outputs the expected string' do + @tpl_vars = tpl_vars.merge( + status: { 'success': true, 'plan' => 'paid', 'requests_remaining' => 120 }, + api_requests: 3 + ) + end + end + + context 'when unlimited requests' do + let(:expected_view) { 'unlimited_requests' } + + it 'outputs the expected string' do + @tpl_vars = tpl_vars.merge( + status: { 'success': true, 'plan' => 'enterprise', 'requests_remaining' => 'Unlimited' }, + api_requests: 3 + ) + end + end + end +end diff --git a/spec/shared_examples/views/wp_version.rb b/spec/shared_examples/views/wp_version.rb index 93b416ff..18cc6f3d 100644 --- a/spec/shared_examples/views/wp_version.rb +++ b/spec/shared_examples/views/wp_version.rb @@ -17,6 +17,7 @@ shared_examples 'App::Views::WpVersion' do context 'when the version is not nil' do let(:version) { WPScan::Model::WpVersion.new('4.0', found_by: 'rspec') } + before { allow(version).to receive(:db_data).and_return(vuln_api_data_for('wordpresses/40')) } context 'when confirmed_by is empty' do context 'when no interesting_entries' do @@ -77,9 +78,12 @@ shared_examples 'App::Views::WpVersion' do context 'when the version is vulnerable' do let(:expected_view) { 'with_vulns' } + let(:version) { WPScan::Model::WpVersion.new('3.8.1', found_by: 'rspec') } + + before { allow(version).to receive(:db_data).and_return(vuln_api_data_for('wordpresses/381')) } it 'outputs the expected string' do - @tpl_vars = tpl_vars.merge(version: WPScan::Model::WpVersion.new('3.8.1', found_by: 'rspec')) + @tpl_vars = tpl_vars.merge(version: version) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6dc92641..9dfe59b1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -44,6 +44,10 @@ def df_stubbed_response(fixture, finder_super_class) end end +def vuln_api_data_for(path) + JSON.parse(File.read(FIXTURES.join('db', 'vuln_api', "#{path}.json"))) +end + require 'wpscan' require 'shared_examples'