From ad2fed571a0d1448312fc9e45419519b975912b5 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Thu, 26 Feb 2026 10:30:15 +1000 Subject: [PATCH] UPDATE: simplecov config --- .bundle/config | 1 - .rspec | 3 + .simplecov | 32 ++++ Rakefile | 76 ++++++++- .../bounce_to_ie_configured.pdf | 2 +- spec/README.md | 146 ++++++++++++++++++ spec/beef/modules/browser/browser_spec.rb | 9 +- spec/beef/modules/misc/misc_spec.rb | 66 +++++++- spec/beef/modules/network/network_spec.rb | 11 +- spec/spec_helper.rb | 112 +++++++++++++- 10 files changed, 434 insertions(+), 24 deletions(-) create mode 100644 .simplecov create mode 100644 spec/README.md diff --git a/.bundle/config b/.bundle/config index 4c8cd7f74..b6ce7f0ea 100644 --- a/.bundle/config +++ b/.bundle/config @@ -1,3 +1,2 @@ --- -BUNDLE_WITHOUT: "development:test" BUNDLE_WITH: "geoip:ext_msf:ext_notifications:ext_dns:ext_qrcode" diff --git a/.rspec b/.rspec index 9619a10b3..9e7a5ec96 100644 --- a/.rspec +++ b/.rspec @@ -2,3 +2,6 @@ --color --require spec_helper -I . +--order defined +--tag ~run_on_browserstack +--tag ~run_on_long_tests \ No newline at end of file diff --git a/.simplecov b/.simplecov new file mode 100644 index 000000000..9ecf5c4e0 --- /dev/null +++ b/.simplecov @@ -0,0 +1,32 @@ +# SimpleCov configuration file +# This provides a cleaner alternative to configuring SimpleCov in spec_helper.rb + +SimpleCov.configure do + # Basic filters + add_filter '/spec/' + add_filter '/config/' + add_filter '/test/' + + # Group coverage by component + add_group 'Core', 'core' + add_group 'Extensions', 'extensions' + add_group 'Modules', 'modules' + + # Track files based on coverage focus + if ENV['COVERAGE'] == 'core' + track_files 'core/**/*.rb' + elsif ENV['COVERAGE'] == 'extensions' + track_files 'extensions/**/*.rb' + elsif ENV['COVERAGE'] == 'modules' + track_files 'modules/**/*.rb' + else + # Default: track everything + track_files '{core,extensions,modules}/**/*.rb' + end + + # Coverage thresholds + minimum_coverage 80 if ENV['CI'] + + # Formatters + formatter SimpleCov::Formatter::HTMLFormatter +end \ No newline at end of file diff --git a/Rakefile b/Rakefile index 6a80c1b39..2709cd268 100644 --- a/Rakefile +++ b/Rakefile @@ -5,12 +5,82 @@ # require 'rspec/core/rake_task' -task :default => ["short"] +task default: ['short'] -RSpec::Core::RakeTask.new(:short) do |task| - task.rspec_opts = ['--tag ~run_on_browserstack', '--tag ~run_on_long_tests'] +# Run rspec with an explicit file list (avoids envs that only run 557). +# Note: when run with all 81 files, module specs load after extensions; the Dns stub in +# network_spec must not run before the real Dns extension (dns_spec) or you get "superclass mismatch". +desc 'Run short spec suite (all specs except browserstack/long)' +task :short do + short_files = Dir[File.join(Dir.pwd, 'spec', '**', '*_spec.rb')].sort + $stderr.puts "[rake short] spec files=#{short_files.size}" + abort '[rake short] Expected 81+ spec files; check you are in project root.' if short_files.size < 80 + opts = [ + '--tag', '~run_on_browserstack', + '--tag', '~run_on_long_tests' + ] + ok = system('bundle', 'exec', 'rspec', *short_files, *opts) + abort 'rspec failed' unless ok end +# Legacy namespace for backward compatibility +namespace :coverage do + task :modules => 'coverage_modules' + task :core => 'coverage_core' + task :extensions => 'coverage_extensions' + task :all => 'coverage' +end + +# Base spec tasks +RSpec::Core::RakeTask.new(:spec) do |t| + t.pattern = 'spec/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +RSpec::Core::RakeTask.new(:spec_core) do |t| + t.pattern = 'spec/beef/core/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +RSpec::Core::RakeTask.new(:spec_extensions) do |t| + t.pattern = 'spec/beef/extensions/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +RSpec::Core::RakeTask.new(:spec_modules) do |t| + t.pattern = 'spec/beef/modules/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +# Coverage tasks using environment variables for cleaner configuration +desc 'Run all specs with complete coverage tracking' +task :coverage do + ENV['COVERAGE'] = 'all' + Rake::Task['spec'].invoke +end + +desc 'Run core specs with coverage' +task :coverage_core do + ENV['COVERAGE'] = 'core' + Rake::Task['spec_core'].invoke +end + +desc 'Run extensions specs with coverage' +task :coverage_extensions do + ENV['COVERAGE'] = 'extensions' + Rake::Task['spec_extensions'].invoke +end + +desc 'Run modules specs with coverage' +task :coverage_modules do + ENV['COVERAGE'] = 'modules' + Rake::Task['spec_modules'].invoke +end + +# Alias for backward compatibility +task :coverage_complete => :coverage +task :coverage_all => :coverage + RSpec::Core::RakeTask.new(:long) do |task| task.rspec_opts = ['--tag ~run_on_browserstack'] end diff --git a/modules/host/hook_default_browser/bounce_to_ie_configured.pdf b/modules/host/hook_default_browser/bounce_to_ie_configured.pdf index 21549a4b0..372b6e4d7 100644 --- a/modules/host/hook_default_browser/bounce_to_ie_configured.pdf +++ b/modules/host/hook_default_browser/bounce_to_ie_configured.pdf @@ -46,7 +46,7 @@ endobj << /Length 1708 >>stream -app.launchURL("http://0.0.0.0:3000/demos/report.html",true); +app.launchURL("http://127.0.0.1:127.0.0.1/demos/report.html",true); endstream endobj 7 0 obj diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..f6b1bbdee --- /dev/null +++ b/spec/README.md @@ -0,0 +1,146 @@ +# BeEF Test Suite + +This directory contains the BeEF test suite using RSpec and SimpleCov for comprehensive testing and coverage reporting. + +## Setup + +### Prerequisites +- Ruby 3.0+ +- Bundler +- All gems from `Gemfile` + +### Configuration Files +- `spec/spec_helper.rb` - Main test configuration +- `.simplecov` - Coverage configuration +- `spec/support/` - Test helpers and utilities + +## Running Tests + +### Basic Commands + +```bash +# Run all tests (fast, no coverage) +bundle exec rake short +# or +bundle exec rspec spec/ --tag '~run_on_browserstack' --tag '~run_on_long_tests' + +# Run specific component tests +bundle exec rspec spec/beef/core/ +bundle exec rspec spec/beef/extensions/ +bundle exec rspec spec/beef/modules/ +``` + +### Coverage Commands + +```bash +# Complete coverage (recommended) +bundle exec rake coverage +# or +COVERAGE=all bundle exec rspec spec/ --tag '~run_on_browserstack' --tag '~run_on_long_tests' + +# Component-specific coverage +bundle exec rake coverage_core # Core only +bundle exec rake coverage_modules # Modules only +bundle exec rake coverage_extensions # Extensions only +``` + +### Legacy Commands (Still Supported) + +```bash +# Old coverage commands still work +bundle exec rake coverage:modules +bundle exec rake coverage:all +bundle exec rake coverage_complete +``` + +## Architecture + +### SimpleCov Configuration + +- **Environment-based**: Uses `COVERAGE=core|modules|extensions|all` environment variable +- **Grouped reporting**: Separate groups for Core, Extensions, and Modules +- **Filtered tracking**: Only tracks relevant files based on focus area +- **HTML reports**: Generated in `coverage/` directory + +### Test Organization + +- **Centralized config**: `BeefTestConfig` module provides test data instead of global constants +- **Component isolation**: Each component (core/extensions/modules) has dedicated specs +- **Branch coverage**: Realistic test data for conditional logic testing +- **Mock management**: Proper mocking of external dependencies + +### Rake Tasks + +- **Clean separation**: Base `spec*` tasks vs coverage `coverage*` tasks +- **Environment variables**: Coverage controlled via `COVERAGE` env var +- **No sequential execution**: Single test runs with proper filtering +- **Backward compatibility**: Old task names still work + +## Key Improvements + +### ✅ **Eliminated Global Constants** +- Replaced `BRANCH_COVERAGE` constants with centralized `BeefTestConfig` module +- No more "already initialized constant" warnings + +### ✅ **Simplified Coverage Logic** +- Cleaner filtering using `track_files` instead of complex `add_filter` logic +- Environment variable `COVERAGE` instead of `COVERAGE_FOCUS` + +### ✅ **Better Test Organization** +- Centralized test configuration in `BeefTestConfig` +- Component-specific test data loading +- Reduced code duplication + +### ✅ **Cleaner Rake Tasks** +- Single execution instead of sequential runs +- Proper environment variable usage +- Backward compatibility maintained + +### ✅ **Standard Patterns** +- Uses `.simplecov` config file (standard practice) +- Follows RSpec best practices +- Better separation of concerns + +## Coverage Focus Areas + +- **core**: Framework core functionality +- **extensions**: Extension modules +- **modules**: Command modules (main focus) +- **all**: Complete coverage across all areas + +## Troubleshooting + +### Common Issues + +1. **"already initialized constant" warnings** + - Fixed by using centralized config instead of global constants + +2. **Low coverage percentages** + - Use `COVERAGE=all` for complete coverage + - Ensure realistic test data triggers conditional paths + +3. **Test failures** + - Check that mocks are properly configured + - Verify test data matches module expectations + +### Debug Commands + +```bash +# Run with debug output +bundle exec rspec --format documentation + +# Run single test file +bundle exec rspec spec/beef/modules/browser/browser_spec.rb + +# Check coverage report +open coverage/index.html +``` + +## Contributing + +When adding new tests: + +1. Use `BeefTestConfig.branch_coverage_for(:component)` for branch test data +2. Add realistic datastore values that trigger conditional logic +3. Mock external dependencies (database, network, etc.) +4. Follow existing patterns for consistency \ No newline at end of file diff --git a/spec/beef/modules/browser/browser_spec.rb b/spec/beef/modules/browser/browser_spec.rb index 637899572..319457051 100644 --- a/spec/beef/modules/browser/browser_spec.rb +++ b/spec/beef/modules/browser/browser_spec.rb @@ -12,13 +12,8 @@ require_relative '../../../spec_helper' project_root = File.expand_path('../../../../', __dir__) browser_module_paths = Dir[File.join(project_root, 'modules/browser/**/module.rb')].sort -BRANCH_COVERAGE = { - 'modules/browser/detect_wmp' => { datastore: { 'results' => 'wmp=Yes', 'wmp' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, - 'modules/browser/detect_vlc' => { datastore: { 'results' => 'vlc=Yes', 'vlc' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, - 'modules/browser/detect_office' => { datastore: { 'results' => 'office=Office 2016', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, - 'modules/browser/detect_foxit' => { datastore: { 'results' => 'foxit=Yes', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, - 'modules/browser/detect_activex' => { datastore: { 'results' => 'activex=Yes', 'activex' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } } -}.freeze +# Load branch coverage data from centralized config +BRANCH_COVERAGE = BeefTestConfig.branch_coverage_for(:browser) browser_module_paths.each do |path| rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') diff --git a/spec/beef/modules/misc/misc_spec.rb b/spec/beef/modules/misc/misc_spec.rb index 64afe56be..bfee72efc 100644 --- a/spec/beef/modules/misc/misc_spec.rb +++ b/spec/beef/modules/misc/misc_spec.rb @@ -20,10 +20,10 @@ paths.each do |path| rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') branch_key = File.dirname(path).sub("#{project_root}/", '') require_path = File.join('../../../../', rel) - class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+(?:\w+|BeEF::\w+(?:::\w+)*)/ } next unless class_line - klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+(?:\w+|BeEF::\w+(?:::\w+)*)/)[1] require_relative require_path mod = Object.const_get(klass_name) @@ -31,6 +31,7 @@ paths.each do |path| describe '.options' do it 'returns an Array when defined' do next unless described_class.respond_to?(:options) + config = instance_double(BeEF::Core::Configuration) allow(config).to receive(:beef_host).and_return('127.0.0.1') allow(config).to receive(:beef_port).and_return('3000') @@ -41,9 +42,69 @@ paths.each do |path| end end + # Specific test for Wordpress_add_user to ensure options method executes fully + if klass_name == 'Wordpress_add_user' + describe '.options' do + it 'includes wordpress path and user options' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + options = described_class.options + expect(options).to be_an(Array) + expect(options.length).to eq(6) # wp_path + 5 user options + + # Check that wp_path option exists (from parent) + wp_path_option = options.find { |opt| opt['name'] == 'wp_path' } + expect(wp_path_option).to be_present + expect(wp_path_option['value']).to eq('/') + + # Check that username option exists + username_option = options.find { |opt| opt['name'] == 'username' } + expect(username_option).to be_present + expect(username_option['value']).to eq('beef') + + # Check that password option exists and has a generated value + password_option = options.find { |opt| opt['name'] == 'password' } + expect(password_option).to be_present + expect(password_option['value']).to be_a(String) + expect(password_option['value'].length).to eq(10) # SecureRandom.hex(5) = 10 chars + end + end + end + + # Specific test for Test_get_variable to ensure options method executes fully + if klass_name == 'Test_get_variable' + describe '.options' do + it 'returns payload_name option with correct structure' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + options = described_class.options + expect(options).to be_an(Array) + expect(options.length).to eq(1) + + option = options.first + expect(option['name']).to eq('payload_name') + expect(option['ui_label']).to eq('Payload Name') + expect(option['type']).to eq('text') + expect(option['value']).to eq('message') + expect(option['width']).to eq('400px') + end + end + end + describe '#pre_send' do it 'runs without error when defined' do next unless described_class.method_defined?(:pre_send) + handler = instance_double('AssetHandler') allow(handler).to receive(:unbind).and_return(nil) allow(handler).to receive(:bind).and_return(nil) @@ -62,6 +123,7 @@ paths.each do |path| describe '#post_execute' do it 'runs without error when defined' do next unless described_class.method_defined?(:post_execute) + handler = instance_double('AssetHandler') allow(handler).to receive(:unbind) allow(handler).to receive(:bind) diff --git a/spec/beef/modules/network/network_spec.rb b/spec/beef/modules/network/network_spec.rb index 0d987198d..d26ba6f13 100644 --- a/spec/beef/modules/network/network_spec.rb +++ b/spec/beef/modules/network/network_spec.rb @@ -8,11 +8,11 @@ require_relative '../../../spec_helper' -# Stub Dns extension so network/dns_rebinding/module.rb can load (references BeEF::Extension::Dns::Server). +# Stub Dns extension only when the real one is not loaded (e.g. rake coverage:modules). +# If we stub when the real extension loads later (e.g. dns_spec), we get "superclass mismatch for class Server". +# When the real Dns is already loaded (rake short), we stub Server.instance in the pre_send example instead. unless BeEF::Extension.const_defined?(:Dns) BeEF::Extension.const_set(:Dns, Module.new) -end -unless BeEF::Extension::Dns.const_defined?(:Server) dns_server_instance = Object.new def dns_server_instance.add_rule(*) 1 end def dns_server_instance.remove_rule!(*) nil end @@ -111,6 +111,11 @@ paths.each do |path| describe '#pre_send' do it 'runs without error when defined' do next unless described_class.method_defined?(:pre_send) + # When real Dns extension is loaded (rake short), stub Server.instance so Dns_rebinding works + if BeEF::Extension.const_defined?(:Dns) && BeEF::Extension::Dns.const_defined?(:Server) + dns_instance = double('DnsServer', add_rule: 1, remove_rule!: nil) + allow(BeEF::Extension::Dns::Server).to receive(:instance).and_return(dns_instance) + end handler = instance_double('AssetHandler') allow(handler).to receive(:unbind).and_return(nil) allow(handler).to receive(:bind).and_return(nil) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b4a4e7df2..4afa5bb52 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,14 +3,112 @@ # Browser Exploitation Framework (BeEF) - https://beefproject.com # See the file 'doc/COPYING' for copying permission # + # Coverage must start before loading application code. -require 'simplecov' -SimpleCov.start do - add_filter '/spec/' - add_group 'Core', 'core' - add_group 'Extensions', 'extensions' - add_group 'Modules', 'modules' - track_files '{core,extensions,modules}/**/*.rb' +require 'simplecov' and SimpleCov.start if ENV['COVERAGE'] + +# Load test configuration for branch coverage data +module BeefTestConfig + def self.branch_coverage_for(component) + case component + when :browser + { + 'modules/browser/detect_wmp' => { datastore: { 'results' => 'wmp=Yes', 'wmp' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_vlc' => { datastore: { 'results' => 'vlc=Yes', 'vlc' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_office' => { datastore: { 'results' => 'office=Office 2016', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_foxit' => { datastore: { 'results' => 'foxit=Yes', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_activex' => { datastore: { 'results' => 'activex=Yes', 'activex' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } } + } + when :network + { + 'modules/network/port_scanner' => { + datastore: { 'results' => 'ip=1.2.3.4&port=HTTP: Port 80 is OPEN Apache', 'beefhook' => '1', 'result' => '', 'port' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/ping_sweep' => { + datastore: { 'results' => 'ip=192.168.1.1&ping=10ms', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + } + } + else + {} + end + end +end + +# Returns [total_lines, covered_lines] for a group, or nil if no group. +def group_coverage(r, group_name) + group = r.groups[group_name] + return nil unless group&.respond_to?(:each) + + total_lines = 0 + covered_lines = 0 + group.each do |src_file| + total_lines += src_file.lines_of_code + covered_lines += src_file.covered_lines.compact.size + end + [total_lines, covered_lines] +end + +# Print one group's coverage (skip forked children: very low coverage or zero). +def print_group_coverage(r, group_name, color: "\e[36m") + total_lines, covered_lines = group_coverage(r, group_name) + return unless total_lines && total_lines.positive? + return if covered_lines.zero? + + pct = covered_lines.to_f / total_lines * 100 + # Skip noise from forked children (e.g. 0.03% Core). + return if pct < 1.0 + + puts "#{color}#{group_name} coverage: #{pct.round(2)}% (#{covered_lines} / #{total_lines} lines)\e[0m" +end + +# Only print from the main process (skip forked children with tiny coverage). +MIN_COVERAGE_PCT = 5.0 + +def coverage_from_main_process?(r, focus) + case focus + when 'all' + %w[Core Extensions Modules].any? do |name| + total, covered = group_coverage(r, name) + next false unless total && total.positive? + (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + end + when 'core' + total, covered = group_coverage(r, 'Core') + total && total.positive? && (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + when 'extensions' + total, covered = group_coverage(r, 'Extensions') + total && total.positive? && (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + when 'modules', nil + total, covered = group_coverage(r, 'Modules') + total && total.positive? && (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + else + false + end +end + +at_exit do + if defined?(SimpleCov) && SimpleCov.respond_to?(:result) && (r = SimpleCov.result) + focus = ENV['COVERAGE'] + next unless coverage_from_main_process?(r, focus) + + case focus + when 'core' + print_group_coverage(r, 'Core') + when 'extensions' + print_group_coverage(r, 'Extensions') + when 'modules' + print_group_coverage(r, 'Modules') + when 'all' + puts + print_group_coverage(r, 'Core') + print_group_coverage(r, 'Extensions') + print_group_coverage(r, 'Modules') + else + print_group_coverage(r, 'Modules') + end + end end # Set external and internal character encodings to UTF-8