From 905157edfb5a27c00ffc5943906bc53bb234fb8f Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:24:41 +1000 Subject: [PATCH 01/18] TEST: gbmanager spec --- spec/beef/core/hbmanager_spec.rb | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 spec/beef/core/hbmanager_spec.rb diff --git a/spec/beef/core/hbmanager_spec.rb b/spec/beef/core/hbmanager_spec.rb new file mode 100644 index 000000000..c1a00589c --- /dev/null +++ b/spec/beef/core/hbmanager_spec.rb @@ -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 From 3287d3f49dea4ed32433a0c8d4be3c69befb8458 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:24:52 +1000 Subject: [PATCH 02/18] TEST: core main logger spec --- spec/beef/core/main/logger_spec.rb | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 spec/beef/core/main/logger_spec.rb diff --git a/spec/beef/core/main/logger_spec.rb b/spec/beef/core/main/logger_spec.rb new file mode 100644 index 000000000..c3481331b --- /dev/null +++ b/spec/beef/core/main/logger_spec.rb @@ -0,0 +1,62 @@ +# +# 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 + end +end From 1b2bd10a0a8d78c2f8f3748654968f4930e64c55 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:25:10 +1000 Subject: [PATCH 03/18] TEST: core main constatns browsers spec --- .../beef/core/main/constants/browsers_spec.rb | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 spec/beef/core/main/constants/browsers_spec.rb diff --git a/spec/beef/core/main/constants/browsers_spec.rb b/spec/beef/core/main/constants/browsers_spec.rb new file mode 100644 index 000000000..c28ee66a8 --- /dev/null +++ b/spec/beef/core/main/constants/browsers_spec.rb @@ -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 From fc10884d50ebc0815bdcfb665dc55be63d38a9c1 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:25:26 +1000 Subject: [PATCH 04/18] TEST: core main models specs --- .../core/main/models/commandmodule_spec.rb | 26 +++++++++++ .../core/main/models/hookedbrowser_spec.rb | 44 +++++++++++++++++++ spec/beef/core/main/models/log_spec.rb | 42 ++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 spec/beef/core/main/models/commandmodule_spec.rb create mode 100644 spec/beef/core/main/models/hookedbrowser_spec.rb create mode 100644 spec/beef/core/main/models/log_spec.rb diff --git a/spec/beef/core/main/models/commandmodule_spec.rb b/spec/beef/core/main/models/commandmodule_spec.rb new file mode 100644 index 000000000..96a1e272a --- /dev/null +++ b/spec/beef/core/main/models/commandmodule_spec.rb @@ -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 diff --git a/spec/beef/core/main/models/hookedbrowser_spec.rb b/spec/beef/core/main/models/hookedbrowser_spec.rb new file mode 100644 index 000000000..eeef52924 --- /dev/null +++ b/spec/beef/core/main/models/hookedbrowser_spec.rb @@ -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 diff --git a/spec/beef/core/main/models/log_spec.rb b/spec/beef/core/main/models/log_spec.rb new file mode 100644 index 000000000..9c2750ceb --- /dev/null +++ b/spec/beef/core/main/models/log_spec.rb @@ -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 From a69423fb942ef265ecb964db369880bbdc0f89f8 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:38:40 +1000 Subject: [PATCH 05/18] TEST: main autorun specs --- .../core/main/autorun_engine/parser_spec.rb | 119 ++++++++++++++++++ .../main/autorun_engine/rule_loader_spec.rb | 83 ++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 spec/beef/core/main/autorun_engine/parser_spec.rb create mode 100644 spec/beef/core/main/autorun_engine/rule_loader_spec.rb diff --git a/spec/beef/core/main/autorun_engine/parser_spec.rb b/spec/beef/core/main/autorun_engine/parser_spec.rb new file mode 100644 index 000000000..9ab37d85a --- /dev/null +++ b/spec/beef/core/main/autorun_engine/parser_spec.rb @@ -0,0 +1,119 @@ +# +# 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::AutorunEngine::Parser do + let(:parser) { described_class.instance } + + def valid_minimal_args + { + name: 'Test Rule', + author: 'Test Author', + browser: 'ALL', + browser_version: 'ALL', + os: 'Windows', + os_version: 'ALL', + modules: [], + execution_order: [], + execution_delay: [], + chain_mode: 'sequential' + } + end + + describe '#parse' do + it 'returns true for valid minimal args (empty modules)' do + result = parser.parse( + valid_minimal_args[:name], + valid_minimal_args[:author], + valid_minimal_args[:browser], + valid_minimal_args[:browser_version], + valid_minimal_args[:os], + valid_minimal_args[:os_version], + valid_minimal_args[:modules], + valid_minimal_args[:execution_order], + valid_minimal_args[:execution_delay], + valid_minimal_args[:chain_mode] + ) + + expect(result).to be true + end + + it 'raises ArgumentError for empty name' do + expect do + parser.parse('', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') + end.to raise_error(ArgumentError, /Invalid rule name/) + end + + it 'raises ArgumentError for nil name' do + expect do + parser.parse(nil, 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') + end.to raise_error(ArgumentError, /Invalid rule name/) + end + + it 'raises ArgumentError for empty author' do + expect do + parser.parse('Name', '', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') + end.to raise_error(ArgumentError, /Invalid author name/) + end + + it 'raises ArgumentError for invalid chain_mode' do + expect do + parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'invalid') + end.to raise_error(ArgumentError, /Invalid chain_mode definition/) + end + + it 'raises ArgumentError for invalid os' do + expect do + parser.parse('Name', 'Author', 'ALL', 'ALL', 'InvalidOS', 'ALL', [], [], [], 'sequential') + end.to raise_error(ArgumentError, /Invalid os definition/) + end + + it 'raises ArgumentError when execution_delay size does not match modules size' do + expect do + parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{ 'name' => 'a' }], [1], [], 'sequential') + end.to raise_error(ArgumentError, /execution_delay.*consistent with number of modules/) + end + + it 'raises ArgumentError when execution_order size does not match modules size' do + expect do + parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{ 'name' => 'a' }], [], [0], 'sequential') + end.to raise_error(ArgumentError, /execution_order.*consistent with number of modules/) + end + + it 'raises TypeError when execution_delay contains non-Integer' do + # Use one module so sizes match; then type check runs on execution_delay + expect do + parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{}], [1], ['not_an_int'], 'sequential') + end.to raise_error(TypeError, /execution_delay.*Integers/) + end + + it 'raises TypeError when execution_order contains non-Integer' do + # Use one module so sizes match; then type check runs on execution_order + expect do + parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{}], ['x'], [0], 'sequential') + end.to raise_error(TypeError, /execution_order.*Integers/) + end + + it 'raises ArgumentError for invalid browser' do + expect do + parser.parse('Name', 'Author', 'XX', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') + end.to raise_error(ArgumentError, /Invalid browser definition/) + end + + it 'accepts nested-forward as chain_mode' do + result = parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'nested-forward') + expect(result).to be true + end + + it 'accepts valid os values' do + %w[Linux Windows OSX Android iOS BlackBerry ALL].each do |os| + result = parser.parse('Name', 'Author', 'ALL', 'ALL', os, 'ALL', [], [], [], 'sequential') + expect(result).to be true + end + end + end +end diff --git a/spec/beef/core/main/autorun_engine/rule_loader_spec.rb b/spec/beef/core/main/autorun_engine/rule_loader_spec.rb new file mode 100644 index 000000000..a27560b68 --- /dev/null +++ b/spec/beef/core/main/autorun_engine/rule_loader_spec.rb @@ -0,0 +1,83 @@ +# +# 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::AutorunEngine::RuleLoader do + let(:loader) { described_class.instance } + + def valid_rule_data + { + 'name' => 'Test Rule', + 'author' => 'Test Author', + 'browser' => 'ALL', + 'browser_version' => 'ALL', + 'os' => 'Windows', + 'os_version' => 'ALL', + 'modules' => [], + 'execution_order' => [], + 'execution_delay' => [], + 'chain_mode' => 'sequential' + } + end + + before do + allow(loader).to receive(:print_error) + allow(loader).to receive(:print_info) + allow(loader).to receive(:print_more) + end + + describe '#load_rule_json' do + it 'returns success and rule_id when parse succeeds and rule is new' do + # Parser will succeed with valid minimal data; no existing rule + result = loader.load_rule_json(valid_rule_data) + + expect(result['success']).to be true + expect(result).to have_key('rule_id') + expect(result['rule_id']).to be_a(Integer) + end + + it 'returns success false and error when parse raises' do + allow(BeEF::Core::AutorunEngine::Parser.instance).to receive(:parse).and_raise(ArgumentError.new('Invalid rule name')) + + result = loader.load_rule_json(valid_rule_data.merge('name' => 'x')) + + expect(result['success']).to be false + expect(result['error']).to include('Invalid rule name') + end + + it 'returns success false and error when rule already exists' do + # Create the rule first so it already exists + BeEF::Core::Models::Rule.create!( + name: 'Duplicate Rule', + author: 'Test Author', + browser: 'ALL', + browser_version: 'ALL', + os: 'Windows', + os_version: 'ALL', + modules: [].to_json, + execution_order: [].to_s, + execution_delay: [].to_s, + chain_mode: 'sequential' + ) + + result = loader.load_rule_json( + valid_rule_data.merge('name' => 'Duplicate Rule') + ) + + expect(result['success']).to be false + expect(result['error']).to include('Duplicate rule already exists') + end + + it 'uses default chain_mode sequential when missing' do + data = valid_rule_data.except('chain_mode') + result = loader.load_rule_json(data) + + expect(result['success']).to be true + expect(result).to have_key('rule_id') + end + end +end From 83d2b2ff761c17b16750e72a17d2e144e124405a Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:38:50 +1000 Subject: [PATCH 06/18] TEST: main models command spec --- spec/beef/core/main/models/command_spec.rb | 123 +++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 spec/beef/core/main/models/command_spec.rb diff --git a/spec/beef/core/main/models/command_spec.rb b/spec/beef/core/main/models/command_spec.rb new file mode 100644 index 000000000..2cec84b98 --- /dev/null +++ b/spec/beef/core/main/models/command_spec.rb @@ -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 + other_hb = 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 From 51afa04e6b2b53fc5411786fce048c3dc12ad95d Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:40:39 +1000 Subject: [PATCH 07/18] TEST: main constants specs --- .../beef/core/main/constants/hardware_spec.rb | 69 ++++++++++++++++++ spec/beef/core/main/constants/os_spec.rb | 73 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 spec/beef/core/main/constants/hardware_spec.rb create mode 100644 spec/beef/core/main/constants/os_spec.rb diff --git a/spec/beef/core/main/constants/hardware_spec.rb b/spec/beef/core/main/constants/hardware_spec.rb new file mode 100644 index 000000000..1846d0c4c --- /dev/null +++ b/spec/beef/core/main/constants/hardware_spec.rb @@ -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 diff --git a/spec/beef/core/main/constants/os_spec.rb b/spec/beef/core/main/constants/os_spec.rb new file mode 100644 index 000000000..5cba824e9 --- /dev/null +++ b/spec/beef/core/main/constants/os_spec.rb @@ -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 From d27fba46b8d684111f323421d3eed179ee436688 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:40:54 +1000 Subject: [PATCH 08/18] TEST: main constatns commandmodule spec --- .../core/main/constants/commandmodule_spec.rb | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 spec/beef/core/main/constants/commandmodule_spec.rb diff --git a/spec/beef/core/main/constants/commandmodule_spec.rb b/spec/beef/core/main/constants/commandmodule_spec.rb new file mode 100644 index 000000000..eb94fc00a --- /dev/null +++ b/spec/beef/core/main/constants/commandmodule_spec.rb @@ -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 From dfce4728d5b2c4a4a3753ffd1cf6169a260c4deb Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 16:54:15 +1000 Subject: [PATCH 09/18] TEST: updates to base and crpto specs --- spec/beef/core/filter/base_spec.rb | 138 +++++++++++++++++++++++++++++ spec/beef/core/main/crypto_spec.rb | 1 + 2 files changed, 139 insertions(+) diff --git a/spec/beef/core/filter/base_spec.rb b/spec/beef/core/filter/base_spec.rb index 6e54d0dea..04275ecc8 100644 --- a/spec/beef/core/filter/base_spec.rb +++ b/spec/beef/core/filter/base_spec.rb @@ -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 diff --git a/spec/beef/core/main/crypto_spec.rb b/spec/beef/core/main/crypto_spec.rb index bfe4ccfbf..048d75c3e 100644 --- a/spec/beef/core/main/crypto_spec.rb +++ b/spec/beef/core/main/crypto_spec.rb @@ -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 From 035f17a5c797f8308c2c2e079663e54e962095e5 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 17:10:28 +1000 Subject: [PATCH 10/18] TEST: core main various specs --- .../core/main/autorun_engine/engine_spec.rb | 117 +++++++++++ spec/beef/core/main/console/banners_spec.rb | 196 ++++++++++++++++++ .../core/main/handlers/hookedbrowsers_spec.rb | 51 +++-- spec/beef/core/ruby/security_spec.rb | 26 +++ 4 files changed, 364 insertions(+), 26 deletions(-) create mode 100644 spec/beef/core/main/autorun_engine/engine_spec.rb create mode 100644 spec/beef/core/main/console/banners_spec.rb diff --git a/spec/beef/core/main/autorun_engine/engine_spec.rb b/spec/beef/core/main/autorun_engine/engine_spec.rb new file mode 100644 index 000000000..ed708ed53 --- /dev/null +++ b/spec/beef/core/main/autorun_engine/engine_spec.rb @@ -0,0 +1,117 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Example: unit specs for AutorunEngine::Engine using mocks instead of a real server/DB. +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::AutorunEngine::Engine do + let(:engine) { described_class.instance } + let(:config) { BeEF::Core::Configuration.instance } + + before do + allow(engine).to receive(:print_debug) + allow(engine).to receive(:print_info) + allow(engine).to receive(:print_more) + allow(engine).to receive(:print_error) + end + + # Fake rule object (could be a double or a persisted Rule with minimal attributes) + def rule_with(browser: 'ALL', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') + double( + 'Rule', + id: 1, + browser: browser, + browser_version: browser_version, + os: os, + os_version: os_version + ) + end + + describe '#zombie_matches_rule?' do + it 'returns false when rule is nil' do + expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', nil)).to be false + end + + it 'returns true when rule is ALL for browser and OS' do + rule = rule_with(browser: 'ALL', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') + allow(engine).to receive(:zombie_browser_matches_rule?).with('FF', '41', rule).and_return(true) + allow(engine).to receive(:zombie_os_matches_rule?).with('Windows', '7', rule).and_return(true) + expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', rule)).to be true + end + + it 'returns false when browser does not match' do + rule = rule_with(browser: 'FF', browser_version: '>= 41', os: 'ALL', os_version: 'ALL') + allow(engine).to receive(:zombie_browser_matches_rule?).with('FF', '41', rule).and_return(false) + expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', rule)).to be false + end + + it 'returns false when OS does not match' do + rule = rule_with(browser: 'ALL', browser_version: 'ALL', os: 'Windows', os_version: '7') + allow(engine).to receive(:zombie_browser_matches_rule?).with('FF', '41', rule).and_return(true) + allow(engine).to receive(:zombie_os_matches_rule?).with('Windows', '7', rule).and_return(false) + expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', rule)).to be false + end + end + + describe '#zombie_os_matches_rule?' do + it 'returns false when rule is nil' do + expect(engine.zombie_os_matches_rule?('Windows', '7', nil)).to be false + end + + it 'returns true when rule os is ALL' do + rule = double('Rule', os: 'ALL', os_version: 'ALL') + expect(engine.zombie_os_matches_rule?('Windows', '7', rule)).to be true + end + + it 'returns false when hook os does not match rule os' do + rule = double('Rule', os: 'Linux', os_version: 'ALL') + expect(engine.zombie_os_matches_rule?('Windows', '7', rule)).to be false + end + + it 'returns true when rule os matches and os_version is ALL' do + rule = double('Rule', os: 'Windows', os_version: 'ALL') + expect(engine.zombie_os_matches_rule?('Windows', '7', rule)).to be true + end + end + + describe '#zombie_browser_matches_rule?' do + it 'returns false when rule is nil' do + expect(engine.zombie_browser_matches_rule?('FF', '41', nil)).to be false + end + + it 'returns true when rule browser is ALL and version is ALL' do + rule = double('Rule', browser: 'ALL', browser_version: 'ALL') + expect(engine.zombie_browser_matches_rule?('FF', '41', rule)).to be true + end + + it 'returns true when rule browser matches and version is ALL' do + rule = double('Rule', browser: 'FF', browser_version: 'ALL') + expect(engine.zombie_browser_matches_rule?('FF', '41', rule)).to be true + end + + it 'returns false when rule browser does not match' do + rule = double('Rule', browser: 'IE', browser_version: 'ALL') + expect(engine.zombie_browser_matches_rule?('FF', '41', rule)).to be false + end + end + + describe '#find_matching_rules_for_zombie' do + it 'returns nil when no rules exist' do + allow(BeEF::Core::Models::Rule).to receive(:all).and_return([]) + expect(engine.find_matching_rules_for_zombie('FF', '41', 'Windows', '7')).to be_nil + end + + it 'returns matching rule ids when rules match zombie' do + rule1 = double('Rule', id: 1, name: 'Rule1', browser: 'ALL', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') + rule2 = double('Rule', id: 2, name: 'Rule2', browser: 'IE', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') + allow(BeEF::Core::Models::Rule).to receive(:all).and_return([rule1, rule2]) + allow(engine).to receive(:zombie_matches_rule?).with('FF', '41', 'Windows', '7', rule1).and_return(true) + allow(engine).to receive(:zombie_matches_rule?).with('FF', '41', 'Windows', '7', rule2).and_return(false) + expect(engine.find_matching_rules_for_zombie('FF', '41', 'Windows', '7')).to eq([1]) + end + end +end diff --git a/spec/beef/core/main/console/banners_spec.rb b/spec/beef/core/main/console/banners_spec.rb new file mode 100644 index 000000000..e27b7cc8f --- /dev/null +++ b/spec/beef/core/main/console/banners_spec.rb @@ -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 diff --git a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb index 595b7f9e8..0bfa9ead0 100644 --- a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb +++ b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb @@ -4,37 +4,36 @@ # 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 + # .new returns Sinatra::Wrapper; use allocate to get the real class instance for unit testing + let(:handler) { described_class.allocate } + + 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']) - - # 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 + + # 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 'does not match non-legacy browser user agents' do + 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([]) - - 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) - end - - expect(matched).to be false + + 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 diff --git a/spec/beef/core/ruby/security_spec.rb b/spec/beef/core/ruby/security_spec.rb index ba257bfcd..9b9fa9a33 100644 --- a/spec/beef/core/ruby/security_spec.rb +++ b/spec/beef/core/ruby/security_spec.rb @@ -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 From cad6fc9d815a739fc7db8fbb5b543df7f3c27f8c Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 17:54:19 +1000 Subject: [PATCH 11/18] TEST: core main server spec --- spec/beef/core/main/server_spec.rb | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/beef/core/main/server_spec.rb b/spec/beef/core/main/server_spec.rb index b694c9bb1..71419d062 100644 --- a/spec/beef/core/main/server_spec.rb +++ b/spec/beef/core/main/server_spec.rb @@ -110,4 +110,31 @@ 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 + end end From ee8234958eed8c478ee0ce229fa8bd9ecc37af71 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 17:54:49 +1000 Subject: [PATCH 12/18] TEST: more engine spec unit tests --- .../core/main/autorun_engine/engine_spec.rb | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/spec/beef/core/main/autorun_engine/engine_spec.rb b/spec/beef/core/main/autorun_engine/engine_spec.rb index ed708ed53..357af25f7 100644 --- a/spec/beef/core/main/autorun_engine/engine_spec.rb +++ b/spec/beef/core/main/autorun_engine/engine_spec.rb @@ -114,4 +114,246 @@ RSpec.describe BeEF::Core::AutorunEngine::Engine do expect(engine.find_matching_rules_for_zombie('FF', '41', 'Windows', '7')).to eq([1]) end end + + describe '#compare_versions' do + it 'returns true when cond is ALL' do + expect(engine.send(:compare_versions, '7', 'ALL', '8')).to be true + end + + it 'returns true when cond is == and versions equal' do + expect(engine.send(:compare_versions, '41', '==', '41')).to be true + end + + it 'returns false when cond is == and versions differ' do + expect(engine.send(:compare_versions, '41', '==', '42')).to be false + end + + it 'returns true when cond is <= and ver_a <= ver_b' do + expect(engine.send(:compare_versions, '41', '<=', '42')).to be true + expect(engine.send(:compare_versions, '41', '<=', '41')).to be true + end + + it 'returns false when cond is <= and ver_a > ver_b' do + expect(engine.send(:compare_versions, '42', '<=', '41')).to be false + end + + it 'returns true when cond is < and ver_a < ver_b' do + expect(engine.send(:compare_versions, '41', '<', '42')).to be true + end + + it 'returns false when cond is < and ver_a >= ver_b' do + expect(engine.send(:compare_versions, '42', '<', '41')).to be false + expect(engine.send(:compare_versions, '41', '<', '41')).to be false + end + + it 'returns true when cond is >= and ver_a >= ver_b' do + expect(engine.send(:compare_versions, '42', '>=', '41')).to be true + expect(engine.send(:compare_versions, '41', '>=', '41')).to be true + end + + it 'returns true when cond is > and ver_a > ver_b' do + expect(engine.send(:compare_versions, '42', '>', '41')).to be true + end + + it 'returns false when cond is > and ver_a <= ver_b' do + expect(engine.send(:compare_versions, '41', '>', '42')).to be false + expect(engine.send(:compare_versions, '41', '>', '41')).to be false + end + + it 'returns false for unknown cond' do + expect(engine.send(:compare_versions, '41', '!=', '42')).to be false + end + end + + describe '#clean_command_body' do + it 'extracts body range and replaces single-quoted mod_input when replace_input is true' do + body = "beef.execute(function(){\n alert('<>');\n});\n" + result = engine.send(:clean_command_body, body, true) + expect(result).to include('alert(mod_input)') + expect(result).to include('beef.execute(function(){') + end + + it 'returns cleaned body without mod_input replacement when replace_input is false' do + body = "beef.execute(function(){\n doSomething('<>');\n});\n" + result = engine.send(:clean_command_body, body, false) + expect(result).to include('<>') + end + + it 'replaces double-quoted <> with mod_input when replace_input is true' do + body = "beef.execute(function(){\n x(\"<>\");\n});\n" + result = engine.send(:clean_command_body, body, true) + expect(result).to include('mod_input') + expect(result).not_to include('"<>"') + end + + it 'replaces single-quoted <> with mod_input when replace_input is true' do + body = "beef.execute(function(){\n x('<>');\n});\n" + result = engine.send(:clean_command_body, body, true) + expect(result).to include('mod_input') + end + end + + describe '#prepare_sequential_wrapper' do + it 'builds wrapper with mod bodies and setTimeout calls in order' do + mods = [ + { mod_name: 'mod_a', mod_body: 'var mod_a_mod_output = 1;' }, + { mod_name: 'mod_b', mod_body: 'var mod_b_mod_output = 2;' } + ] + order = [0, 1] + delay = [0, 500] + token = 't1' + result = engine.send(:prepare_sequential_wrapper, mods, order, delay, token) + expect(result).to include('mod_a_t1') + expect(result).to include('mod_b_t1') + expect(result).to include('setTimeout(function(){mod_a_t1();}, 0)') + expect(result).to include('setTimeout(function(){mod_b_t1();}, 500)') + expect(result).to include('mod_a_t1_mod_output') + expect(result).to include('mod_b_t1_mod_output') + end + + it 'handles single module' do + mods = [{ mod_name: 'single', mod_body: 'x();' }] + order = [0] + delay = [0] + result = engine.send(:prepare_sequential_wrapper, mods, order, delay, 'tk') + expect(result).to include('single_tk') + expect(result).to include('setTimeout(function(){single_tk();}, 0)') + end + end + + describe '#prepare_nested_forward_wrapper' do + it 'builds wrapper for single module' do + mods = [{ mod_name: 'only', mod_body: 'only();' }] + code = ['null'] + conditions = [true] + order = [0] + token = 'nf1' + result = engine.send(:prepare_nested_forward_wrapper, mods, code, conditions, order, token) + expect(result).to include('only_nf1') + expect(result).to include('only_nf1_f') + expect(result).to include('only_nf1_mod_output') + end + end + + describe '#find_and_run_all_matching_rules_for_zombie' do + it 'returns without calling run_rules when hb_id is nil' do + expect(engine).not_to receive(:run_rules_on_zombie) + engine.find_and_run_all_matching_rules_for_zombie(nil) + end + + it 'returns without calling run_rules when find_matching_rules returns nil' do + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') + allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return(nil) + expect(engine).not_to receive(:run_rules_on_zombie) + engine.find_and_run_all_matching_rules_for_zombie(1) + end + + it 'returns without calling run_rules when find_matching_rules returns empty' do + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') + allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([]) + expect(engine).not_to receive(:run_rules_on_zombie) + engine.find_and_run_all_matching_rules_for_zombie(1) + end + + it 'calls run_rules_on_zombie with matching rule ids when rules match' do + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') + allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([1, 2]) + expect(engine).to receive(:run_rules_on_zombie).with([1, 2], 1) + engine.find_and_run_all_matching_rules_for_zombie(1) + end + end + + describe '#run_matching_rules_on_zombie' do + it 'returns when rule_ids is nil' do + expect(engine).not_to receive(:run_rules_on_zombie) + engine.run_matching_rules_on_zombie(nil, 1) + end + + it 'returns when hb_id is nil' do + expect(engine).not_to receive(:run_rules_on_zombie) + engine.run_matching_rules_on_zombie([1], nil) + end + + it 'returns without calling run_rules when find_matching_rules returns nil' do + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') + allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return(nil) + expect(engine).not_to receive(:run_rules_on_zombie) + engine.run_matching_rules_on_zombie([1], 1) + end + + it 'calls run_rules_on_zombie with intersection of rule_ids and matching rules' do + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') + allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([1, 2]) + expect(engine).to receive(:run_rules_on_zombie).with([1], 1) + engine.run_matching_rules_on_zombie([1], 1) + end + + it 'does not call run_rules_on_zombie when no rule_ids overlap matching rules' do + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') + allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([1, 2]) + expect(engine).not_to receive(:run_rules_on_zombie) + engine.run_matching_rules_on_zombie([99], 1) + end + end + + describe '#run_rules_on_zombie' do + it 'returns when rule_ids is nil' do + expect(BeEF::HBManager).not_to receive(:get_by_id) + engine.run_rules_on_zombie(nil, 1) + end + + it 'returns when hb_id is nil' do + expect(BeEF::HBManager).not_to receive(:get_by_id) + engine.run_rules_on_zombie([1], nil) + end + + it 'normalizes single Integer rule_id to array and processes rule' do + hb = double('HookedBrowser', session: 'sess1') + allow(BeEF::HBManager).to receive(:get_by_id).with(1).and_return(hb) + rule = double( + 'Rule', + modules: '[]', + execution_order: '[]', + execution_delay: '[]', + chain_mode: 'invalid' + ) + allow(BeEF::Core::Models::Rule).to receive(:find).with(1).and_return(rule) + engine.run_rules_on_zombie(1, 1) + expect(BeEF::Core::Models::Rule).to have_received(:find).with(1) + expect(engine).to have_received(:print_error).with(/Invalid chain mode 'invalid'/) + end + + it 'prints error and returns when rule has invalid chain_mode' do + hb = double('HookedBrowser', session: 'sess1') + allow(BeEF::HBManager).to receive(:get_by_id).with(1).and_return(hb) + rule = double( + 'Rule', + modules: '[]', + execution_order: '[]', + execution_delay: '[]', + chain_mode: 'invalid' + ) + allow(BeEF::Core::Models::Rule).to receive(:find).with(1).and_return(rule) + engine.run_rules_on_zombie([1], 1) + expect(engine).to have_received(:print_error).with(/Invalid chain mode 'invalid'/) + end + end end From 65395cdd4d8499b86216429c088c2b5d415a35d5 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 17:55:05 +1000 Subject: [PATCH 13/18] TEST: more logger spec unit tests --- spec/beef/core/main/logger_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/beef/core/main/logger_spec.rb b/spec/beef/core/main/logger_spec.rb index c3481331b..5bb6a3c8c 100644 --- a/spec/beef/core/main/logger_spec.rb +++ b/spec/beef/core/main/logger_spec.rb @@ -58,5 +58,18 @@ RSpec.describe BeEF::Core::Logger do 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 From 30ae29e30d6b11c0ccbd6237f07163bea3121814 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 17:56:22 +1000 Subject: [PATCH 14/18] TEST: additional specs for command and commandline spec --- spec/beef/core/filter/command_spec.rb | 15 ++- .../core/main/console/commandline_spec.rb | 126 ++++++++++++++++++ 2 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 spec/beef/core/main/console/commandline_spec.rb diff --git a/spec/beef/core/filter/command_spec.rb b/spec/beef/core/filter/command_spec.rb index 1cb4dda42..c15d2759f 100644 --- a/spec/beef/core/filter/command_spec.rb +++ b/spec/beef/core/filter/command_spec.rb @@ -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 diff --git a/spec/beef/core/main/console/commandline_spec.rb b/spec/beef/core/main/console/commandline_spec.rb new file mode 100644 index 000000000..2804b6b30 --- /dev/null +++ b/spec/beef/core/main/console/commandline_spec.rb @@ -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 From 0ceb550409cc0fbee20822b709b2f1a42683c754 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 30 Jan 2026 17:58:24 +1000 Subject: [PATCH 15/18] TEST: more server spec --- spec/beef/core/main/server_spec.rb | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/spec/beef/core/main/server_spec.rb b/spec/beef/core/main/server_spec.rb index 71419d062..b19ddcf53 100644 --- a/spec/beef/core/main/server_spec.rb +++ b/spec/beef/core/main/server_spec.rb @@ -136,5 +136,42 @@ RSpec.describe BeEF::Core::Server 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 From 088b2441094ff942fa5b9ee76b2d0cdbab5754fc Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Mon, 2 Feb 2026 11:00:50 +1000 Subject: [PATCH 16/18] TEST: hookerbrowser and browserdetails spec --- .../core/main/handlers/browserdetails_spec.rb | 153 ++++++++++++++++++ .../core/main/handlers/hookedbrowsers_spec.rb | 125 ++++++++++++++ 2 files changed, 278 insertions(+) diff --git a/spec/beef/core/main/handlers/browserdetails_spec.rb b/spec/beef/core/main/handlers/browserdetails_spec.rb index 6cf623346..c773633e1 100644 --- a/spec/beef/core/main/handlers/browserdetails_spec.rb +++ b/spec/beef/core/main/handlers/browserdetails_spec.rb @@ -413,5 +413,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 diff --git a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb index 0bfa9ead0..aae0d90f0 100644 --- a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb +++ b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb @@ -10,6 +10,131 @@ RSpec.describe BeEF::Core::Handlers::HookedBrowsers do # .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 + + 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 + + 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::Core::Models::Execution).to receive(:where).with(is_sent: false, session_id: 'existing_session').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']) From 82c57b4ea126bc4c6d89a268c6fcbae0bdbf1637 Mon Sep 17 00:00:00 2001 From: Jake Webster Date: Fri, 27 Feb 2026 11:40:44 +1000 Subject: [PATCH 17/18] FIX: unused variable --- spec/beef/core/main/models/command_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/beef/core/main/models/command_spec.rb b/spec/beef/core/main/models/command_spec.rb index 2cec84b98..0a6d75d03 100644 --- a/spec/beef/core/main/models/command_spec.rb +++ b/spec/beef/core/main/models/command_spec.rb @@ -113,7 +113,7 @@ RSpec.describe BeEF::Core::Models::Command do end it 'raises TypeError when command is not found for id and hooked_browser' do - other_hb = BeEF::Core::Models::HookedBrowser.create!(session: 'other_session', ip: '127.0.0.1') + 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) From c7985cdf51bd6e8396575affe60deea7960ad859 Mon Sep 17 00:00:00 2001 From: zinduolis Date: Fri, 22 May 2026 15:54:11 +1000 Subject: [PATCH 18/18] TEST: remove autorun_engine specs (ARE removed in #3482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #3482 deleted the AutorunEngine source code (engine.rb, parser.rb, rule_loader.rb) along with the Rule and Execution models. The three new spec files added by this PR were testing classes that no longer exist on master, so they would fail at load time once #3482 merged. Also drop a leftover BeEF::Core::Models::Execution stub in hookedbrowsers_spec.rb — the handler no longer queries pending ARE executions, so the stub is dead. Removed: spec/beef/core/main/autorun_engine/engine_spec.rb (359 lines) spec/beef/core/main/autorun_engine/parser_spec.rb (119 lines) spec/beef/core/main/autorun_engine/rule_loader_spec.rb ( 83 lines) Modified: spec/beef/core/main/handlers/hookedbrowsers_spec.rb (-1 dead stub) --- .../core/main/autorun_engine/engine_spec.rb | 359 ------------------ .../core/main/autorun_engine/parser_spec.rb | 119 ------ .../main/autorun_engine/rule_loader_spec.rb | 83 ---- .../core/main/handlers/hookedbrowsers_spec.rb | 1 - 4 files changed, 562 deletions(-) delete mode 100644 spec/beef/core/main/autorun_engine/engine_spec.rb delete mode 100644 spec/beef/core/main/autorun_engine/parser_spec.rb delete mode 100644 spec/beef/core/main/autorun_engine/rule_loader_spec.rb diff --git a/spec/beef/core/main/autorun_engine/engine_spec.rb b/spec/beef/core/main/autorun_engine/engine_spec.rb deleted file mode 100644 index 357af25f7..000000000 --- a/spec/beef/core/main/autorun_engine/engine_spec.rb +++ /dev/null @@ -1,359 +0,0 @@ -# -# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net -# Browser Exploitation Framework (BeEF) - https://beefproject.com -# See the file 'doc/COPYING' for copying permission -# -# Example: unit specs for AutorunEngine::Engine using mocks instead of a real server/DB. -# - -require 'spec_helper' - -RSpec.describe BeEF::Core::AutorunEngine::Engine do - let(:engine) { described_class.instance } - let(:config) { BeEF::Core::Configuration.instance } - - before do - allow(engine).to receive(:print_debug) - allow(engine).to receive(:print_info) - allow(engine).to receive(:print_more) - allow(engine).to receive(:print_error) - end - - # Fake rule object (could be a double or a persisted Rule with minimal attributes) - def rule_with(browser: 'ALL', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') - double( - 'Rule', - id: 1, - browser: browser, - browser_version: browser_version, - os: os, - os_version: os_version - ) - end - - describe '#zombie_matches_rule?' do - it 'returns false when rule is nil' do - expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', nil)).to be false - end - - it 'returns true when rule is ALL for browser and OS' do - rule = rule_with(browser: 'ALL', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') - allow(engine).to receive(:zombie_browser_matches_rule?).with('FF', '41', rule).and_return(true) - allow(engine).to receive(:zombie_os_matches_rule?).with('Windows', '7', rule).and_return(true) - expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', rule)).to be true - end - - it 'returns false when browser does not match' do - rule = rule_with(browser: 'FF', browser_version: '>= 41', os: 'ALL', os_version: 'ALL') - allow(engine).to receive(:zombie_browser_matches_rule?).with('FF', '41', rule).and_return(false) - expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', rule)).to be false - end - - it 'returns false when OS does not match' do - rule = rule_with(browser: 'ALL', browser_version: 'ALL', os: 'Windows', os_version: '7') - allow(engine).to receive(:zombie_browser_matches_rule?).with('FF', '41', rule).and_return(true) - allow(engine).to receive(:zombie_os_matches_rule?).with('Windows', '7', rule).and_return(false) - expect(engine.zombie_matches_rule?('FF', '41', 'Windows', '7', rule)).to be false - end - end - - describe '#zombie_os_matches_rule?' do - it 'returns false when rule is nil' do - expect(engine.zombie_os_matches_rule?('Windows', '7', nil)).to be false - end - - it 'returns true when rule os is ALL' do - rule = double('Rule', os: 'ALL', os_version: 'ALL') - expect(engine.zombie_os_matches_rule?('Windows', '7', rule)).to be true - end - - it 'returns false when hook os does not match rule os' do - rule = double('Rule', os: 'Linux', os_version: 'ALL') - expect(engine.zombie_os_matches_rule?('Windows', '7', rule)).to be false - end - - it 'returns true when rule os matches and os_version is ALL' do - rule = double('Rule', os: 'Windows', os_version: 'ALL') - expect(engine.zombie_os_matches_rule?('Windows', '7', rule)).to be true - end - end - - describe '#zombie_browser_matches_rule?' do - it 'returns false when rule is nil' do - expect(engine.zombie_browser_matches_rule?('FF', '41', nil)).to be false - end - - it 'returns true when rule browser is ALL and version is ALL' do - rule = double('Rule', browser: 'ALL', browser_version: 'ALL') - expect(engine.zombie_browser_matches_rule?('FF', '41', rule)).to be true - end - - it 'returns true when rule browser matches and version is ALL' do - rule = double('Rule', browser: 'FF', browser_version: 'ALL') - expect(engine.zombie_browser_matches_rule?('FF', '41', rule)).to be true - end - - it 'returns false when rule browser does not match' do - rule = double('Rule', browser: 'IE', browser_version: 'ALL') - expect(engine.zombie_browser_matches_rule?('FF', '41', rule)).to be false - end - end - - describe '#find_matching_rules_for_zombie' do - it 'returns nil when no rules exist' do - allow(BeEF::Core::Models::Rule).to receive(:all).and_return([]) - expect(engine.find_matching_rules_for_zombie('FF', '41', 'Windows', '7')).to be_nil - end - - it 'returns matching rule ids when rules match zombie' do - rule1 = double('Rule', id: 1, name: 'Rule1', browser: 'ALL', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') - rule2 = double('Rule', id: 2, name: 'Rule2', browser: 'IE', browser_version: 'ALL', os: 'ALL', os_version: 'ALL') - allow(BeEF::Core::Models::Rule).to receive(:all).and_return([rule1, rule2]) - allow(engine).to receive(:zombie_matches_rule?).with('FF', '41', 'Windows', '7', rule1).and_return(true) - allow(engine).to receive(:zombie_matches_rule?).with('FF', '41', 'Windows', '7', rule2).and_return(false) - expect(engine.find_matching_rules_for_zombie('FF', '41', 'Windows', '7')).to eq([1]) - end - end - - describe '#compare_versions' do - it 'returns true when cond is ALL' do - expect(engine.send(:compare_versions, '7', 'ALL', '8')).to be true - end - - it 'returns true when cond is == and versions equal' do - expect(engine.send(:compare_versions, '41', '==', '41')).to be true - end - - it 'returns false when cond is == and versions differ' do - expect(engine.send(:compare_versions, '41', '==', '42')).to be false - end - - it 'returns true when cond is <= and ver_a <= ver_b' do - expect(engine.send(:compare_versions, '41', '<=', '42')).to be true - expect(engine.send(:compare_versions, '41', '<=', '41')).to be true - end - - it 'returns false when cond is <= and ver_a > ver_b' do - expect(engine.send(:compare_versions, '42', '<=', '41')).to be false - end - - it 'returns true when cond is < and ver_a < ver_b' do - expect(engine.send(:compare_versions, '41', '<', '42')).to be true - end - - it 'returns false when cond is < and ver_a >= ver_b' do - expect(engine.send(:compare_versions, '42', '<', '41')).to be false - expect(engine.send(:compare_versions, '41', '<', '41')).to be false - end - - it 'returns true when cond is >= and ver_a >= ver_b' do - expect(engine.send(:compare_versions, '42', '>=', '41')).to be true - expect(engine.send(:compare_versions, '41', '>=', '41')).to be true - end - - it 'returns true when cond is > and ver_a > ver_b' do - expect(engine.send(:compare_versions, '42', '>', '41')).to be true - end - - it 'returns false when cond is > and ver_a <= ver_b' do - expect(engine.send(:compare_versions, '41', '>', '42')).to be false - expect(engine.send(:compare_versions, '41', '>', '41')).to be false - end - - it 'returns false for unknown cond' do - expect(engine.send(:compare_versions, '41', '!=', '42')).to be false - end - end - - describe '#clean_command_body' do - it 'extracts body range and replaces single-quoted mod_input when replace_input is true' do - body = "beef.execute(function(){\n alert('<>');\n});\n" - result = engine.send(:clean_command_body, body, true) - expect(result).to include('alert(mod_input)') - expect(result).to include('beef.execute(function(){') - end - - it 'returns cleaned body without mod_input replacement when replace_input is false' do - body = "beef.execute(function(){\n doSomething('<>');\n});\n" - result = engine.send(:clean_command_body, body, false) - expect(result).to include('<>') - end - - it 'replaces double-quoted <> with mod_input when replace_input is true' do - body = "beef.execute(function(){\n x(\"<>\");\n});\n" - result = engine.send(:clean_command_body, body, true) - expect(result).to include('mod_input') - expect(result).not_to include('"<>"') - end - - it 'replaces single-quoted <> with mod_input when replace_input is true' do - body = "beef.execute(function(){\n x('<>');\n});\n" - result = engine.send(:clean_command_body, body, true) - expect(result).to include('mod_input') - end - end - - describe '#prepare_sequential_wrapper' do - it 'builds wrapper with mod bodies and setTimeout calls in order' do - mods = [ - { mod_name: 'mod_a', mod_body: 'var mod_a_mod_output = 1;' }, - { mod_name: 'mod_b', mod_body: 'var mod_b_mod_output = 2;' } - ] - order = [0, 1] - delay = [0, 500] - token = 't1' - result = engine.send(:prepare_sequential_wrapper, mods, order, delay, token) - expect(result).to include('mod_a_t1') - expect(result).to include('mod_b_t1') - expect(result).to include('setTimeout(function(){mod_a_t1();}, 0)') - expect(result).to include('setTimeout(function(){mod_b_t1();}, 500)') - expect(result).to include('mod_a_t1_mod_output') - expect(result).to include('mod_b_t1_mod_output') - end - - it 'handles single module' do - mods = [{ mod_name: 'single', mod_body: 'x();' }] - order = [0] - delay = [0] - result = engine.send(:prepare_sequential_wrapper, mods, order, delay, 'tk') - expect(result).to include('single_tk') - expect(result).to include('setTimeout(function(){single_tk();}, 0)') - end - end - - describe '#prepare_nested_forward_wrapper' do - it 'builds wrapper for single module' do - mods = [{ mod_name: 'only', mod_body: 'only();' }] - code = ['null'] - conditions = [true] - order = [0] - token = 'nf1' - result = engine.send(:prepare_nested_forward_wrapper, mods, code, conditions, order, token) - expect(result).to include('only_nf1') - expect(result).to include('only_nf1_f') - expect(result).to include('only_nf1_mod_output') - end - end - - describe '#find_and_run_all_matching_rules_for_zombie' do - it 'returns without calling run_rules when hb_id is nil' do - expect(engine).not_to receive(:run_rules_on_zombie) - engine.find_and_run_all_matching_rules_for_zombie(nil) - end - - it 'returns without calling run_rules when find_matching_rules returns nil' do - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') - allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return(nil) - expect(engine).not_to receive(:run_rules_on_zombie) - engine.find_and_run_all_matching_rules_for_zombie(1) - end - - it 'returns without calling run_rules when find_matching_rules returns empty' do - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') - allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([]) - expect(engine).not_to receive(:run_rules_on_zombie) - engine.find_and_run_all_matching_rules_for_zombie(1) - end - - it 'calls run_rules_on_zombie with matching rule ids when rules match' do - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') - allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([1, 2]) - expect(engine).to receive(:run_rules_on_zombie).with([1, 2], 1) - engine.find_and_run_all_matching_rules_for_zombie(1) - end - end - - describe '#run_matching_rules_on_zombie' do - it 'returns when rule_ids is nil' do - expect(engine).not_to receive(:run_rules_on_zombie) - engine.run_matching_rules_on_zombie(nil, 1) - end - - it 'returns when hb_id is nil' do - expect(engine).not_to receive(:run_rules_on_zombie) - engine.run_matching_rules_on_zombie([1], nil) - end - - it 'returns without calling run_rules when find_matching_rules returns nil' do - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') - allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return(nil) - expect(engine).not_to receive(:run_rules_on_zombie) - engine.run_matching_rules_on_zombie([1], 1) - end - - it 'calls run_rules_on_zombie with intersection of rule_ids and matching rules' do - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') - allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([1, 2]) - expect(engine).to receive(:run_rules_on_zombie).with([1], 1) - engine.run_matching_rules_on_zombie([1], 1) - end - - it 'does not call run_rules_on_zombie when no rule_ids overlap matching rules' do - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.name').and_return('FF') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'browser.version').and_return('41') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.name').and_return('Windows') - allow(BeEF::Core::Models::BrowserDetails).to receive(:get).with(1, 'host.os.version').and_return('7') - allow(engine).to receive(:find_matching_rules_for_zombie).with('FF', '41', 'Windows', '7').and_return([1, 2]) - expect(engine).not_to receive(:run_rules_on_zombie) - engine.run_matching_rules_on_zombie([99], 1) - end - end - - describe '#run_rules_on_zombie' do - it 'returns when rule_ids is nil' do - expect(BeEF::HBManager).not_to receive(:get_by_id) - engine.run_rules_on_zombie(nil, 1) - end - - it 'returns when hb_id is nil' do - expect(BeEF::HBManager).not_to receive(:get_by_id) - engine.run_rules_on_zombie([1], nil) - end - - it 'normalizes single Integer rule_id to array and processes rule' do - hb = double('HookedBrowser', session: 'sess1') - allow(BeEF::HBManager).to receive(:get_by_id).with(1).and_return(hb) - rule = double( - 'Rule', - modules: '[]', - execution_order: '[]', - execution_delay: '[]', - chain_mode: 'invalid' - ) - allow(BeEF::Core::Models::Rule).to receive(:find).with(1).and_return(rule) - engine.run_rules_on_zombie(1, 1) - expect(BeEF::Core::Models::Rule).to have_received(:find).with(1) - expect(engine).to have_received(:print_error).with(/Invalid chain mode 'invalid'/) - end - - it 'prints error and returns when rule has invalid chain_mode' do - hb = double('HookedBrowser', session: 'sess1') - allow(BeEF::HBManager).to receive(:get_by_id).with(1).and_return(hb) - rule = double( - 'Rule', - modules: '[]', - execution_order: '[]', - execution_delay: '[]', - chain_mode: 'invalid' - ) - allow(BeEF::Core::Models::Rule).to receive(:find).with(1).and_return(rule) - engine.run_rules_on_zombie([1], 1) - expect(engine).to have_received(:print_error).with(/Invalid chain mode 'invalid'/) - end - end -end diff --git a/spec/beef/core/main/autorun_engine/parser_spec.rb b/spec/beef/core/main/autorun_engine/parser_spec.rb deleted file mode 100644 index 9ab37d85a..000000000 --- a/spec/beef/core/main/autorun_engine/parser_spec.rb +++ /dev/null @@ -1,119 +0,0 @@ -# -# 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::AutorunEngine::Parser do - let(:parser) { described_class.instance } - - def valid_minimal_args - { - name: 'Test Rule', - author: 'Test Author', - browser: 'ALL', - browser_version: 'ALL', - os: 'Windows', - os_version: 'ALL', - modules: [], - execution_order: [], - execution_delay: [], - chain_mode: 'sequential' - } - end - - describe '#parse' do - it 'returns true for valid minimal args (empty modules)' do - result = parser.parse( - valid_minimal_args[:name], - valid_minimal_args[:author], - valid_minimal_args[:browser], - valid_minimal_args[:browser_version], - valid_minimal_args[:os], - valid_minimal_args[:os_version], - valid_minimal_args[:modules], - valid_minimal_args[:execution_order], - valid_minimal_args[:execution_delay], - valid_minimal_args[:chain_mode] - ) - - expect(result).to be true - end - - it 'raises ArgumentError for empty name' do - expect do - parser.parse('', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') - end.to raise_error(ArgumentError, /Invalid rule name/) - end - - it 'raises ArgumentError for nil name' do - expect do - parser.parse(nil, 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') - end.to raise_error(ArgumentError, /Invalid rule name/) - end - - it 'raises ArgumentError for empty author' do - expect do - parser.parse('Name', '', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') - end.to raise_error(ArgumentError, /Invalid author name/) - end - - it 'raises ArgumentError for invalid chain_mode' do - expect do - parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'invalid') - end.to raise_error(ArgumentError, /Invalid chain_mode definition/) - end - - it 'raises ArgumentError for invalid os' do - expect do - parser.parse('Name', 'Author', 'ALL', 'ALL', 'InvalidOS', 'ALL', [], [], [], 'sequential') - end.to raise_error(ArgumentError, /Invalid os definition/) - end - - it 'raises ArgumentError when execution_delay size does not match modules size' do - expect do - parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{ 'name' => 'a' }], [1], [], 'sequential') - end.to raise_error(ArgumentError, /execution_delay.*consistent with number of modules/) - end - - it 'raises ArgumentError when execution_order size does not match modules size' do - expect do - parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{ 'name' => 'a' }], [], [0], 'sequential') - end.to raise_error(ArgumentError, /execution_order.*consistent with number of modules/) - end - - it 'raises TypeError when execution_delay contains non-Integer' do - # Use one module so sizes match; then type check runs on execution_delay - expect do - parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{}], [1], ['not_an_int'], 'sequential') - end.to raise_error(TypeError, /execution_delay.*Integers/) - end - - it 'raises TypeError when execution_order contains non-Integer' do - # Use one module so sizes match; then type check runs on execution_order - expect do - parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [{}], ['x'], [0], 'sequential') - end.to raise_error(TypeError, /execution_order.*Integers/) - end - - it 'raises ArgumentError for invalid browser' do - expect do - parser.parse('Name', 'Author', 'XX', 'ALL', 'Windows', 'ALL', [], [], [], 'sequential') - end.to raise_error(ArgumentError, /Invalid browser definition/) - end - - it 'accepts nested-forward as chain_mode' do - result = parser.parse('Name', 'Author', 'ALL', 'ALL', 'Windows', 'ALL', [], [], [], 'nested-forward') - expect(result).to be true - end - - it 'accepts valid os values' do - %w[Linux Windows OSX Android iOS BlackBerry ALL].each do |os| - result = parser.parse('Name', 'Author', 'ALL', 'ALL', os, 'ALL', [], [], [], 'sequential') - expect(result).to be true - end - end - end -end diff --git a/spec/beef/core/main/autorun_engine/rule_loader_spec.rb b/spec/beef/core/main/autorun_engine/rule_loader_spec.rb deleted file mode 100644 index a27560b68..000000000 --- a/spec/beef/core/main/autorun_engine/rule_loader_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -# -# 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::AutorunEngine::RuleLoader do - let(:loader) { described_class.instance } - - def valid_rule_data - { - 'name' => 'Test Rule', - 'author' => 'Test Author', - 'browser' => 'ALL', - 'browser_version' => 'ALL', - 'os' => 'Windows', - 'os_version' => 'ALL', - 'modules' => [], - 'execution_order' => [], - 'execution_delay' => [], - 'chain_mode' => 'sequential' - } - end - - before do - allow(loader).to receive(:print_error) - allow(loader).to receive(:print_info) - allow(loader).to receive(:print_more) - end - - describe '#load_rule_json' do - it 'returns success and rule_id when parse succeeds and rule is new' do - # Parser will succeed with valid minimal data; no existing rule - result = loader.load_rule_json(valid_rule_data) - - expect(result['success']).to be true - expect(result).to have_key('rule_id') - expect(result['rule_id']).to be_a(Integer) - end - - it 'returns success false and error when parse raises' do - allow(BeEF::Core::AutorunEngine::Parser.instance).to receive(:parse).and_raise(ArgumentError.new('Invalid rule name')) - - result = loader.load_rule_json(valid_rule_data.merge('name' => 'x')) - - expect(result['success']).to be false - expect(result['error']).to include('Invalid rule name') - end - - it 'returns success false and error when rule already exists' do - # Create the rule first so it already exists - BeEF::Core::Models::Rule.create!( - name: 'Duplicate Rule', - author: 'Test Author', - browser: 'ALL', - browser_version: 'ALL', - os: 'Windows', - os_version: 'ALL', - modules: [].to_json, - execution_order: [].to_s, - execution_delay: [].to_s, - chain_mode: 'sequential' - ) - - result = loader.load_rule_json( - valid_rule_data.merge('name' => 'Duplicate Rule') - ) - - expect(result['success']).to be false - expect(result['error']).to include('Duplicate rule already exists') - end - - it 'uses default chain_mode sequential when missing' do - data = valid_rule_data.except('chain_mode') - result = loader.load_rule_json(data) - - expect(result['success']).to be true - expect(result).to have_key('rule_id') - end - end -end diff --git a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb index aae0d90f0..d3953f6c8 100644 --- a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb +++ b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb @@ -111,7 +111,6 @@ RSpec.describe BeEF::Core::Handlers::HookedBrowsers do 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::Core::Models::Execution).to receive(:where).with(is_sent: false, session_id: 'existing_session').and_return([]) allow(BeEF::API::Registrar.instance).to receive(:fire) end