Testing WordPress Theme Detection and Analysis in WPScan
This comprehensive test suite evaluates the WordPress Theme model functionality in WPScan. It verifies theme detection, version handling, vulnerability assessment, and parent theme relationships. The tests ensure robust theme analysis capabilities for security scanning.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
wpscanteam/wpscan
spec/app/models/theme_spec.rb
# frozen_string_literal: true
describe WPScan::Model::Theme do
subject(:theme) { described_class.new(slug, blog, opts) }
let(:slug) { 'spec' }
let(:blog) { WPScan::Target.new('http://wp.lab/') }
let(:opts) { {} }
let(:fixtures) { FIXTURES.join('models', 'theme') }
before { expect(blog).to receive(:content_dir).at_least(1).and_return('wp-content') }
describe '#new' do
before { stub_request(:get, /.*\.css\z/).to_return(body: File.read(fixture)) }
let(:fixture) { fixtures.join('style.css') }
its(:url) { should eql 'http://wp.lab/wp-content/themes/spec/' }
its(:style_url) { should eql 'http://wp.lab/wp-content/themes/spec/style.css' }
its(:style_name) { should eql 'Twenty Fifteen' }
its(:style_uri) { should eql 'https://wordpress.org/themes/twentyfifteen' }
its(:author) { should eql 'the WordPress team' }
its(:author_uri) { should eql nil }
its(:template) { should eql nil }
its(:description) { should eql 'Our 2015 default theme is clean, blog-focused.' }
its(:license) { should eql 'GNU General Public License v2 or later' }
its(:license_uri) { should eql 'http://www.gnu.org/licenses/gpl-2.0.html' }
its(:tags) { should eql 'black, blue, gray, pink, purple, white, yellow.' }
its(:text_domain) { should eql 'twentyfifteen' }
context 'when opts[:style_url]' do
let(:opts) { super().merge(style_url: 'http://wp.lab/wp-content/themes/spec/custom.css') }
its(:style_url) { should eql opts[:style_url] }
end
context 'when some new lines are stripped' do
let(:fixture) { fixtures.join('stripped_new_lines.css') }
its(:style_name) { should eql 'Divi' }
its(:style_uri) { should eql 'http://www.elegantthemes.com/gallery/divi/' }
its(:license_uri) { should eql 'http://www.gnu.org/licenses/gpl-2.0.html' }
end
context 'when no tags' do
let(:fixture) { fixtures.join('no_tags.css') }
its(:author) { should eql nil }
end
end
describe '#version' do
after do
stub_request(:get, /.*\.css\z/)
.to_return(body: File.read(fixtures.join('style.css')))
expect(WPScan::Finders::ThemeVersion::Base).to receive(:find).with(theme, @expected_opts)
theme.version(version_opts)
end
let(:default_opts) { {} }
context 'when no :detection_mode' do
context 'when no :mode opt supplied' do
let(:version_opts) { { something: 'k' } }
it 'calls the finder with the correct parameters' do
@expected_opts = version_opts
end
end
context 'when :mode supplied' do
let(:version_opts) { { mode: :passive } }
it 'calls the finder with the correct parameters' do
@expected_opts = default_opts.merge(mode: :passive)
end
end
end
context 'when :detection_mode' do
let(:opts) { super().merge(mode: :passive) }
context 'when no :mode' do
let(:version_opts) { {} }
it 'calls the finder without mode' do
@expected_opts = version_opts
end
end
context 'when :mode' do
let(:version_opts) { { mode: :mixed } }
it 'calls the finder with the :mixed mode' do
@expected_opts = default_opts.merge(mode: :mixed)
end
end
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
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 [email protected]?
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',
references: { wpvulndb: '1' },
type: 'LFI',
fixed_in: '6.3.10'
),
WPScan::Vulnerability.new('No Fixed In', references: { 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
before do
stub_request(:get, blog.url('wp-content/themes/spec/style.css'))
.to_return(body: File.read(fixtures.join(main_theme)))
end
context 'when no template' do
let(:main_theme) { 'style.css' }
it 'returns nil' do
expect(theme.parent_theme).to eql nil
end
end
context 'when a template' do
let(:main_theme) { 'child_style.css' }
let(:parent_url) { blog.url('wp-content/themes/twentyfourteen/custom.css') }
before do
stub_request(:get, parent_url)
.to_return(body: File.read(fixtures.join('style.css')))
end
%w[child_style windows_line_endings].each do |fixture|
context "when #{fixture}" do
let(:main_theme) { "#{fixture}.css" }
it 'returns the expected theme' do
parent = theme.parent_theme
expect(parent).to eql described_class.new(
'twentyfourteen', blog,
style_url: parent_url,
confidence: 100,
found_by: 'Parent Themes (Passive Detection)'
)
expect(parent.style_url).to eql parent_url
end
end
end
end
end
describe '#parent_themes' do
xit
end
describe '#==' do
before { stub_request(:get, /.*\.css\z/) }
context 'when default style' do
it 'returns true when equal' do
expect(theme == described_class.new(slug, blog, opts)).to be true
end
it 'returns false when not equal' do
expect(theme == described_class.new(slug, blog, opts.merge(style_url: 'spec.css'))).to be false
end
end
context 'when custom style' do
let(:opts) { super().merge(style_url: 'spec.css') }
it 'returns true when equal' do
expect(theme == described_class.new(slug, blog, opts.merge(style_url: 'spec.css'))).to be true
end
it 'returns false when not equal' do
expect(theme == described_class.new(slug, blog, opts.merge(style_url: 'spec2.css'))).to be false
end
end
end
end