Back to Repositories

Testing Dynamic Plugin Version Detection in WPScan

This test suite validates the dynamic version detection capabilities for WordPress plugins in WPScan. It implements comprehensive testing for various version finder configurations and ensures accurate plugin version identification through both passive and aggressive scanning methods.

Test Coverage Overview

The test suite provides extensive coverage for plugin version detection mechanisms.

  • Tests both passive and aggressive version detection methods
  • Validates multiple finder configurations and classes
  • Covers homepage and 404 page detection scenarios
  • Verifies version extraction from different response patterns

Implementation Analysis

The implementation uses RSpec’s describe/context/it blocks to structure the tests systematically.

Key patterns include:
  • Dynamic finder class generation based on configuration
  • Stubbed HTTP responses for controlled testing
  • Shared examples for common version detection scenarios
  • Flexible version matching against expected results

Technical Details

  • RSpec testing framework with shared contexts
  • WebMock for HTTP request stubbing
  • YAML fixtures for test data
  • Dynamic class constant generation
  • Custom matcher support for version validation

Best Practices Demonstrated

The test suite exemplifies strong testing practices through structured organization and thorough validation.

  • Comprehensive edge case coverage
  • Clear test isolation and setup
  • Consistent error handling verification
  • Modular test organization
  • Detailed documentation of test requirements

wpscanteam/wpscan

spec/lib/finders/dynamic_finder/plugin_version_spec.rb

            
# frozen_string_literal: true

# All Plugin Dynamic Finders returning a Version are tested here.
# When adding one to the spec/fixtures/db/dynamic_finder.yml, a few files have
# to be edited/created
#
# - spec/fixtures/dynamic_finder/expected.yml with the expected result/s
# - Then, depending on the finder class used: spec/fixtures/dynamic_finder/plugin_version/
#
# Furthermore, the fixtures files _passive_all.html are also used by plugins/themes
# finders in spec/app/finders/plugins|themes to check the items existence from the homepage
#
# In case of a failure, it's recommended to use rspec -e "<Full Description>" while fixing.
# e.g: rspec -e "WPScan::Finders::PluginVersion::Cardealerpress::HeaderPattern#passive"
# The -e option can also be used to test all HeaderPattern, for example: rspec -e "::HeaderPattern"

expected_all = df_expected_all['plugins']

WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |slug, configs|
  WPScan::DB::DynamicFinders::Plugin.create_versions_finders(slug)

  configs.each do |finder_class, config|
    finder_super_class = config['class'] || finder_class

    # The QueryParameter specs are slow given the huge fixture file
    # If someone find a fix for that, please share!
    describe df_tested_class_constant('PluginVersion', finder_class, slug), slow: true do
      subject(:finder) { described_class.new(plugin) }
      let(:plugin)     { WPScan::Model::Plugin.new(slug, target) }
      let(:target)     { WPScan::Target.new('http://wp.lab/') }
      let(:fixtures)   { DYNAMIC_FINDERS_FIXTURES.join('plugin_version') }

      let(:expected) do
        if expected_all[slug][finder_class].is_a?(Hash)
          [expected_all[slug][finder_class]]
        else
          expected_all[slug][finder_class]
        end
      end

      before { allow(target).to receive(:content_dir).and_return('wp-content') }

      describe '#passive', slow: true do
        before do
          if defined?(stubbed_homepage_res)
            stub_request(:get, target.url).to_return(stubbed_homepage_res)
          else
            stub_request(:get, target.url)
          end

          if defined?(stubbed_404_res)
            stub_request(:get, ERROR_404_URL_PATTERN).to_return(stubbed_404_res)
          else
            stub_request(:get, ERROR_404_URL_PATTERN)
          end
        end

        if config['path']
          context 'when PATH' do
            it 'returns nil' do
              expect(finder.passive).to eql nil
            end
          end
        else
          context 'when no PATH' do
            context 'when the version is detected' do
              context 'from the homepage' do
                let(:ie_url) { target.url }
                let(:stubbed_homepage_res) do
                  df_stubbed_response(
                    fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
                    finder_super_class
                  )
                end

                it 'returns the expected version/s' do
                  found = Array(finder.passive)

                  expect(found).to_not be_empty

                  found.each_with_index do |version, index|
                    expected_version = expected.at(index)
                    expected_ie = expected_version['interesting_entries'].map do |ie|
                      ie.gsub("#{target.url},", "#{ie_url},")
                    end

                    expect(version).to be_a WPScan::Model::Version
                    expect(version.number).to eql expected_version['number'].to_s
                    expect(version.found_by).to eql expected_version['found_by']
                    expect(version.interesting_entries).to match_array expected_ie

                    expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
                  end
                end
              end

              context 'from the 404' do
                let(:ie_url) { target.error_404_url }
                let(:stubbed_404_res) do
                  df_stubbed_response(
                    fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
                    finder_super_class
                  )
                end

                it 'returns the expected version/s' do
                  found = Array(finder.passive)

                  expect(found).to_not be_empty

                  found.each_with_index do |version, index|
                    expected_version = expected.at(index)
                    expected_ie = expected_version['interesting_entries'].map do |ie|
                      ie.gsub("#{target.url},", "#{ie_url},")
                    end

                    expect(version).to be_a WPScan::Model::Version
                    expect(version.number).to eql expected_version['number'].to_s
                    expect(version.found_by).to eql expected_version['found_by']
                    expect(version.interesting_entries).to match_array expected_ie

                    expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
                  end
                end
              end
            end

            context 'when the version is not detected' do
              it 'returns nil or an empty array' do
                expect(finder.passive).to eql finder_super_class == 'QueryParameter' ? [] : nil
              end
            end
          end
        end
      end

      describe '#aggressive' do
        let(:fixtures)         { super().join(slug, finder_class.underscore) }
        let(:stubbed_response) { { body: 'aa' } }

        before do
          stub_request(:get, plugin.url(config['path'])).to_return(stubbed_response) if config['path']
        end

        if config['path']
          context 'when the version is detected' do
            let(:stubbed_response) do
              df_stubbed_response(fixtures.join(config['path']), finder_super_class)
            end

            it 'returns the expected version' do
              found = Array(finder.aggressive)

              expect(found).to_not be_empty

              found.each_with_index do |version, index|
                expected_version = expected.at(index)

                expect(version).to be_a WPScan::Model::Version
                expect(version.number).to eql expected_version['number'].to_s
                expect(version.found_by).to eql expected_version['found_by']
                expect(version.interesting_entries).to match_array expected_version['interesting_entries']

                expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
              end
            end
          end

          context 'when the version is not detected' do
            it 'returns nil or an empty array' do
              expect(finder.aggressive).to eql finder_super_class == 'QueryParameter' ? [] : nil
            end
          end
        else
          it 'returns nil' do
            expect(finder.aggressive).to eql nil
          end
        end
      end
    end
  end
end