Merge pull request #3518 from jake-the-dev/tests/improve-core-test-coverage

Tests/improve core test coverage
This commit is contained in:
zinduolis
2026-05-22 16:04:49 +10:00
committed by GitHub
19 changed files with 1438 additions and 29 deletions
+138
View File
@@ -307,4 +307,142 @@ RSpec.describe BeEF::Filters do
end
end
end
describe '.is_valid_ip?' do
it 'returns false for nil, empty, or non-string' do
expect(BeEF::Filters.is_valid_ip?(nil)).to be(false)
expect(BeEF::Filters.is_valid_ip?('')).to be(false)
end
it 'returns true for valid IPv4' do
expect(BeEF::Filters.is_valid_ip?('127.0.0.1')).to be(true)
expect(BeEF::Filters.is_valid_ip?('192.168.1.1')).to be(true)
expect(BeEF::Filters.is_valid_ip?('10.0.0.1')).to be(true)
expect(BeEF::Filters.is_valid_ip?('0.0.0.0')).to be(true)
end
it 'returns false for invalid IPv4' do
expect(BeEF::Filters.is_valid_ip?('256.1.1.1')).to be(false)
expect(BeEF::Filters.is_valid_ip?('1.2.3')).to be(false)
expect(BeEF::Filters.is_valid_ip?('not.an.ip')).to be(false)
end
it 'accepts :ipv4 version' do
expect(BeEF::Filters.is_valid_ip?('127.0.0.1', :ipv4)).to be(true)
expect(BeEF::Filters.is_valid_ip?('256.1.1.1', :ipv4)).to be(false)
end
it 'accepts :both version (default)' do
expect(BeEF::Filters.is_valid_ip?('127.0.0.1')).to be(true)
end
end
describe '.is_valid_private_ip?' do
it 'returns false when ip is not valid' do
expect(BeEF::Filters.is_valid_private_ip?(nil)).to be(false)
expect(BeEF::Filters.is_valid_private_ip?('8.8.8.8')).to be(false)
end
it 'returns true for 127.x (localhost)' do
expect(BeEF::Filters.is_valid_private_ip?('127.0.0.1')).to be(true)
end
it 'returns true for 192.168.x' do
expect(BeEF::Filters.is_valid_private_ip?('192.168.1.1')).to be(true)
end
it 'returns true for 10.x' do
expect(BeEF::Filters.is_valid_private_ip?('10.0.0.1')).to be(true)
end
it 'returns false for public IPv4' do
expect(BeEF::Filters.is_valid_private_ip?('8.8.8.8')).to be(false)
end
end
describe '.is_valid_port?' do
it 'returns true for valid port range' do
expect(BeEF::Filters.is_valid_port?(1)).to be(true)
expect(BeEF::Filters.is_valid_port?('80')).to be(true)
expect(BeEF::Filters.is_valid_port?(65535)).to be(true)
end
it 'returns false for 0 or negative' do
expect(BeEF::Filters.is_valid_port?(0)).to be(false)
expect(BeEF::Filters.is_valid_port?('0')).to be(false)
end
it 'returns false for port above 65535' do
expect(BeEF::Filters.is_valid_port?(65536)).to be(false)
end
end
describe '.is_valid_domain?' do
it 'returns false for nil or empty' do
expect(BeEF::Filters.is_valid_domain?(nil)).to be(false)
expect(BeEF::Filters.is_valid_domain?('')).to be(false)
end
it 'returns true for valid domain format' do
expect(BeEF::Filters.is_valid_domain?('example.com')).to be(true)
expect(BeEF::Filters.is_valid_domain?('sub.example.co.uk')).to be(true)
end
it 'returns false for invalid domain format' do
expect(BeEF::Filters.is_valid_domain?('no-tld')).to be(false)
expect(BeEF::Filters.is_valid_domain?('.leading')).to be(false)
end
end
describe '.has_valid_browser_details_chars?' do
it 'returns false for nil or empty' do
expect(BeEF::Filters.has_valid_browser_details_chars?(nil)).to be(false)
expect(BeEF::Filters.has_valid_browser_details_chars?('')).to be(false)
end
it 'returns false when string only has allowed chars' do
# Method returns true when regex matches (invalid char found); false when only valid chars
expect(BeEF::Filters.has_valid_browser_details_chars?('abc')).to be(false)
expect(BeEF::Filters.has_valid_browser_details_chars?('a-b (c)')).to be(false)
end
it 'returns true when string contains disallowed character' do
expect(BeEF::Filters.has_valid_browser_details_chars?('ab@c')).to be(true)
end
end
describe '.has_valid_base_chars?' do
it 'returns false for nil or empty' do
expect(BeEF::Filters.has_valid_base_chars?(nil)).to be(false)
expect(BeEF::Filters.has_valid_base_chars?('')).to be(false)
end
it 'returns true when string only has printable (and registered symbol)' do
expect(BeEF::Filters.has_valid_base_chars?('abc')).to be(true)
expect(BeEF::Filters.has_valid_base_chars?('Hello 123')).to be(true)
end
it 'returns false when string has non-printable character' do
expect(BeEF::Filters.has_valid_base_chars?("ab\x00c")).to be(false)
end
end
describe '.is_valid_yes_no?' do
it 'returns true for Yes and No (case insensitive)' do
expect(BeEF::Filters.is_valid_yes_no?('Yes')).to be(true)
expect(BeEF::Filters.is_valid_yes_no?('No')).to be(true)
expect(BeEF::Filters.is_valid_yes_no?('yes')).to be(true)
expect(BeEF::Filters.is_valid_yes_no?('no')).to be(true)
end
it 'returns false for other values' do
expect(BeEF::Filters.is_valid_yes_no?('')).to be(false)
expect(BeEF::Filters.is_valid_yes_no?('maybe')).to be(false)
expect(BeEF::Filters.is_valid_yes_no?('1')).to be(false)
end
it 'returns false when string has non-printable character' do
expect(BeEF::Filters.is_valid_yes_no?("Yes\x00")).to be(false)
end
end
end
+13 -2
View File
@@ -11,6 +11,10 @@ RSpec.describe BeEF::Filters do
expect(BeEF::Filters.is_valid_path_info?("\x00")).to be(false)
expect(BeEF::Filters.is_valid_path_info?(nil)).to be(false)
end
it 'returns false when argument is not a String' do
expect(BeEF::Filters.is_valid_path_info?(123)).to be(false)
end
end
describe '.is_valid_hook_session_id?' do
@@ -43,15 +47,22 @@ RSpec.describe BeEF::Filters do
end
describe '.has_valid_param_chars?' do
it 'false' do
it 'returns false for nil, empty, or invalid chars' do
chars = [nil, '', '+']
chars.each do |c|
expect(BeEF::Filters.has_valid_param_chars?(c)).to be(false)
end
end
it 'true' do
it 'returns true for word, underscore, and colon' do
expect(BeEF::Filters.has_valid_param_chars?('A')).to be(true)
expect(BeEF::Filters.has_valid_param_chars?('key_name')).to be(true)
expect(BeEF::Filters.has_valid_param_chars?('a:1')).to be(true)
end
it 'returns false for string with spaces or special chars' do
expect(BeEF::Filters.has_valid_param_chars?('a b')).to be(false)
expect(BeEF::Filters.has_valid_param_chars?('a-b')).to be(false)
end
end
end
+41
View File
@@ -0,0 +1,41 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::HBManager do
describe '.get_by_session' do
it 'returns the hooked browser when session exists' do
hb = BeEF::Core::Models::HookedBrowser.create!(session: 'hb_session_123', ip: '127.0.0.1')
result = described_class.get_by_session('hb_session_123')
expect(result).to eq(hb)
expect(result.session).to eq('hb_session_123')
end
it 'returns nil when no hooked browser has the session' do
result = described_class.get_by_session('nonexistent_session')
expect(result).to be_nil
end
end
describe '.get_by_id' do
it 'returns the hooked browser when id exists' do
hb = BeEF::Core::Models::HookedBrowser.create!(session: 'hb_by_id', ip: '127.0.0.1')
result = described_class.get_by_id(hb.id)
expect(result).to eq(hb)
expect(result.id).to eq(hb.id)
end
it 'raises when id does not exist' do
expect { described_class.get_by_id(999_999) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
+196
View File
@@ -0,0 +1,196 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Console::Banners do
let(:config) { BeEF::Core::Configuration.instance }
before do
allow(described_class).to receive(:print_info)
allow(described_class).to receive(:print_more)
end
describe '.print_welcome_msg' do
it 'calls print_info with version from config' do
allow(config).to receive(:get).with('beef.version').and_return('1.0.0')
described_class.print_welcome_msg
expect(described_class).to have_received(:print_info).with('Browser Exploitation Framework (BeEF) 1.0.0')
end
it 'calls print_more with project links' do
allow(config).to receive(:get).with('beef.version').and_return('1.0.0')
described_class.print_welcome_msg
expect(described_class).to have_received(:print_more).with(a_string_including('@beefproject'))
expect(described_class).to have_received(:print_more).with(a_string_including('beefproject.com'))
expect(described_class).to have_received(:print_more).with(a_string_including('github.com/beefproject'))
end
it 'calls print_info with project creator' do
allow(config).to receive(:get).with('beef.version').and_return('1.0.0')
described_class.print_welcome_msg
expect(described_class).to have_received(:print_info).with(a_string_including('Wade Alcorn'))
expect(described_class).to have_received(:print_info).with(a_string_including('@WadeAlcorn'))
end
end
describe '.print_network_interfaces_count' do
it 'uses config local_host and sets interfaces when host is 0.0.0.0' do
allow(config).to receive(:local_host).and_return('0.0.0.0')
mock_addrs = [double('Addr', ip_address: '127.0.0.1', ipv4?: true), double('Addr', ip_address: '192.168.1.1', ipv4?: true)]
allow(Socket).to receive(:ip_address_list).and_return(mock_addrs)
described_class.print_network_interfaces_count
expect(described_class.interfaces).to eq(['127.0.0.1', '192.168.1.1'])
expect(described_class).to have_received(:print_info).with('2 network interfaces were detected.')
end
it 'sets single interface when host is not 0.0.0.0' do
allow(config).to receive(:local_host).and_return('192.168.1.1')
described_class.print_network_interfaces_count
expect(described_class.interfaces).to eq(['192.168.1.1'])
expect(described_class).to have_received(:print_info).with('1 network interfaces were detected.')
end
end
describe '.print_loaded_extensions' do
it 'calls print_info with count from Extensions.get_loaded' do
allow(BeEF::Extensions).to receive(:get_loaded).and_return({ 'AdminUI' => { 'name' => 'Admin UI' }, 'DNS' => { 'name' => 'DNS' } })
described_class.print_loaded_extensions
expect(described_class).to have_received(:print_info).with('2 extensions enabled:')
expect(described_class).to have_received(:print_more).with(a_string_including('Admin UI'))
expect(described_class).to have_received(:print_more).with(a_string_including('DNS'))
end
it 'handles empty extensions' do
allow(BeEF::Extensions).to receive(:get_loaded).and_return({})
described_class.print_loaded_extensions
expect(described_class).to have_received(:print_info).with('0 extensions enabled:')
end
end
describe '.print_loaded_modules' do
it 'calls print_info with count from Modules.get_enabled' do
enabled = double('Relation', count: 42)
allow(BeEF::Modules).to receive(:get_enabled).and_return(enabled)
described_class.print_loaded_modules
expect(described_class).to have_received(:print_info).with('42 modules enabled.')
end
end
describe '.print_ascii_art' do
it 'reads and puts file content when beef.ascii exists' do
allow(File).to receive(:exist?).with('core/main/console/beef.ascii').and_return(true)
io = StringIO.new("BEEF\nASCII\n")
allow(File).to receive(:open).with('core/main/console/beef.ascii', 'r').and_yield(io)
allow(described_class).to receive(:puts)
described_class.print_ascii_art
expect(described_class).to have_received(:puts).with("BEEF\n")
expect(described_class).to have_received(:puts).with("ASCII\n")
end
it 'does nothing when beef.ascii does not exist' do
allow(File).to receive(:exist?).with('core/main/console/beef.ascii').and_return(false)
expect(File).not_to receive(:open)
described_class.print_ascii_art
end
end
describe '.print_network_interfaces_routes' do
before do
described_class.interfaces = ['127.0.0.1', '192.168.1.1']
end
it 'prints hook and UI URL for each interface when admin_ui enabled' do
allow(config).to receive(:local_proto).and_return('http')
allow(config).to receive(:hook_file_path).and_return('/hook.js')
allow(config).to receive(:get).with('beef.extension.admin_ui.enable').and_return(true)
allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
allow(config).to receive(:local_port).and_return(3000)
allow(config).to receive(:public_enabled?).and_return(false)
described_class.print_network_interfaces_routes
expect(described_class).to have_received(:print_info).with('running on network interface: 127.0.0.1')
expect(described_class).to have_received(:print_info).with('running on network interface: 192.168.1.1')
expect(described_class).to have_received(:print_more).with(a_string_matching(%r{Hook URL: http://127\.0\.0\.1:3000/hook\.js}))
expect(described_class).to have_received(:print_more).at_least(:twice).with(a_string_matching(%r{UI URL:.*/ui/panel}))
end
it 'omits UI URL when admin_ui disabled' do
allow(config).to receive(:local_proto).and_return('http')
allow(config).to receive(:hook_file_path).and_return('/hook.js')
allow(config).to receive(:get).with('beef.extension.admin_ui.enable').and_return(false)
allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
allow(config).to receive(:local_port).and_return(3000)
allow(config).to receive(:public_enabled?).and_return(false)
described_class.print_network_interfaces_routes
expect(described_class).to have_received(:print_more).with("Hook URL: http://127.0.0.1:3000/hook.js\n")
expect(described_class).to have_received(:print_more).with("Hook URL: http://192.168.1.1:3000/hook.js\n")
end
it 'prints public hook and UI when public_enabled?' do
allow(config).to receive(:local_proto).and_return('http')
allow(config).to receive(:hook_file_path).and_return('/hook.js')
allow(config).to receive(:get).with('beef.extension.admin_ui.enable').and_return(true)
allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
allow(config).to receive(:local_port).and_return(3000)
allow(config).to receive(:public_enabled?).and_return(true)
allow(config).to receive(:hook_url).and_return('http://public.example.com/hook.js')
allow(config).to receive(:beef_url_str).and_return('http://public.example.com')
described_class.print_network_interfaces_routes
expect(described_class).to have_received(:print_info).with('Public:')
expect(described_class).to have_received(:print_more).with(a_string_including('http://public.example.com/hook.js'))
expect(described_class).to have_received(:print_more).at_least(:once).with(a_string_including('/ui/panel'))
end
end
describe '.print_websocket_servers' do
it 'prints WebSocket server line with host, port and timer' do
allow(config).to receive(:beef_host).and_return('0.0.0.0')
allow(config).to receive(:get).with('beef.http.websocket.ws_poll_timeout').and_return(5)
allow(config).to receive(:get).with('beef.http.websocket.port').and_return(61_985)
allow(config).to receive(:get).with('beef.http.websocket.secure').and_return(false)
described_class.print_websocket_servers
expect(described_class).to have_received(:print_info).with('Starting WebSocket server ws://0.0.0.0:61985 [timer: 5]')
end
it 'prints WebSocketSecure server when secure enabled' do
allow(config).to receive(:beef_host).and_return('0.0.0.0')
allow(config).to receive(:get).with('beef.http.websocket.ws_poll_timeout').and_return(10)
allow(config).to receive(:get).with('beef.http.websocket.port').and_return(61_985)
allow(config).to receive(:get).with('beef.http.websocket.secure').and_return(true)
allow(config).to receive(:get).with('beef.http.websocket.secure_port').and_return(61_986)
described_class.print_websocket_servers
expect(described_class).to have_received(:print_info).with('Starting WebSocket server ws://0.0.0.0:61985 [timer: 10]')
expect(described_class).to have_received(:print_info).with(a_string_matching(/WebSocketSecure.*wss:.*61986.*timer: 10/))
end
end
describe '.print_http_proxy' do
it 'prints proxy address and port from config' do
allow(config).to receive(:get).with('beef.extension.proxy.address').and_return('127.0.0.1')
allow(config).to receive(:get).with('beef.extension.proxy.port').and_return(8080)
described_class.print_http_proxy
expect(described_class).to have_received(:print_info).with('HTTP Proxy: http://127.0.0.1:8080')
end
end
describe '.print_dns' do
it 'does not raise when DNS config is not set' do
expect { described_class.print_dns }.not_to raise_error
end
end
end
@@ -0,0 +1,126 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Console::CommandLine do
DEFAULT_OPTIONS = {
verbose: false,
resetdb: false,
ascii_art: false,
ext_config: '',
port: '',
ws_port: '',
update_disabled: false,
update_auto: false
}.freeze
def reset_commandline_state
described_class.instance_variable_set(:@already_parsed, false)
described_class.instance_variable_set(:@options, DEFAULT_OPTIONS.dup)
end
before do
reset_commandline_state
end
describe '.parse' do
it 'returns default options when ARGV is empty' do
original_argv = ARGV.dup
ARGV.replace([])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:verbose]).to be false
expect(result[:resetdb]).to be false
expect(result[:ext_config]).to eq('')
expect(result[:port]).to eq('')
end
it 'sets verbose when -v is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-v])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:verbose]).to be true
end
it 'sets resetdb when -x is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-x])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:resetdb]).to be true
end
it 'sets ascii_art when -a is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-a])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:ascii_art]).to be true
end
it 'sets ext_config when -c FILE is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-c custom.yaml])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:ext_config]).to eq('custom.yaml')
end
it 'sets port when -p PORT is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-p 9090])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:port]).to eq('9090')
end
it 'sets ws_port when -w WS_PORT is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-w 61985])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:ws_port]).to eq('61985')
end
it 'sets update_disabled when -d is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-d])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:update_disabled]).to be true
end
it 'sets update_auto when -u is given' do
original_argv = ARGV.dup
ARGV.replace(%w[-u])
result = described_class.parse
ARGV.replace(original_argv)
expect(result[:update_auto]).to be true
end
it 'returns cached options on second parse' do
original_argv = ARGV.dup
ARGV.replace([])
first = described_class.parse
ARGV.replace(%w[-v -x])
second = described_class.parse
ARGV.replace(original_argv)
expect(second).to eq(first)
expect(second[:verbose]).to be false
end
it 'prints and exits on invalid option' do
original_argv = ARGV.dup
ARGV.replace(%w[--invalid-option])
allow(Kernel).to receive(:puts).with(/Invalid command line option/)
allow(Kernel).to receive(:exit).with(1) { raise SystemExit.new(1) }
expect { described_class.parse }.to raise_error(SystemExit)
ARGV.replace(original_argv)
end
end
end
@@ -0,0 +1,60 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Constants::Browsers do
describe 'constants' do
it 'defines short browser codes' do
expect(described_class::FF).to eq('FF')
expect(described_class::C).to eq('C')
expect(described_class::IE).to eq('IE')
expect(described_class::S).to eq('S')
expect(described_class::ALL).to eq('ALL')
expect(described_class::UNKNOWN).to eq('UN')
end
it 'defines friendly names' do
expect(described_class::FRIENDLY_FF_NAME).to eq('Firefox')
expect(described_class::FRIENDLY_C_NAME).to eq('Chrome')
expect(described_class::FRIENDLY_UN_NAME).to eq('UNKNOWN')
end
end
describe '.friendly_name' do
it 'returns Firefox for FF' do
expect(described_class.friendly_name(described_class::FF)).to eq('Firefox')
end
it 'returns Chrome for C' do
expect(described_class.friendly_name(described_class::C)).to eq('Chrome')
end
it 'returns Internet Explorer for IE' do
expect(described_class.friendly_name(described_class::IE)).to eq('Internet Explorer')
end
it 'returns Safari for S' do
expect(described_class.friendly_name(described_class::S)).to eq('Safari')
end
it 'returns MSEdge for E' do
expect(described_class.friendly_name(described_class::E)).to eq('MSEdge')
end
it 'returns UNKNOWN for UN' do
expect(described_class.friendly_name(described_class::UNKNOWN)).to eq('UNKNOWN')
end
it 'returns nil for unknown browser code' do
expect(described_class.friendly_name('XX')).to be_nil
end
it 'returns nil for nil' do
expect(described_class.friendly_name(nil)).to be_nil
end
end
end
@@ -0,0 +1,18 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Constants::CommandModule do
describe 'constants' do
it 'defines verified working status values' do
expect(described_class::VERIFIED_WORKING).to eq(0)
expect(described_class::VERIFIED_UNKNOWN).to eq(1)
expect(described_class::VERIFIED_USER_NOTIFY).to eq(2)
expect(described_class::VERIFIED_NOT_WORKING).to eq(3)
end
end
end
@@ -0,0 +1,69 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Constants::Hardware do
describe 'constants' do
it 'defines hardware UA strings and image paths' do
expect(described_class::HW_IPHONE_UA_STR).to eq('iPhone')
expect(described_class::HW_IPAD_UA_STR).to eq('iPad')
expect(described_class::HW_BLACKBERRY_UA_STR).to eq('BlackBerry')
expect(described_class::HW_ALL_UA_STR).to eq('All')
expect(described_class::HW_UNKNOWN_IMG).to eq('pc.png')
end
end
describe '.match_hardware' do
it 'returns iPhone for iphone-like strings' do
expect(described_class.match_hardware('iPhone')).to eq('iPhone')
expect(described_class.match_hardware('iPhone OS')).to eq('iPhone')
end
it 'returns iPad for ipad-like strings' do
expect(described_class.match_hardware('iPad')).to eq('iPad')
end
it 'returns iPod for ipod-like strings' do
expect(described_class.match_hardware('iPod')).to eq('iPod')
end
it 'returns BlackBerry for blackberry-like strings' do
expect(described_class.match_hardware('BlackBerry')).to eq('BlackBerry')
end
it 'returns Windows Phone for windows phone-like strings' do
expect(described_class.match_hardware('Windows Phone')).to eq('Windows Phone')
end
it 'returns Kindle for kindle-like strings' do
expect(described_class.match_hardware('Kindle')).to eq('Kindle')
end
it 'returns Nokia for nokia-like strings' do
expect(described_class.match_hardware('Nokia')).to eq('Nokia')
end
it 'returns HTC for htc-like strings' do
expect(described_class.match_hardware('HTC')).to eq('HTC')
end
it 'returns Nexus for google-like strings' do
expect(described_class.match_hardware('Google Nexus')).to eq('Nexus')
end
it 'is case insensitive' do
expect(described_class.match_hardware('IPHONE')).to eq('iPhone')
expect(described_class.match_hardware('ipad')).to eq('iPad')
expect(described_class.match_hardware('BLACKBERRY')).to eq('BlackBerry')
end
it 'returns ALL for unknown hardware strings' do
expect(described_class.match_hardware('UnknownDevice')).to eq('ALL')
expect(described_class.match_hardware('')).to eq('ALL')
end
end
end
+73
View File
@@ -0,0 +1,73 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Constants::Os do
describe 'constants' do
it 'defines OS UA strings and image paths' do
expect(described_class::OS_WINDOWS_UA_STR).to eq('Windows')
expect(described_class::OS_LINUX_UA_STR).to eq('Linux')
expect(described_class::OS_MAC_UA_STR).to eq('Mac')
expect(described_class::OS_ANDROID_UA_STR).to eq('Android')
expect(described_class::OS_ALL_UA_STR).to eq('All')
expect(described_class::OS_UNKNOWN_IMG).to eq('unknown.png')
end
end
describe '.match_os' do
it 'returns Windows for win-like strings' do
expect(described_class.match_os('Windows')).to eq('Windows')
expect(described_class.match_os('Windows NT')).to eq('Windows')
expect(described_class.match_os('Win32')).to eq('Windows')
end
it 'returns Linux for lin-like strings' do
expect(described_class.match_os('Linux')).to eq('Linux')
expect(described_class.match_os('Lin')).to eq('Linux')
end
it 'returns Mac for os x, osx, mac-like strings' do
expect(described_class.match_os('Mac OS X')).to eq('Mac')
expect(described_class.match_os('OSX')).to eq('Mac')
expect(described_class.match_os('Macintosh')).to eq('Mac')
end
it 'returns iOS for iphone, ipad, ipod' do
expect(described_class.match_os('iPhone')).to eq('iOS')
expect(described_class.match_os('iPad')).to eq('iOS')
expect(described_class.match_os('iPod')).to eq('iOS')
expect(described_class.match_os('iOS')).to eq('iOS')
end
it 'returns Android for android-like strings' do
expect(described_class.match_os('Android')).to eq('Android')
end
it 'returns BlackBerry for blackberry-like strings' do
expect(described_class.match_os('BlackBerry')).to eq('BlackBerry')
end
it 'returns QNX for qnx-like strings' do
expect(described_class.match_os('QNX')).to eq('QNX')
end
it 'returns SunOS for sun-like strings' do
expect(described_class.match_os('SunOS')).to eq('SunOS')
end
it 'is case insensitive' do
expect(described_class.match_os('WINDOWS')).to eq('Windows')
expect(described_class.match_os('linux')).to eq('Linux')
expect(described_class.match_os('ANDROID')).to eq('Android')
end
it 'returns ALL for unknown OS strings' do
expect(described_class.match_os('UnknownOS')).to eq('ALL')
expect(described_class.match_os('')).to eq('ALL')
end
end
end
+1
View File
@@ -59,6 +59,7 @@ RSpec.describe 'BeEF::Core::Crypto' do
it 'raises TypeError for invalid inputs' do
expect { BeEF::Core::Crypto.random_hex_string('invalid') }.to raise_error(TypeError)
expect { BeEF::Core::Crypto.random_hex_string(0) }.to raise_error(TypeError, /Invalid length/)
expect { BeEF::Core::Crypto.random_hex_string(-1) }.to raise_error(TypeError, /Invalid length/)
end
end
@@ -412,5 +412,158 @@ RSpec.describe BeEF::Core::Handlers::BrowserDetails do
expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'network.proxy.server', 'proxy.example.com')
described_class.new(proxy_data)
end
context 'filter failures (err_msg branches)' do
# Full data with optional keys so later filters (e.g. battery, capabilities) don't trigger err_msg
let(:full_data) do
data.merge('results' => data['results'].merge(
'hardware.battery.level' => '50%',
'browser.name.reported' => 'Mozilla/5.0',
'browser.engine' => 'Gecko',
'browser.window.cookies' => 'session=abc',
'host.os.name' => 'Windows',
'host.os.family' => 'Windows',
'host.os.version' => '10',
'browser.capabilities.vbscript' => 'yes'
))
end
def stub_all_filters_valid_except(except_key = nil)
%i[
is_valid_hook_session_id? is_valid_browsername? is_valid_browserversion? is_valid_ip?
is_valid_browserstring? is_valid_cookies? is_valid_osname? is_valid_hwname?
is_valid_date_stamp? is_valid_pagetitle? is_valid_url? is_valid_pagereferrer?
is_valid_hostname? is_valid_port? is_valid_browser_plugins? is_valid_system_platform?
nums_only? is_valid_yes_no? is_valid_memory? is_valid_gpu? is_valid_cpu? alphanums_only?
].each do |m|
allow(BeEF::Filters).to receive(m).and_return(except_key == m ? false : true)
end
end
it 'calls err_msg when browser name is invalid' do
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
stub_all_filters_valid_except(:is_valid_browsername?)
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name)
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
allow(zombie).to receive(:firstseen=)
allow(zombie).to receive(:domain=)
allow(zombie).to receive(:port=)
allow(zombie).to receive(:httpheaders=)
allow(zombie).to receive(:httpheaders).and_return('{}')
allow(zombie).to receive(:save!)
allow(JSON).to receive(:parse).with('{}').and_return({})
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
err_msg_calls = []
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
described_class.new(full_data)
expect(err_msg_calls).to include(a_string_matching(/Invalid browser name/))
end
it 'calls err_msg when IP is invalid' do
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
stub_all_filters_valid_except(:is_valid_ip?)
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
allow(zombie).to receive(:firstseen=)
allow(zombie).to receive(:domain=)
allow(zombie).to receive(:port=)
allow(zombie).to receive(:httpheaders=)
allow(zombie).to receive(:httpheaders).and_return('{}')
allow(zombie).to receive(:save!)
allow(JSON).to receive(:parse).with('{}').and_return({})
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
err_msg_calls = []
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
described_class.new(full_data)
expect(err_msg_calls).to include(a_string_matching(/Invalid IP address/))
end
it 'calls err_msg when browser version is invalid' do
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
stub_all_filters_valid_except(:is_valid_browserversion?)
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
allow(zombie).to receive(:firstseen=)
allow(zombie).to receive(:domain=)
allow(zombie).to receive(:port=)
allow(zombie).to receive(:httpheaders=)
allow(zombie).to receive(:httpheaders).and_return('{}')
allow(zombie).to receive(:save!)
allow(JSON).to receive(:parse).with('{}').and_return({})
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
err_msg_calls = []
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
described_class.new(full_data)
expect(err_msg_calls).to include(a_string_matching(/Invalid browser version/))
end
it 'calls err_msg when browser.name.reported is invalid' do
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
allow(BeEF::Filters).to receive(:is_valid_browsername?).and_return(true)
allow(BeEF::Filters).to receive(:is_valid_browserversion?).and_return(true)
allow(BeEF::Filters).to receive(:is_valid_ip?).and_return(true)
allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(false)
stub_all_filters_valid_except(nil)
allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(false)
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
allow(zombie).to receive(:firstseen=)
allow(zombie).to receive(:domain=)
allow(zombie).to receive(:port=)
allow(zombie).to receive(:httpheaders=)
allow(zombie).to receive(:httpheaders).and_return('{}')
allow(zombie).to receive(:save!)
allow(JSON).to receive(:parse).with('{}').and_return({})
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
err_msg_calls = []
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
described_class.new(full_data)
expect(err_msg_calls).to include(a_string_matching(/browser\.name\.reported/))
end
it 'calls err_msg when cookies are invalid' do
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
stub_all_filters_valid_except(:is_valid_cookies?)
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
allow(zombie).to receive(:firstseen=)
allow(zombie).to receive(:domain=)
allow(zombie).to receive(:port=)
allow(zombie).to receive(:httpheaders=)
allow(zombie).to receive(:httpheaders).and_return('{}')
allow(zombie).to receive(:save!)
allow(JSON).to receive(:parse).with('{}').and_return({})
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
err_msg_calls = []
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
described_class.new(full_data)
expect(err_msg_calls).to include(a_string_matching(/Invalid cookies/))
end
it 'calls err_msg when host.os.name is invalid' do
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
stub_all_filters_valid_except(:is_valid_osname?)
allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1')
allow(zombie).to receive(:firstseen=)
allow(zombie).to receive(:domain=)
allow(zombie).to receive(:port=)
allow(zombie).to receive(:httpheaders=)
allow(zombie).to receive(:httpheaders).and_return('{}')
allow(zombie).to receive(:save!)
allow(JSON).to receive(:parse).with('{}').and_return({})
allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
err_msg_calls = []
allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last }
described_class.new(full_data)
expect(err_msg_calls).to include(a_string_matching(/operating system name/))
end
end
end
end
@@ -4,37 +4,160 @@
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Handlers::HookedBrowsers do
# Test the confirm_browser_user_agent logic directly
describe 'confirm_browser_user_agent logic' do
it 'matches legacy browser user agents' do
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0'])
# Test the logic: browser_type = user_agent.split(' ').last
user_agent = 'Mozilla/5.0 IE 8.0'
browser_type = user_agent.split(' ').last
# Test the matching logic
matched = false
BeEF::Core::Models::LegacyBrowserUserAgents.user_agents.each do |ua_string|
matched = true if ua_string.include?(browser_type)
end
expect(matched).to be true
# .new returns Sinatra::Wrapper; use allocate to get the real class instance for unit testing
let(:handler) { described_class.allocate }
describe "GET '/'" do
let(:config) { BeEF::Core::Configuration.instance }
# Use a host permitted by Router's host_authorization (.localhost, .test, or config public host)
let(:rack_env) { { 'REMOTE_ADDR' => '192.168.1.1', 'HTTP_HOST' => 'localhost' } }
def app
described_class
end
it 'does not match non-legacy browser user agents' do
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return([])
user_agent = 'Chrome/91.0'
browser_type = user_agent.split(' ').last
matched = false
BeEF::Core::Models::LegacyBrowserUserAgents.user_agents.each do |ua_string|
matched = true if ua_string.include?(browser_type)
before do
allow(BeEF::Core::Logger.instance).to receive(:register)
allow(config).to receive(:get).and_call_original
allow(config).to receive(:get).with('beef.http.restful_api.allow_cors').and_return(false)
end
it 'returns 404 when permitted_hooking_subnet is nil' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(nil)
get '/', {}, rack_env
expect(last_response.status).to eq(404)
end
it 'returns 404 when permitted_hooking_subnet is empty' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return([])
get '/', {}, rack_env
expect(last_response.status).to eq(404)
end
it 'returns 404 when client IP is not in permitted subnet' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['10.0.0.0/8'])
get '/', {}, rack_env
expect(last_response.status).to eq(404)
end
it 'returns 404 when client IP is in excluded_hooking_subnet' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0'])
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return(['192.168.1.0/24'])
get '/', {}, rack_env
expect(last_response.status).to eq(404)
end
it 'returns 200 and hook body when IP permitted, not excluded, no session (new browser)' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['192.168.0.0/16'])
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(true)
allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false)
allow_any_instance_of(described_class).to receive(:confirm_browser_user_agent).and_return(false)
allow_any_instance_of(described_class).to receive(:legacy_build_beefjs!).with('localhost')
get '/', {}, rack_env
expect(last_response.status).to eq(200)
expect(last_response.headers['Content-Type']).to include('javascript')
end
it 'uses multi_stage_beefjs when websocket disabled and confirm_browser_user_agent true' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0'])
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(true)
allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false)
allow_any_instance_of(described_class).to receive(:confirm_browser_user_agent).and_return(true)
allow_any_instance_of(described_class).to receive(:multi_stage_beefjs!).with('localhost')
get '/', {}, { 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_HOST' => 'localhost' }
expect(last_response.status).to eq(200)
end
it 'returns early with empty body when hostname is invalid' do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0'])
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
# Use permitted host so request reaches handler; stub hostname validation to fail
allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(false)
get '/', {}, { 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_HOST' => 'localhost' }
expect(last_response.status).to eq(200)
expect(last_response.body).to eq('')
end
context 'when session exists (existing browser path)' do
let(:hooked_browser) do
double('HookedBrowser',
id: 1,
ip: '192.168.1.1',
lastseen: Time.new.to_i - 120,
session: 'existing_session',
count!: nil,
save!: true).tap do |d|
allow(d).to receive(:lastseen=)
allow(d).to receive(:ip=)
end
end
expect(matched).to be false
before do
allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['192.168.0.0/16'])
allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([])
allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook')
allow(config).to receive(:get).with('beef.http.allow_reverse_proxy').and_return(false)
relation = double('Relation', first: hooked_browser)
allow(BeEF::Core::Models::HookedBrowser).to receive(:where).with(session: 'existing_session').and_return(relation)
allow(BeEF::Core::Models::Command).to receive(:where).with(hooked_browser_id: 1, instructions_sent: false).and_return([])
allow(BeEF::API::Registrar.instance).to receive(:fire)
end
it 'returns 200 and updates lastseen' do
get '/', { 'beefhook' => 'existing_session' }, rack_env
expect(last_response.status).to eq(200)
expect(hooked_browser).to have_received(:save!)
end
it 'logs zombie comeback when lastseen was more than 60 seconds ago' do
get '/', { 'beefhook' => 'existing_session' }, rack_env
expect(BeEF::Core::Logger.instance).to have_received(:register).with('Zombie', /appears to have come back online/, '1')
end
it 'calls add_command_instructions for each pending command' do
command = double('Command', id: 1, command_module_id: 1)
allow(BeEF::Core::Models::Command).to receive(:where).with(hooked_browser_id: 1, instructions_sent: false).and_return([command])
expect_any_instance_of(described_class).to receive(:add_command_instructions).with(command, hooked_browser)
get '/', { 'beefhook' => 'existing_session' }, rack_env
end
end
end
describe '#confirm_browser_user_agent' do
it 'returns true when user_agent suffix matches a legacy UA string' do
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0'])
# browser_type = user_agent.split(' ').last => '8.0'; 'IE 8.0'.include?('8.0') => true
expect(handler.confirm_browser_user_agent('Mozilla/5.0 IE 8.0')).to be true
end
it 'returns true when first legacy UA matches' do
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0', 'Firefox/3.6'])
expect(handler.confirm_browser_user_agent('Mozilla/5.0 IE 8.0')).to be true
end
it 'returns false when no legacy UA includes the browser type' do
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return([])
expect(handler.confirm_browser_user_agent('Mozilla/5.0 Chrome/91.0')).to be false
end
it 'returns false when legacy list has entries but none match' do
allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0'])
expect(handler.confirm_browser_user_agent('Chrome/91.0')).to be false
end
end
end
+75
View File
@@ -0,0 +1,75 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Logger do
let(:logger) { described_class.instance }
let(:log_double) { instance_double(BeEF::Core::Models::Log, save!: true) }
before do
allow(BeEF::Core::Models::Log).to receive(:create).and_return(log_double)
allow(logger).to receive(:print_debug)
logger.instance_variable_set(:@notifications, nil)
end
describe '#register' do
it 'creates a log entry with from, event, and hooked_browser_id' do
result = logger.register('Authentication', 'User logged in', 0)
expect(result).to be true
expect(BeEF::Core::Models::Log).to have_received(:create).with(
hash_including(
logtype: 'Authentication',
event: 'User logged in',
hooked_browser_id: 0
)
)
expect(log_double).to have_received(:save!)
end
it 'converts hb to integer' do
logger.register('From', 'Event', '42')
expect(BeEF::Core::Models::Log).to have_received(:create).with(
hash_including(hooked_browser_id: 42)
)
end
it 'defaults hb to 0 when not provided' do
logger.register('From', 'Event')
expect(BeEF::Core::Models::Log).to have_received(:create).with(
hash_including(hooked_browser_id: 0)
)
end
it 'raises TypeError when from is not a String' do
expect { logger.register(123, 'Event', 0) }.to raise_error(
TypeError, "'from' is Integer; expected String"
)
end
it 'raises TypeError when event is not a String' do
expect { logger.register('From', nil, 0) }.to raise_error(
TypeError, "'event' is NilClass; expected String"
)
end
it 'calls notifications when extension is enabled' do
notifications_double = double('Notifications')
logger.instance_variable_set(:@notifications, notifications_double)
allow(notifications_double).to receive(:new)
logger.register('Zombie', 'Browser hooked', 7)
expect(notifications_double).to have_received(:new).with(
'Zombie',
'Browser hooked',
kind_of(Time),
7
)
end
end
end
+123
View File
@@ -0,0 +1,123 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Models::Command do
describe 'associations' do
it 'has_many results' do
expect(described_class.reflect_on_association(:results)).not_to be_nil
expect(described_class.reflect_on_association(:results).macro).to eq(:has_many)
end
it 'has_one command_module' do
expect(described_class.reflect_on_association(:command_module)).not_to be_nil
expect(described_class.reflect_on_association(:command_module).macro).to eq(:has_one)
end
it 'has_one hooked_browser' do
expect(described_class.reflect_on_association(:hooked_browser)).not_to be_nil
expect(described_class.reflect_on_association(:hooked_browser).macro).to eq(:has_one)
end
end
describe '.show_status' do
it 'returns ERROR for status -1' do
expect(described_class.show_status(-1)).to eq('ERROR')
end
it 'returns SUCCESS for status 1' do
expect(described_class.show_status(1)).to eq('SUCCESS')
end
it 'returns UNKNOWN for status 0' do
expect(described_class.show_status(0)).to eq('UNKNOWN')
end
it 'returns UNKNOWN for any other status' do
expect(described_class.show_status(2)).to eq('UNKNOWN')
expect(described_class.show_status(99)).to eq('UNKNOWN')
end
end
describe '.save_result' do
let(:hooked_browser) { BeEF::Core::Models::HookedBrowser.create!(session: 'cmd_save_session', ip: '127.0.0.1') }
let(:command_module) { BeEF::Core::Models::CommandModule.create!(name: 'cmd_save_mod', path: 'modules/test/') }
let(:command) do
described_class.create!(
hooked_browser_id: hooked_browser.id,
command_module_id: command_module.id
)
end
before do
allow(BeEF::Core::Logger.instance).to receive(:register)
allow(described_class).to receive(:print_info)
end
it 'creates a Result and returns true when all args are valid' do
result = described_class.save_result(
'cmd_save_session',
command.id,
'Friendly Name',
{ 'output' => 'data' },
1
)
expect(result).to be true
created = BeEF::Core::Models::Result.last
expect(created).not_to be_nil
expect(created.command_id).to eq(command.id)
expect(created.hooked_browser_id).to eq(hooked_browser.id)
expect(created.status).to eq(1)
expect(JSON.parse(created.data)).to eq({ 'output' => 'data' })
end
it 'raises TypeError when hook_session_id is not a String' do
expect do
described_class.save_result(123, command.id, 'Name', {}, 1)
end.to raise_error(TypeError, '"hook_session_id" needs to be a string')
end
it 'raises TypeError when command_id is not an Integer' do
expect do
described_class.save_result('cmd_save_session', '1', 'Name', {}, 1)
end.to raise_error(TypeError, '"command_id" needs to be an integer')
end
it 'raises TypeError when command_friendly_name is not a String' do
expect do
described_class.save_result('cmd_save_session', command.id, 123, {}, 1)
end.to raise_error(TypeError, '"command_friendly_name" needs to be a string')
end
it 'raises TypeError when result is not a Hash' do
expect do
described_class.save_result('cmd_save_session', command.id, 'Name', 'string', 1)
end.to raise_error(TypeError, '"result" needs to be a hash')
end
it 'raises TypeError when status is not an Integer' do
expect do
described_class.save_result('cmd_save_session', command.id, 'Name', {}, '1')
end.to raise_error(TypeError, '"status" needs to be an integer')
end
it 'raises TypeError when hooked_browser is not found for session' do
expect do
described_class.save_result('nonexistent_session', command.id, 'Name', {}, 1)
end.to raise_error(TypeError, 'hooked_browser is nil')
end
it 'raises TypeError when command is not found for id and hooked_browser' do
BeEF::Core::Models::HookedBrowser.create!(session: 'other_session', ip: '127.0.0.1')
expect do
described_class.save_result('other_session', command.id, 'Name', {}, 1)
end.to raise_error(TypeError, 'command is nil')
end
end
end
@@ -0,0 +1,26 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Models::CommandModule do
describe 'associations' do
it 'has_many commands' do
expect(described_class.reflect_on_association(:commands)).not_to be_nil
expect(described_class.reflect_on_association(:commands).macro).to eq(:has_many)
end
end
describe '.create' do
it 'creates a command module with name and path' do
mod = described_class.create!(name: 'test_module', path: 'modules/test/')
expect(mod).to be_persisted
expect(mod.name).to eq('test_module')
expect(mod.path).to eq('modules/test/')
end
end
end
@@ -0,0 +1,44 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Models::HookedBrowser do
describe 'associations' do
it 'has_many commands' do
expect(described_class.reflect_on_association(:commands)).not_to be_nil
expect(described_class.reflect_on_association(:commands).macro).to eq(:has_many)
end
it 'has_many results' do
expect(described_class.reflect_on_association(:results)).not_to be_nil
expect(described_class.reflect_on_association(:results).macro).to eq(:has_many)
end
it 'has_many logs' do
expect(described_class.reflect_on_association(:logs)).not_to be_nil
expect(described_class.reflect_on_association(:logs).macro).to eq(:has_many)
end
end
describe '#count!' do
it 'sets count to 1 when count is nil' do
hb = described_class.create!(session: 'count_nil', ip: '127.0.0.1', count: nil)
hb.count!
expect(hb.count).to eq(1)
end
it 'increments count when count is already set' do
hb = described_class.create!(session: 'count_set', ip: '127.0.0.1', count: 3)
hb.count!
expect(hb.count).to eq(4)
end
end
end
+42
View File
@@ -0,0 +1,42 @@
#
# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
# Browser Exploitation Framework (BeEF) - https://beefproject.com
# See the file 'doc/COPYING' for copying permission
#
require 'spec_helper'
RSpec.describe BeEF::Core::Models::Log do
describe 'associations' do
it 'has_one hooked_browser' do
expect(described_class.reflect_on_association(:hooked_browser)).not_to be_nil
expect(described_class.reflect_on_association(:hooked_browser).macro).to eq(:has_one)
end
end
describe '.create' do
it 'creates a log with logtype, event, and date' do
log = described_class.create!(
logtype: 'TestSource',
event: 'Test event message',
date: Time.now
)
expect(log).to be_persisted
expect(log.logtype).to eq('TestSource')
expect(log.event).to eq('Test event message')
end
it 'can store hooked_browser_id' do
hb = BeEF::Core::Models::HookedBrowser.create!(session: 'log_hb', ip: '127.0.0.1')
log = described_class.create!(
logtype: 'Hook',
event: 'Browser hooked',
date: Time.now,
hooked_browser_id: hb.id
)
expect(log.hooked_browser_id).to eq(hb.id)
end
end
end
+64
View File
@@ -110,4 +110,68 @@ RSpec.describe BeEF::Core::Server do
server.remap
end
end
describe '#prepare' do
before do
allow(BeEF::API::Registrar.instance).to receive(:fire).with(BeEF::API::Server, 'mount_handler', server)
allow(config).to receive(:get).and_return(nil)
allow(config).to receive(:get).with('beef.http.hook_file').and_return('/hook.js')
allow(config).to receive(:get).with('beef.http.host').and_return('0.0.0.0')
allow(config).to receive(:get).with('beef.http.port').and_return('3000')
allow(config).to receive(:get).with('beef.http.debug').and_return(false)
allow(config).to receive(:get).with('beef.http.https.enable').and_return(false)
allow(config).to receive(:get).with('beef.debug').and_return(false)
allow(Thin::Server).to receive(:new).and_return(double('Thin::Server'))
end
it 'mounts hook file handler and init handler' do
server.prepare
expect(server.mounts).to have_key('/hook.js')
expect(server.mounts).to have_key('/init')
expect(server.mounts['/hook.js']).not_to be_nil
expect(server.mounts['/init']).to eq(BeEF::Core::Handlers::BrowserDetails)
end
it 'builds Rack URLMap from mounts' do
server.prepare
expect(server.instance_variable_get(:@rack_app)).to be_a(Rack::URLMap)
end
it 'returns early when @http_server already set' do
allow(Thin::Server).to receive(:new)
existing = double('Thin::Server')
server.instance_variable_set(:@http_server, existing)
server.prepare
expect(Thin::Server).not_to have_received(:new)
end
it 'sets Thin::Logging when beef.http.debug is true' do
allow(config).to receive(:get).with('beef.http.debug').and_return(true)
allow(Thin::Logging).to receive(:silent=)
allow(Thin::Logging).to receive(:debug=)
server.prepare
expect(Thin::Logging).to have_received(:silent=).with(false)
expect(Thin::Logging).to have_received(:debug=).with(true)
end
end
describe '#start' do
it 'rescues port-in-use error and exits' do
mock_thin = double('Thin::Server')
allow(mock_thin).to receive(:start).and_raise(RuntimeError.new('no acceptor'))
server.instance_variable_set(:@http_server, mock_thin)
allow(server).to receive(:print_error)
allow(server).to receive(:exit).with(127) { raise SystemExit.new(127) }
expect { server.start }.to raise_error(SystemExit)
expect(server).to have_received(:print_error).with(/port|invalid IP/i)
expect(server).to have_received(:exit).with(127)
end
it 're-raises RuntimeError when message does not include no acceptor' do
mock_thin = double('Thin::Server')
allow(mock_thin).to receive(:start).and_raise(RuntimeError.new('other error'))
server.instance_variable_set(:@http_server, mock_thin)
expect { server.start }.to raise_error(RuntimeError, 'other error')
end
end
end
+26
View File
@@ -25,4 +25,30 @@ RSpec.describe 'Security method overrides' do
expect(Kernel.method(:system).source_location).not_to be_nil
expect(Kernel.method(:system).source_location[0]).to include('core/ruby/security.rb')
end
describe 'override behavior' do
it 'exec prints security message and exits' do
allow(Kernel).to receive(:puts)
allow(Kernel).to receive(:exit)
exec('ls')
expect(Kernel).to have_received(:puts).with(/security reasons.*exec/)
expect(Kernel).to have_received(:exit)
end
it 'system prints security message and exits' do
allow(Kernel).to receive(:puts)
allow(Kernel).to receive(:exit)
system('ls')
expect(Kernel).to have_received(:puts).with(/security reasons.*system/)
expect(Kernel).to have_received(:exit)
end
it 'Kernel.system prints security message and exits' do
allow(Kernel).to receive(:puts)
allow(Kernel).to receive(:exit)
Kernel.system('ls')
expect(Kernel).to have_received(:puts).with(/security reasons.*system/)
expect(Kernel).to have_received(:exit)
end
end
end