diff --git a/core/filters/browser.rb b/core/filters/browser.rb
index 2d7d1e305..c7c4cb1fa 100644
--- a/core/filters/browser.rb
+++ b/core/filters/browser.rb
@@ -8,7 +8,7 @@ module BeEF
# Check the browser type value - for example, 'FF'
# @param [String] str String for testing
# @return [Boolean] If the string has valid browser name characters
- def self.is_valid_browsername?(str)
+ def self.is_valid_browsername?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if str.length > 2
return false if has_non_printable_char?(str)
@@ -19,7 +19,7 @@ module BeEF
# Check the Operating System name value - for example, 'Windows XP'
# @param [String] str String for testing
# @return [Boolean] If the string has valid Operating System name characters
- def self.is_valid_osname?(str)
+ def self.is_valid_osname?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length < 2
@@ -30,7 +30,7 @@ module BeEF
# Check the Hardware name value - for example, 'iPhone'
# @param [String] str String for testing
# @return [Boolean] If the string has valid Hardware name characters
- def self.is_valid_hwname?(str)
+ def self.is_valid_hwname?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length < 2
@@ -41,12 +41,12 @@ module BeEF
# Verify the browser version string is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid browser version characters
- def self.is_valid_browserversion?(str)
+ def self.is_valid_browserversion?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return true if str.eql? 'UNKNOWN'
return true if str.eql? 'ALL'
- return false if !nums_only?(str) and !str.match(/\A(0|[1-9][0-9]{0,3})(\.(0|[1-9][0-9]{0,3})){0,3}\z/)
+ return false if !nums_only?(str) && !str.match(/\A(0|[1-9][0-9]{0,3})(\.(0|[1-9][0-9]{0,3})){0,3}\z/)
return false if str.length > 20
true
@@ -55,7 +55,7 @@ module BeEF
# Verify the os version string is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid os version characters
- def self.is_valid_osversion?(str)
+ def self.is_valid_osversion?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return true if str.eql? 'UNKNOWN'
@@ -69,7 +69,7 @@ module BeEF
# Verify the browser/UA string is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid browser / ua string characters
- def self.is_valid_browserstring?(str)
+ def self.is_valid_browserstring?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 300
@@ -80,7 +80,7 @@ module BeEF
# Verify the cookies are valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid cookie characters
- def self.is_valid_cookies?(str)
+ def self.is_valid_cookies?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 2000
@@ -91,7 +91,7 @@ module BeEF
# Verify the system platform is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid system platform characters
- def self.is_valid_system_platform?(str)
+ def self.is_valid_system_platform?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 200
@@ -102,7 +102,7 @@ module BeEF
# Verify the date stamp is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid date stamp characters
- def self.is_valid_date_stamp?(str)
+ def self.is_valid_date_stamp?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 200
@@ -113,7 +113,7 @@ module BeEF
# Verify the CPU type string is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid CPU type characters
- def self.is_valid_cpu?(str)
+ def self.is_valid_cpu?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 200
@@ -124,7 +124,7 @@ module BeEF
# Verify the memory string is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid memory type characters
- def self.is_valid_memory?(str)
+ def self.is_valid_memory?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 200
@@ -135,7 +135,7 @@ module BeEF
# Verify the GPU type string is valid
# @param [String] str String for testing
# @return [Boolean] If the string has valid GPU type characters
- def self.is_valid_gpu?(str)
+ def self.is_valid_gpu?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if has_non_printable_char?(str)
return false if str.length > 200
@@ -148,11 +148,11 @@ module BeEF
# @return [Boolean] If the string has valid browser plugin characters
# @note This string can be empty if there are no browser plugins
# @todo Verify if the ruby version statement is still necessary
- def self.is_valid_browser_plugins?(str)
+ def self.is_valid_browser_plugins?(str) # rubocop:disable Naming/PredicatePrefix
return false unless is_non_empty_string?(str)
return false if str.length > 1000
- if str.encoding === Encoding.find('UTF-8')
+ if str.encoding == Encoding.find('UTF-8') # Style/CaseEquality: Avoid the use of the case equality operator `===`.
(str =~ /[^\w\d\s()-.,';_!\302\256]/u).nil?
else
(str =~ /[^\w\d\s()-.,';_!\302\256]/n).nil?
diff --git a/modules/social_engineering/text_to_voice/module.rb b/modules/social_engineering/text_to_voice/module.rb
index 4c897c7c4..4cce5d437 100644
--- a/modules/social_engineering/text_to_voice/module.rb
+++ b/modules/social_engineering/text_to_voice/module.rb
@@ -4,17 +4,23 @@
# See the file 'doc/COPYING' for copying permission
#
class Text_to_voice < BeEF::Core::Command
- require 'espeak'
- include ESpeak
-
def pre_send
- # Ensure lame and espeak are installed
- if IO.popen(%w[which lame], 'r').read.to_s.eql?('')
- print_error('[Text to Voice] Lame is not in $PATH (apt-get install lame)')
+ # Check for required binaries
+ if IO.popen(%w[which espeak], 'r').read.to_s.eql?('')
+ print_error('[Text to Voice] eSpeak is not in $PATH (brew install espeak on macOS, apt-get install espeak on Linux)')
return
end
- if IO.popen(%w[which espeak], 'r').read.to_s.eql?('')
- print_error('[Text to Voice] eSpeak is not in $PATH (apt-get install espeak)')
+ if IO.popen(%w[which lame], 'r').read.to_s.eql?('')
+ print_error('[Text to Voice] Lame is not in $PATH (brew install lame on macOS, apt-get install lame on Linux)')
+ return
+ end
+
+ # Load espeak gem (only if binaries are available)
+ begin
+ require 'espeak'
+ include ESpeak
+ rescue LoadError, StandardError => e
+ print_error("[Text to Voice] Failed to load espeak gem: #{e.message}")
return
end
@@ -25,9 +31,16 @@ class Text_to_voice < BeEF::Core::Command
message = input['value'] if input['name'] == 'message'
language = input['value'] if input['name'] == 'language'
end
- unless Voice.all.map(&:language).include?(language)
- print_error("[Text to Voice] Language '#{language}' is not supported")
- print_more("Supported languages: #{Voice.all.map(&:language).join(',')}")
+
+ # Validate language
+ begin
+ unless Voice.all.map(&:language).include?(language)
+ print_error("[Text to Voice] Language '#{language}' is not supported")
+ print_more("Supported languages: #{Voice.all.map(&:language).join(',')}")
+ return
+ end
+ rescue StandardError => e
+ print_error("[Text to Voice] Could not validate language: #{e.message}")
return
end
diff --git a/spec/beef/core/extension_spec.rb b/spec/beef/core/extension_spec.rb
new file mode 100644
index 000000000..74b240a48
--- /dev/null
+++ b/spec/beef/core/extension_spec.rb
@@ -0,0 +1,100 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Extension do
+ let(:config) { BeEF::Core::Configuration.instance }
+
+ describe '.is_present' do
+ it 'returns true when extension exists in configuration' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ expect(described_class.is_present('test_ext')).to be true
+ end
+
+ it 'returns false when extension does not exist' do
+ allow(config).to receive(:get).with('beef.extension').and_return({})
+ expect(described_class.is_present('nonexistent')).to be false
+ end
+
+ it 'converts extension key to string' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ expect(described_class.is_present(:test_ext)).to be true
+ end
+ end
+
+ describe '.is_enabled' do
+ it 'returns true when extension is present and enabled' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ allow(config).to receive(:get).with('beef.extension.test_ext.enable').and_return(true)
+ expect(described_class.is_enabled('test_ext')).to be true
+ end
+
+ it 'returns false when extension is not present' do
+ allow(config).to receive(:get).with('beef.extension').and_return({})
+ expect(described_class.is_enabled('nonexistent')).to be false
+ end
+
+ it 'returns false when extension is disabled' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ allow(config).to receive(:get).with('beef.extension.test_ext.enable').and_return(false)
+ expect(described_class.is_enabled('test_ext')).to be false
+ end
+ end
+
+ describe '.is_loaded' do
+ it 'returns true when extension is enabled and loaded' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ allow(config).to receive(:get).with('beef.extension.test_ext.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.extension.test_ext.loaded').and_return(true)
+ expect(described_class.is_loaded('test_ext')).to be true
+ end
+
+ it 'returns false when extension is not enabled' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ allow(config).to receive(:get).with('beef.extension.test_ext.enable').and_return(false)
+ expect(described_class.is_loaded('test_ext')).to be false
+ end
+
+ it 'returns false when extension is not loaded' do
+ allow(config).to receive(:get).with('beef.extension').and_return({ 'test_ext' => {} })
+ allow(config).to receive(:get).with('beef.extension.test_ext.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.extension.test_ext.loaded').and_return(false)
+ expect(described_class.is_loaded('test_ext')).to be false
+ end
+ end
+
+ describe '.load' do
+ it 'returns true when extension file exists' do
+ ext_path = "#{$root_dir}/extensions/test_ext/extension.rb"
+ allow(File).to receive(:exist?).with(ext_path).and_return(true)
+ allow(config).to receive(:set).with('beef.extension.test_ext.loaded', true).and_return(true)
+ # Stub require on the module itself since it's called directly
+ allow(described_class).to receive(:require).with(ext_path)
+ expect(described_class.load('test_ext')).to be true
+ end
+
+ it 'returns false when extension file does not exist' do
+ ext_path = "#{$root_dir}/extensions/test_ext/extension.rb"
+ allow(File).to receive(:exist?).with(ext_path).and_return(false)
+ expect(described_class.load('test_ext')).to be false
+ end
+
+ it 'sets loaded flag to true when successfully loaded' do
+ ext_path = "#{$root_dir}/extensions/test_ext/extension.rb"
+ allow(File).to receive(:exist?).with(ext_path).and_return(true)
+ allow(described_class).to receive(:require).with(ext_path)
+ expect(config).to receive(:set).with('beef.extension.test_ext.loaded', true).and_return(true)
+ described_class.load('test_ext')
+ end
+
+ it 'handles errors during loading gracefully' do
+ ext_path = "#{$root_dir}/extensions/test_ext/extension.rb"
+ allow(File).to receive(:exist?).with(ext_path).and_return(true)
+ allow(described_class).to receive(:require).with(ext_path).and_raise(StandardError.new('Load error'))
+ # The rescue block calls print_more which may return a value, so just verify it doesn't raise
+ expect { described_class.load('test_ext') }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/beef/core/extensions_spec.rb b/spec/beef/core/extensions_spec.rb
index b1c22544f..0bd65edc5 100644
--- a/spec/beef/core/extensions_spec.rb
+++ b/spec/beef/core/extensions_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF Extensions' do
it 'loaded successfully' do
diff --git a/spec/beef/core/filter/base_spec.rb b/spec/beef/core/filter/base_spec.rb
new file mode 100644
index 000000000..6e54d0dea
--- /dev/null
+++ b/spec/beef/core/filter/base_spec.rb
@@ -0,0 +1,310 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Filters do
+ describe '.is_non_empty_string?' do
+ it 'nil' do
+ expect(BeEF::Filters.is_non_empty_string?(nil)).to be(false)
+ end
+
+ it 'Integer' do
+ expect(BeEF::Filters.is_non_empty_string?(1)).to be(false)
+ end
+
+ it 'Empty String' do
+ expect(BeEF::Filters.is_non_empty_string?('')).to be(false)
+ end
+
+ it 'null' do
+ expect(BeEF::Filters.is_non_empty_string?("\x00")).to be(true)
+ end
+
+ it 'First char is num' do
+ expect(BeEF::Filters.is_non_empty_string?('0')).to be(true)
+ end
+
+ it 'First char is alpha' do
+ expect(BeEF::Filters.is_non_empty_string?('A')).to be(true)
+ end
+
+ it 'Four num chars' do
+ expect(BeEF::Filters.is_non_empty_string?('3333')).to be(true)
+ end
+
+ it 'Four num chars begining with alpha' do
+ expect(BeEF::Filters.is_non_empty_string?('A3333')).to be(true)
+ end
+
+ it 'Four num chars begining with null' do
+ expect(BeEF::Filters.is_non_empty_string?("\x003333")).to be(true)
+ end
+ end
+
+ describe '.only?' do
+ it 'success' do
+ expect(BeEF::Filters.only?('A', 'A')).to be(true)
+ end
+
+ it 'fail' do
+ expect(BeEF::Filters.only?('A', 'B')).to be(false)
+ end
+ end
+
+ describe '.exists?' do
+ it 'success' do
+ expect(BeEF::Filters.exists?('A', 'A')).to be(true)
+ end
+
+ it 'fail' do
+ expect(BeEF::Filters.exists?('A', 'B')).to be(false)
+ end
+ end
+
+ describe '.has_null?' do
+ context 'false with' do
+ it 'general' do
+ chars = [nil, '', "\x01", "\xFF", 'A', 'A3333', '0', '}', '.', '+', '-', '-1', '0.A', '3333', '33 33', ' AAAAA', 'AAAAAA ']
+ chars.each do |c|
+ expect(BeEF::Filters.has_null?(c)).to be(false)
+ end
+ end
+
+ it 'alphabet' do
+ (1..255).each do |c|
+ str = ''
+ str.concat(c)
+ expect(BeEF::Filters.has_null?(str)).to be(false)
+ end
+ end
+ end
+
+ context 'true with' do
+ it 'general' do
+ chars = ["\x00", "A\x00", "AAAAAA\x00", "\x00A", "\x00AAAAAAAA", "A\x00A", "AAAAA\x00AAAA", "A\n\r\x00", "\x00\n\rA", "A\n\r\x00\n\rA", "\tA\x00A"]
+ chars.each do |c|
+ expect(BeEF::Filters.has_null?(c)).to be(true)
+ end
+ end
+
+ it 'alphabet null after' do
+ (1..255).each do |c|
+ str = ''
+ str.concat(c)
+ str += "\x00"
+ expect(BeEF::Filters.has_null?(str)).to be(true)
+ end
+ end
+
+ it 'alphabet null before' do
+ (1..255).each do |c|
+ str = "\x00"
+ str.concat(c)
+ expect(BeEF::Filters.has_null?(str)).to be(true)
+ end
+ end
+ end
+ end
+
+ describe '.has_non_printable_char?' do
+ context 'false with' do
+ it 'general' do
+ chars = [nil, '', 'A', 'A3333', '0', '}', '.', '+', '-', '-1', '0.A', '3333', ' 0AAAAA', ' 0AAA ']
+ chars.each do |c|
+ expect(BeEF::Filters.has_non_printable_char?(c)).to be(false)
+ end
+ end
+
+ it 'lowercase' do
+ ('a'..'z').each do |c|
+ expect(BeEF::Filters.has_non_printable_char?(c)).to be(false)
+ end
+ end
+
+ it 'uppercase' do
+ ('A'..'Z').each do |c|
+ expect(BeEF::Filters.has_non_printable_char?(c)).to be(false)
+ end
+ end
+
+ it 'numbers' do
+ ('0'..'9').each do |c|
+ expect(BeEF::Filters.has_non_printable_char?(c)).to be(false)
+ end
+ end
+ end
+
+ context 'true with' do
+ it 'general' do
+ chars = ["\x00", "\x01", "\x02", "A\x03", "\x04A", "\x0033333", "\x00AAAAAA", " AAAAA\x00", "\t\x00AAAAA", "\n\x00AAAAA", "\n\r\x00AAAAAAAAA", "AAAAAAA\x00AAAAAAA",
+ "\n\x00"]
+ chars.each do |c|
+ expect(BeEF::Filters.has_non_printable_char?(c)).to be(true)
+ end
+ end
+
+ it 'alphabet null before' do
+ (1..255).each do |c|
+ str = ''
+ str.concat(c)
+ str += "\x00"
+ expect(BeEF::Filters.has_non_printable_char?(str)).to be(true)
+ end
+ end
+ end
+ end
+
+ describe '.nums_only?' do
+ it 'false with general' do
+ chars = [nil, 1, '', 'A', 'A3333', "\x003333", '}', '.', '+', '-', '-1']
+ chars.each do |c|
+ expect(BeEF::Filters.nums_only?(c)).to be(false)
+ end
+ end
+
+ it 'true with general' do
+ chars = %w[0 333]
+ chars.each do |c|
+ expect(BeEF::Filters.nums_only?(c)).to be(true)
+ end
+ end
+ end
+
+ describe '.is_valid_float?' do
+ it 'false with general' do
+ chars = [nil, 1, '', 'A', 'A3333', "\x003333", '}', '.', '+', '-', '-1', '0', '333', '0.A']
+ chars.each do |c|
+ expect(BeEF::Filters.is_valid_float?(c)).to be(false)
+ end
+ end
+
+ it 'true with general' do
+ chars = ['33.33', '0.0', '1.0', '0.1']
+ chars.each do |c|
+ expect(BeEF::Filters.is_valid_float?(c)).to be(true)
+ end
+ end
+ end
+
+ describe '.hexs_only?' do
+ it 'false with general' do
+ chars = [nil, 1, '', "\x003333", '}', '.', '+', '-', '-1', '0.A', '33.33', '0.0', '1.0', '0.1']
+ chars.each do |c|
+ expect(BeEF::Filters.hexs_only?(c)).to be(false)
+ end
+ end
+
+ it 'true with general' do
+ chars = %w[0123456789ABCDEFabcdef 0 333 A33333 A]
+ chars.each do |c|
+ expect(BeEF::Filters.hexs_only?(c)).to be(true)
+ end
+ end
+ end
+
+ describe '.first_char_is_num?' do
+ it 'false with general' do
+ chars = ['', 'A', 'A33333', "\x0033333"]
+ chars.each do |c|
+ expect(BeEF::Filters.first_char_is_num?(c)).to be(false)
+ end
+ end
+
+ it 'true with general' do
+ chars = %w[333 0AAAAAA 0]
+ chars.each do |c|
+ expect(BeEF::Filters.first_char_is_num?(c)).to be(true)
+ end
+ end
+ end
+
+ describe '.has_whitespace_char?' do
+ it 'false with general' do
+ chars = ['', 'A', 'A33333', "\x0033333", '0', '}', '.', '+', '-', '-1', '0.A']
+ chars.each do |c|
+ expect(BeEF::Filters.has_whitespace_char?(c)).to be(false)
+ end
+ end
+
+ it 'true with general' do
+ chars = ['33 33', ' ', ' ', ' 0AAAAAAA', ' 0AAAAAAA ', "\t0AAAAAAA", "\n0AAAAAAAA"]
+ chars.each do |c|
+ expect(BeEF::Filters.has_whitespace_char?(c)).to be(true)
+ end
+ end
+ end
+
+ describe '.alphanums_only?' do
+ context 'false with' do
+ it 'general' do
+ chars = [nil, '', "\n", "\r", "\x01", '}', '.', '+', '-', '-1', 'ee-!@$%^&*}=0.A', '33 33', ' AAAA', 'AAA ']
+ chars.each do |c|
+ expect(BeEF::Filters.alphanums_only?(c)).to be(false)
+ end
+ end
+
+ it 'additional nulls' do
+ chars = ["\x00", "A\x00", "AAAAAAAAA\x00", "\x00A", "\x00AAAAAAAAA", "A\x00A", "AAAAAAAA\x00AAAAAAAA", "A\n\r\x00", "\x00\n\rA", "A\n\r\x00\n\rA", "\tA\x00A"]
+ chars.each do |c|
+ expect(BeEF::Filters.alphanums_only?(c)).to be(false)
+ end
+ end
+
+ it 'alphabet null after' do
+ (1..255).each do |c|
+ str = ''
+ str.concat(c)
+ str += "\x00"
+ expect(BeEF::Filters.alphanums_only?(str)).to be(false)
+ end
+ end
+
+ it 'alphabet null before' do
+ (1..255).each do |c|
+ str = "\x00"
+ str.concat(c)
+ expect(BeEF::Filters.alphanums_only?(str)).to be(false)
+ end
+ end
+
+ it 'alphabet around null' do
+ (1..255).each do |c|
+ str = ''
+ str.concat(c)
+ str += "\x00"
+ str.concat(c)
+ expect(BeEF::Filters.alphanums_only?(str)).to be(false)
+ end
+ end
+ end
+
+ context 'true with' do
+ it 'general' do
+ chars = %w[A A3333 0 3333]
+ chars.each do |c|
+ expect(BeEF::Filters.alphanums_only?(c)).to be(true)
+ end
+ end
+
+ it 'uppercase' do
+ ('A'..'Z').each do |c|
+ expect(BeEF::Filters.alphanums_only?(c)).to be(true)
+ end
+ end
+
+ it 'lowercase' do
+ ('a'..'z').each do |c|
+ expect(BeEF::Filters.alphanums_only?(c)).to be(true)
+ end
+ end
+
+ it 'numbers' do
+ ('0'..'9').each do |c|
+ expect(BeEF::Filters.alphanums_only?(c)).to be(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/beef/core/filter/browser_spec.rb b/spec/beef/core/filter/browser_spec.rb
new file mode 100644
index 000000000..cd58eb95b
--- /dev/null
+++ b/spec/beef/core/filter/browser_spec.rb
@@ -0,0 +1,76 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Filters do
+ describe '.is_valid_browsername?' do
+ it 'validates browser names' do
+ expect(BeEF::Filters.is_valid_browsername?('FF')).to be(true)
+ expect(BeEF::Filters.is_valid_browsername?('IE')).to be(true)
+ expect(BeEF::Filters.is_valid_browsername?('CH')).to be(true)
+ expect(BeEF::Filters.is_valid_browsername?('TOOLONG')).to be(false)
+ expect(BeEF::Filters.is_valid_browsername?('')).to be(false)
+ end
+ end
+
+ describe '.is_valid_osname?' do
+ it 'validates OS names' do
+ expect(BeEF::Filters.is_valid_osname?('Windows XP')).to be(true)
+ expect(BeEF::Filters.is_valid_osname?('A')).to be(false) # too short
+ expect(BeEF::Filters.is_valid_osname?('')).to be(false)
+ end
+ end
+
+ describe '.is_valid_hwname?' do
+ it 'validates hardware names' do
+ expect(BeEF::Filters.is_valid_hwname?('iPhone')).to be(true)
+ expect(BeEF::Filters.is_valid_hwname?('A')).to be(false) # too short
+ expect(BeEF::Filters.is_valid_hwname?('')).to be(false)
+ end
+ end
+
+ describe '.is_valid_browserversion?' do
+ it 'validates browser versions' do
+ expect(BeEF::Filters.is_valid_browserversion?('1.0')).to be(true)
+ expect(BeEF::Filters.is_valid_browserversion?('1.2.3.4')).to be(true)
+ expect(BeEF::Filters.is_valid_browserversion?('UNKNOWN')).to be(true)
+ expect(BeEF::Filters.is_valid_browserversion?('ALL')).to be(true)
+ expect(BeEF::Filters.is_valid_browserversion?('invalid')).to be(false)
+ end
+ end
+
+ describe '.is_valid_osversion?' do
+ it 'validates OS versions' do
+ expect(BeEF::Filters.is_valid_osversion?('10.0')).to be(true)
+ expect(BeEF::Filters.is_valid_osversion?('UNKNOWN')).to be(true)
+ expect(BeEF::Filters.is_valid_osversion?('ALL')).to be(true)
+ expect(BeEF::Filters.is_valid_osversion?('invalid!')).to be(false)
+ end
+ end
+
+ describe '.is_valid_browserstring?' do
+ it 'validates browser/UA strings' do
+ expect(BeEF::Filters.is_valid_browserstring?('Mozilla/5.0')).to be(true)
+ expect(BeEF::Filters.is_valid_browserstring?('A' * 300)).to be(true)
+ expect(BeEF::Filters.is_valid_browserstring?('A' * 301)).to be(false)
+ end
+ end
+
+ describe '.is_valid_cookies?' do
+ it 'validates cookie strings' do
+ expect(BeEF::Filters.is_valid_cookies?('session=abc123')).to be(true)
+ expect(BeEF::Filters.is_valid_cookies?('A' * 2000)).to be(true)
+ expect(BeEF::Filters.is_valid_cookies?('A' * 2001)).to be(false)
+ end
+ end
+
+ describe '.is_valid_browser_plugins?' do
+ it 'validates browser plugin strings' do
+ expect(BeEF::Filters.is_valid_browser_plugins?('Flash, Java')).to be(true)
+ expect(BeEF::Filters.is_valid_browser_plugins?('A' * 1000)).to be(true)
+ expect(BeEF::Filters.is_valid_browser_plugins?('A' * 1001)).to be(false)
+ end
+ end
+end
diff --git a/spec/beef/core/filter/command_spec.rb b/spec/beef/core/filter/command_spec.rb
new file mode 100644
index 000000000..1cb4dda42
--- /dev/null
+++ b/spec/beef/core/filter/command_spec.rb
@@ -0,0 +1,57 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Filters do
+ describe '.is_valid_path_info?' do
+ it 'validates path info' do
+ expect(BeEF::Filters.is_valid_path_info?('/path/to/resource')).to be(true)
+ expect(BeEF::Filters.is_valid_path_info?("\x00")).to be(false)
+ expect(BeEF::Filters.is_valid_path_info?(nil)).to be(false)
+ end
+ end
+
+ describe '.is_valid_hook_session_id?' do
+ it 'validates hook session IDs' do
+ expect(BeEF::Filters.is_valid_hook_session_id?('abc123')).to be(true)
+ expect(BeEF::Filters.is_valid_hook_session_id?('')).to be(false)
+ expect(BeEF::Filters.is_valid_hook_session_id?(nil)).to be(false)
+ end
+ end
+
+ describe '.is_valid_command_module_datastore_key?' do
+ it 'validates datastore keys' do
+ expect(BeEF::Filters.is_valid_command_module_datastore_key?('test_key')).to be(true)
+ expect(BeEF::Filters.is_valid_command_module_datastore_key?('')).to be(false)
+ end
+ end
+
+ describe '.is_valid_command_module_datastore_param?' do
+ it 'validates datastore params' do
+ expect(BeEF::Filters.is_valid_command_module_datastore_param?('test_value')).to be(true)
+ expect(BeEF::Filters.is_valid_command_module_datastore_param?("\x00")).to be(false)
+ end
+ end
+
+ describe '.has_valid_key_chars?' do
+ it 'validates key characters' do
+ expect(BeEF::Filters.has_valid_key_chars?('test_key')).to be(true)
+ expect(BeEF::Filters.has_valid_key_chars?('')).to be(false)
+ end
+ end
+
+ describe '.has_valid_param_chars?' do
+ it 'false' do
+ chars = [nil, '', '+']
+ chars.each do |c|
+ expect(BeEF::Filters.has_valid_param_chars?(c)).to be(false)
+ end
+ end
+
+ it 'true' do
+ expect(BeEF::Filters.has_valid_param_chars?('A')).to be(true)
+ end
+ end
+end
diff --git a/spec/beef/core/filter/filters_spec.rb b/spec/beef/core/filter/filters_spec.rb
deleted file mode 100644
index 353c350de..000000000
--- a/spec/beef/core/filter/filters_spec.rb
+++ /dev/null
@@ -1,354 +0,0 @@
-RSpec.describe 'BeEF Filters' do
-
- context 'is_non_empty_string?' do
-
- it 'nil' do
- expect(BeEF::Filters::is_non_empty_string?(nil)).to be(false)
- end
-
- it 'Integer' do
- expect(BeEF::Filters::is_non_empty_string?(1)).to be(false)
- end
-
- it 'Empty String' do
- expect(BeEF::Filters::is_non_empty_string?("")).to be(false)
- end
-
- it 'null' do
- expect(BeEF::Filters::is_non_empty_string?("\x00")).to be(true)
- end
-
- it 'First char is num' do
- expect(BeEF::Filters::is_non_empty_string?("0")).to be(true)
- end
-
- it 'First char is alpha' do
- expect(BeEF::Filters::is_non_empty_string?("A")).to be(true)
- end
-
- it 'Four num chars' do
- expect(BeEF::Filters::is_non_empty_string?("3333")).to be(true)
- end
-
- it 'Four num chars begining with alpha' do
- expect(BeEF::Filters::is_non_empty_string?("A3333")).to be(true)
- end
-
- it 'Four num chars begining with null' do
- expect(BeEF::Filters::is_non_empty_string?("\x003333")).to be(true)
- end
-
- end
-
- context 'only?' do
-
- it 'success' do
- expect(BeEF::Filters::only?('A', 'A')).to be(true)
- end
-
- it 'fail' do
- expect(BeEF::Filters::only?('A', 'B')).to be(false)
- end
-
- end
-
- context 'exists?' do
-
- it 'success' do
- expect(BeEF::Filters::exists?('A', 'A')).to be(true)
- end
-
- it 'fail' do
- expect(BeEF::Filters::exists?('A', 'B')).to be(false)
- end
-
- end
-
- context 'has_null?' do
-
- context 'false with' do
-
- it 'general' do
- chars = [nil, "", "\x01", "\xFF", "A", "A3333", "0", "}", ".", "+", "-", "-1", "0.A", "3333", "33 33", " AAAAA", "AAAAAA "]
- chars.each do |c|
- expect(BeEF::Filters::has_null?(c)).to be(false)
- end
- end
-
- it 'alphabet' do
- (1..255).each do |c|
- str = ''
- str.concat(c)
- expect(BeEF::Filters::has_null?(str)).to be(false)
- end
- end
-
- end
-
- context 'true with' do
-
- it 'general' do
- chars = ["\x00", "A\x00", "AAAAAA\x00", "\x00A", "\x00AAAAAAAA", "A\x00A", "AAAAA\x00AAAA", "A\n\r\x00", "\x00\n\rA", "A\n\r\x00\n\rA", "\tA\x00A"]
- chars.each do |c|
- expect(BeEF::Filters::has_null?(c)).to be(true)
- end
- end
-
- it 'alphabet null after' do
- (1..255).each do |c|
- str = ''
- str.concat(c)
- str += "\x00"
- expect(BeEF::Filters::has_null?(str)).to be(true)
- end
- end
-
- it 'alphabet null before' do
- (1..255).each do |c|
- str = "\x00"
- str.concat(c)
- expect(BeEF::Filters::has_null?(str)).to be(true)
- end
- end
-
- end
-
- end
-
- context 'has_non_printable_char?' do
-
- context 'false with' do
-
- it 'general' do
- chars = [nil, "", "A", "A3333", "0", "}", ".", "+", "-", "-1", "0.A", "3333", " 0AAAAA", " 0AAA "]
- chars.each do |c|
- expect(BeEF::Filters::has_non_printable_char?(c)).to be(false)
- end
- end
-
- it 'lowercase' do
- ('a'..'z').each do |c|
- expect(BeEF::Filters::has_non_printable_char?(c)).to be(false)
- end
- end
-
- it 'uppercase' do
- ('A'..'Z').each do |c|
- expect(BeEF::Filters::has_non_printable_char?(c)).to be(false)
- end
- end
-
- it 'numbers' do
- ('0'..'9').each do |c|
- expect(BeEF::Filters::has_non_printable_char?(c)).to be(false)
- end
- end
-
- end
-
- context 'true with' do
-
- it 'general' do
- chars = ["\x00", "\x01", "\x02", "A\x03", "\x04A", "\x0033333", "\x00AAAAAA", " AAAAA\x00", "\t\x00AAAAA", "\n\x00AAAAA", "\n\r\x00AAAAAAAAA", "AAAAAAA\x00AAAAAAA", "\n\x00"]
- chars.each do |c|
- expect(BeEF::Filters::has_non_printable_char?(c)).to be(true)
- end
- end
-
- it 'alphabet null before' do
- (1..255).each do |c|
- str = ''
- str.concat(c)
- str += "\x00"
- expect(BeEF::Filters::has_non_printable_char?(str)).to be(true)
- end
- end
-
- end
-
- end
-
- context 'nums_only?' do
-
- it 'false with general' do
- chars = [nil, 1, "", "A", "A3333", "\x003333", "}", ".", "+", "-", "-1"]
- chars.each do |c|
- expect(BeEF::Filters::nums_only?(c)).to be(false)
- end
- end
-
- it 'true with general' do
- chars = ["0", "333"]
- chars.each do |c|
- expect(BeEF::Filters::nums_only?(c)).to be(true)
- end
- end
-
- end
-
- context 'is_valid_float?' do
-
- it 'false with general' do
- chars = [nil, 1, "", "A", "A3333", "\x003333", "}", ".", "+", "-", "-1", "0", "333", "0.A"]
- chars.each do |c|
- expect(BeEF::Filters::is_valid_float?(c)).to be(false)
- end
- end
-
- it 'true with general' do
- chars = ["33.33", "0.0", "1.0", "0.1"]
- chars.each do |c|
- expect(BeEF::Filters::is_valid_float?(c)).to be(true)
- end
- end
-
- end
-
- context 'hexs_only?' do
-
- it 'false with general' do
- chars = [nil, 1, "", "\x003333", "}", ".", "+", "-", "-1", "0.A", "33.33", "0.0", "1.0", "0.1"]
- chars.each do |c|
- expect(BeEF::Filters::hexs_only?(c)).to be(false)
- end
- end
-
- it 'true with general' do
- chars = ["0123456789ABCDEFabcdef", "0", "333", "A33333", "A"]
- chars.each do |c|
- expect(BeEF::Filters::hexs_only?(c)).to be(true)
- end
- end
-
- end
-
- context 'first_char_is_num?' do
-
- it 'false with general' do
- chars = ["", "A", "A33333", "\x0033333"]
- chars.each do |c|
- expect(BeEF::Filters::first_char_is_num?(c)).to be(false)
- end
- end
-
- it 'true with general' do
- chars = ["333", "0AAAAAA", "0"]
- chars.each do |c|
- expect(BeEF::Filters::first_char_is_num?(c)).to be(true)
- end
- end
-
- end
-
- context 'has_whitespace_char?' do
-
- it 'false with general' do
- chars = ["", "A", "A33333", "\x0033333", "0", "}", ".", "+", "-", "-1", "0.A"]
- chars.each do |c|
- expect(BeEF::Filters::has_whitespace_char?(c)).to be(false)
- end
- end
-
- it 'true with general' do
- chars = ["33 33", " ", " ", " 0AAAAAAA", " 0AAAAAAA ", "\t0AAAAAAA", "\n0AAAAAAAA"]
- chars.each do |c|
- expect(BeEF::Filters::has_whitespace_char?(c)).to be(true)
- end
- end
-
- end
-
- context 'alphanums_only?' do
-
- context 'false with' do
-
- it 'general' do
- chars = [nil, "", "\n", "\r", "\x01", "}", ".", "+", "-", "-1", "ee-!@$%^&*}=0.A", "33 33", " AAAA", "AAA "]
- chars.each do |c|
- expect(BeEF::Filters::alphanums_only?(c)).to be(false)
- end
- end
-
- it 'additional nulls' do
- chars = ["\x00", "A\x00", "AAAAAAAAA\x00", "\x00A", "\x00AAAAAAAAA", "A\x00A", "AAAAAAAA\x00AAAAAAAA", "A\n\r\x00", "\x00\n\rA", "A\n\r\x00\n\rA", "\tA\x00A"]
- chars.each do |c|
- expect(BeEF::Filters::alphanums_only?(c)).to be(false)
- end
- end
-
- it 'alphabet null after' do
- (1..255).each do |c|
- str = ''
- str.concat(c)
- str += "\x00"
- expect(BeEF::Filters::alphanums_only?(str)).to be(false)
- end
- end
-
- it 'alphabet null before' do
- (1..255).each do |c|
- str = "\x00"
- str.concat(c)
- expect(BeEF::Filters::alphanums_only?(str)).to be(false)
- end
- end
-
- it 'alphabet around null' do
- (1..255).each do |c|
- str = ''
- str.concat(c)
- str += "\x00"
- str.concat(c)
- expect(BeEF::Filters::alphanums_only?(str)).to be(false)
- end
- end
-
- end
-
- context 'true with' do
-
- it 'general' do
- chars = ["A", "A3333", "0", "3333"]
- chars.each do |c|
- expect(BeEF::Filters::alphanums_only?(c)).to be(true)
- end
- end
-
- it 'uppercase' do
- ('A'..'Z').each do |c|
- expect(BeEF::Filters::alphanums_only?(c)).to be(true)
- end
- end
-
- it 'lowercase' do
- ('a'..'z').each do |c|
- expect(BeEF::Filters::alphanums_only?(c)).to be(true)
- end
- end
-
- it 'numbers' do
- ('0'..'9').each do |c|
- expect(BeEF::Filters::alphanums_only?(c)).to be(true)
- end
- end
-
- end
-
- end
-
- context 'has_valid_param_chars?' do
-
- it 'false' do
- chars = [nil, "", "+"]
- chars.each do |c|
- expect(BeEF::Filters::has_valid_param_chars?(c)).to be(false)
- end
- end
-
- it 'true' do
- expect(BeEF::Filters::has_valid_param_chars?("A")).to be(true)
- end
-
- end
-
-end
diff --git a/spec/beef/core/filter/http_spec.rb b/spec/beef/core/filter/http_spec.rb
new file mode 100644
index 000000000..fc3e06faf
--- /dev/null
+++ b/spec/beef/core/filter/http_spec.rb
@@ -0,0 +1,50 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Filters do
+ describe '.is_valid_hostname?' do
+ it 'validates hostnames correctly' do
+ expect(BeEF::Filters.is_valid_hostname?('example.com')).to be(true)
+ expect(BeEF::Filters.is_valid_hostname?('sub.example.com')).to be(true)
+ expect(BeEF::Filters.is_valid_hostname?('a' * 256)).to be(false) # too long
+ expect(BeEF::Filters.is_valid_hostname?('')).to be(false)
+ expect(BeEF::Filters.is_valid_hostname?(nil)).to be(false)
+ end
+ end
+
+ describe '.is_valid_verb?' do
+ it 'validates HTTP verbs' do
+ %w[HEAD GET POST OPTIONS PUT DELETE].each do |verb|
+ expect(BeEF::Filters.is_valid_verb?(verb)).to be(true)
+ end
+ expect(BeEF::Filters.is_valid_verb?('INVALID')).to be(false)
+ end
+ end
+
+ describe '.is_valid_url?' do
+ it 'validates URLs' do
+ expect(BeEF::Filters.is_valid_url?(nil)).to be(false)
+ expect(BeEF::Filters.is_valid_url?('http://example.com')).to be(true)
+ end
+ end
+
+ describe '.is_valid_http_version?' do
+ it 'validates HTTP versions' do
+ expect(BeEF::Filters.is_valid_http_version?('HTTP/1.0')).to be(true)
+ expect(BeEF::Filters.is_valid_http_version?('HTTP/1.1')).to be(true)
+ expect(BeEF::Filters.is_valid_http_version?('HTTP/2.0')).to be(false)
+ end
+ end
+
+ describe '.is_valid_host_str?' do
+ it 'validates host header strings' do
+ expect(BeEF::Filters.is_valid_host_str?('Host:')).to be(true)
+ host_str = "Host:\r".dup
+ expect(BeEF::Filters.is_valid_host_str?(host_str)).to be(true)
+ expect(BeEF::Filters.is_valid_host_str?('Invalid')).to be(false)
+ end
+ end
+end
diff --git a/spec/beef/core/filter/page_spec.rb b/spec/beef/core/filter/page_spec.rb
new file mode 100644
index 000000000..bcc069848
--- /dev/null
+++ b/spec/beef/core/filter/page_spec.rb
@@ -0,0 +1,24 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Filters do
+ describe '.is_valid_pagetitle?' do
+ it 'validates page titles' do
+ expect(BeEF::Filters.is_valid_pagetitle?('Test Page')).to be(true)
+ expect(BeEF::Filters.is_valid_pagetitle?('A' * 500)).to be(true)
+ expect(BeEF::Filters.is_valid_pagetitle?('A' * 501)).to be(false)
+ expect(BeEF::Filters.is_valid_pagetitle?("\x00")).to be(false)
+ end
+ end
+
+ describe '.is_valid_pagereferrer?' do
+ it 'validates page referrers' do
+ expect(BeEF::Filters.is_valid_pagereferrer?('http://example.com')).to be(true)
+ expect(BeEF::Filters.is_valid_pagereferrer?('A' * 350)).to be(true)
+ expect(BeEF::Filters.is_valid_pagereferrer?('A' * 351)).to be(false)
+ end
+ end
+end
diff --git a/spec/beef/core/logger_spec.rb b/spec/beef/core/logger_spec.rb
new file mode 100644
index 000000000..1c7511108
--- /dev/null
+++ b/spec/beef/core/logger_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
+#
+require 'spec_helper'
+require 'fileutils'
+
+RSpec.describe 'BeEF Logger' do
+ let(:home_dir) { $home_dir } # rubocop:disable Style/GlobalVars
+ let(:expected_log_path) { File.join(home_dir, 'beef.log') }
+
+ before(:each) do
+ # Reset the logger to ensure clean state
+ BeEF.logger = nil
+ end
+
+ after(:each) do
+ # Clean up any log files created during tests
+ FileUtils.rm_f(expected_log_path)
+ BeEF.logger = nil
+ end
+
+ describe '.logger' do
+ it 'returns a Logger instance' do
+ expect(BeEF.logger).to be_a(Logger)
+ end
+
+ it 'creates logger with correct file path' do
+ logger = BeEF.logger
+ expect(logger.instance_variable_get(:@logdev).dev.path).to eq(expected_log_path)
+ end
+
+ it 'sets the progname to BeEF' do
+ logger = BeEF.logger
+ expect(logger.progname).to eq('BeEF')
+ end
+
+ it 'sets the log level to WARN' do
+ logger = BeEF.logger
+ expect(logger.level).to eq(Logger::WARN)
+ end
+
+ it 'returns the same logger instance on subsequent calls' do
+ logger1 = BeEF.logger
+ logger2 = BeEF.logger
+ expect(logger1).to be(logger2)
+ end
+
+ it 'creates the log file when logger is accessed' do
+ # Ensure file doesn't exist initially
+ FileUtils.rm_f(expected_log_path)
+
+ BeEF.logger
+
+ expect(File.exist?(expected_log_path)).to be(true)
+ end
+ end
+
+ describe '.logger=' do
+ it 'allows setting a custom logger' do
+ custom_logger = Logger.new($stdout)
+ BeEF.logger = custom_logger
+
+ expect(BeEF.logger).to be(custom_logger)
+ end
+
+ it 'uses the custom logger instead of creating a new one' do
+ custom_logger = Logger.new($stdout)
+ custom_logger.level = Logger::DEBUG
+ BeEF.logger = custom_logger
+
+ expect(BeEF.logger.level).to eq(Logger::DEBUG)
+ expect(BeEF.logger).to be(custom_logger)
+ end
+
+ it 'allows resetting logger to nil' do
+ BeEF.logger = nil
+ expect(BeEF.instance_variable_get(:@logger)).to be_nil
+ end
+
+ it 'creates a new logger after being set to nil' do
+ original_logger = BeEF.logger
+ BeEF.logger = nil
+ new_logger = BeEF.logger
+
+ expect(new_logger).to be_a(Logger)
+ expect(new_logger).not_to be(original_logger)
+ end
+ end
+
+ describe 'logger functionality' do
+ it 'can log messages at WARN level' do
+ logger = BeEF.logger
+ expect { logger.warn('Test warning message') }.not_to raise_error
+ end
+
+ it 'can log messages at ERROR level' do
+ logger = BeEF.logger
+ expect { logger.error('Test error message') }.not_to raise_error
+ end
+
+ it 'does not log messages below WARN level by default' do
+ logger = BeEF.logger
+ # INFO and DEBUG messages should not be logged at WARN level
+ expect(logger.info?).to be(false)
+ expect(logger.debug?).to be(false)
+ end
+
+ it 'logs messages at WARN level and above' do
+ logger = BeEF.logger
+ expect(logger.warn?).to be(true)
+ expect(logger.error?).to be(true)
+ expect(logger.fatal?).to be(true)
+ end
+ end
+end
diff --git a/spec/beef/core/main/command_spec.rb b/spec/beef/core/main/command_spec.rb
index 476bf6f2b..5fd86b136 100644
--- a/spec/beef/core/main/command_spec.rb
+++ b/spec/beef/core/main/command_spec.rb
@@ -1,13 +1,234 @@
-RSpec.describe 'BeEF Command class testing' do
- it 'should return a beef configuration variable' do
- expect {
- BeEF::Modules.load if BeEF::Core::Configuration.instance.get('beef.module').nil?
- }.to_not raise_error
- command_mock = BeEF::Core::Command.new('test_get_variable')
- expect(command_mock.config.beef_host).to eq('0.0.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 'modules/browser/hooked_origin/get_page_links/module'
- gpl = Get_page_links.new('test_get_variable')
- expect(gpl.config.beef_host).to eq('0.0.0.0')
+RSpec.describe BeEF::Core::Command do
+ let(:config) { BeEF::Core::Configuration.instance }
+
+ before do
+ # Ensure modules are loaded
+ BeEF::Modules.load if config.get('beef.module').nil?
+
+ # Set up a test module configuration if it doesn't exist
+ unless config.get('beef.module.test_get_variable')
+ config.set('beef.module.test_get_variable.name', 'Test Get Variable')
+ config.set('beef.module.test_get_variable.path', 'modules/test/')
+ config.set('beef.module.test_get_variable.mount', '/command/test_get_variable.js')
+ config.set('beef.module.test_get_variable.db.id', 1)
+ end
+ end
+
+ describe BeEF::Core::CommandUtils do
+ describe '#format_multiline' do
+ it 'converts newlines to escaped newlines' do
+ result = BeEF::Core::CommandUtils.instance_method(:format_multiline).bind(Object.new).call("line1\nline2")
+ expect(result).to eq("line1\\nline2")
+ end
+
+ it 'handles strings without newlines' do
+ result = BeEF::Core::CommandUtils.instance_method(:format_multiline).bind(Object.new).call("single line")
+ expect(result).to eq("single line")
+ end
+ end
+ end
+
+ describe BeEF::Core::CommandContext do
+ it 'initializes with hash' do
+ context = described_class.new({ 'key' => 'value' })
+ expect(context['key']).to eq('value')
+ end
+
+ it 'initializes without hash' do
+ context = described_class.new
+ expect(context).to be_a(Erubis::Context)
+ end
+
+ it 'includes CommandUtils' do
+ context = described_class.new
+ expect(context).to respond_to(:format_multiline)
+ end
+ end
+
+ describe '#initialize' do
+ it 'initializes with module key' do
+ command = described_class.new('test_get_variable')
+ expect(command.config).to eq(config)
+ expect(command.datastore).to eq({})
+ expect(command.beefjs_components).to eq({})
+ end
+
+ it 'sets friendlyname from configuration' do
+ # Mock all config calls for initialization
+ allow(config).to receive(:get).and_call_original
+ allow(config).to receive(:get).with('beef.module.test_get_variable.name').and_return('Test Get Variable')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.path').and_return('modules/test/')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.mount').and_return('/command/test.js')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.db.id').and_return(1)
+ command = described_class.new('test_get_variable')
+ expect(command.friendlyname).to eq('Test Get Variable')
+ end
+ end
+
+ describe '#needs_configuration?' do
+ it 'returns true when datastore is not nil' do
+ command = described_class.new('test_get_variable')
+ command.instance_variable_set(:@datastore, {})
+ expect(command.needs_configuration?).to be true
+ end
+
+ it 'returns false when datastore is nil' do
+ command = described_class.new('test_get_variable')
+ command.instance_variable_set(:@datastore, nil)
+ expect(command.needs_configuration?).to be false
+ end
+ end
+
+ describe '#to_json' do
+ it 'returns JSON with command information' do
+ # Mock all config calls for this test
+ allow(config).to receive(:get).and_call_original
+ allow(config).to receive(:get).with('beef.module.test_get_variable.name').and_return('Test Get Variable')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.description').and_return('Test Description')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.category').and_return('Test Category')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.path').and_return('modules/test/')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.mount').and_return('/command/test.js')
+ allow(config).to receive(:get).with('beef.module.test_get_variable.db.id').and_return(1)
+ allow(BeEF::Module).to receive(:get_options).with('test_get_variable').and_return([])
+ command = described_class.new('test_get_variable')
+ json = command.to_json
+ parsed = JSON.parse(json)
+ expect(parsed['Name']).to eq('Test Get Variable')
+ expect(parsed['Description']).to eq('Test Description')
+ expect(parsed['Category']).to eq('Test Category')
+ end
+ end
+
+ describe '#build_datastore' do
+ it 'parses JSON data into datastore' do
+ command = described_class.new('test_get_variable')
+ data = '{"key": "value"}'
+ command.build_datastore(data)
+ expect(command.datastore).to eq({ 'key' => 'value' })
+ end
+
+ it 'handles invalid JSON gracefully' do
+ command = described_class.new('test_get_variable')
+ command.build_datastore('invalid json')
+ expect(command.datastore).to eq({})
+ end
+ end
+
+ describe '#build_callback_datastore' do
+ it 'initializes datastore with http_headers' do
+ command = described_class.new('test_get_variable')
+ command.build_callback_datastore('result', 1, 'hook', nil, nil)
+ expect(command.datastore).to have_key('http_headers')
+ expect(command.datastore['http_headers']).to eq({})
+ end
+
+ it 'adds results, command_id, and beefhook' do
+ command = described_class.new('test_get_variable')
+ command.build_callback_datastore('result', 1, 'hook', nil, nil)
+ expect(command.datastore['results']).to eq('result')
+ expect(command.datastore['cid']).to eq(1)
+ expect(command.datastore['beefhook']).to eq('hook')
+ end
+
+ it 'adds valid http_params to datastore' do
+ allow(BeEF::Filters).to receive(:is_valid_command_module_datastore_key?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_command_module_datastore_param?).and_return(true)
+ allow(Erubis::XmlHelper).to receive(:escape_xml) { |arg| arg }
+ command = described_class.new('test_get_variable')
+ command.build_callback_datastore('result', 1, 'hook', { 'param1' => 'value1' }, {})
+ expect(command.datastore['param1']).to eq('value1')
+ end
+
+ it 'skips invalid http_params' do
+ allow(BeEF::Filters).to receive(:is_valid_command_module_datastore_key?).and_return(false)
+ command = described_class.new('test_get_variable')
+ command.build_callback_datastore('result', 1, 'hook', { 'invalid' => 'value' }, {})
+ expect(command.datastore).not_to have_key('invalid')
+ end
+ end
+
+ describe '#save' do
+ it 'saves results' do
+ command = described_class.new('test_get_variable')
+ results = { 'data' => 'test' }
+ command.save(results)
+ expect(command.instance_variable_get(:@results)).to eq(results)
+ end
+ end
+
+ describe '#map_file_to_url' do
+ it 'calls AssetHandler bind' do
+ mock_handler = double('AssetHandler')
+ allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(mock_handler)
+ expect(mock_handler).to receive(:bind).with('file.txt', nil, nil, 1)
+ command = described_class.new('test_get_variable')
+ command.map_file_to_url('file.txt')
+ end
+ end
+
+ describe '#use' do
+ it 'adds component to beefjs_components when file exists' do
+ # The path construction adds an extra /, so account for that
+ component_path = "#{$root_dir}/core/main/client//net/local.js"
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(component_path).and_return(true)
+ command = described_class.new('test_get_variable')
+ command.use('beef.net.local')
+ expect(command.beefjs_components).to have_key('beef.net.local')
+ end
+
+ it 'raises error when component file does not exist' do
+ component_path = "#{$root_dir}/core/main/client//net/nonexistent.js"
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(component_path).and_return(false)
+ command = described_class.new('test_get_variable')
+ expect { command.use('beef.net.nonexistent') }.to raise_error(/Invalid beefjs component/)
+ end
+
+ it 'does not add component twice' do
+ component_path = "#{$root_dir}/core/main/client//net/local.js"
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(component_path).and_return(true)
+ command = described_class.new('test_get_variable')
+ command.use('beef.net.local')
+ command.use('beef.net.local')
+ expect(command.beefjs_components.keys.count).to eq(1)
+ end
+ end
+
+ describe '#oc_value' do
+ it 'returns option value when option exists' do
+ BeEF::Core::Models::OptionCache.create!(name: 'test_option', value: 'test_value')
+ command = described_class.new('test_get_variable')
+ expect(command.oc_value('test_option')).to eq('test_value')
+ end
+
+ it 'returns nil when option does not exist' do
+ command = described_class.new('test_get_variable')
+ expect(command.oc_value('nonexistent')).to be_nil
+ end
+ end
+
+ describe '#apply_defaults' do
+ it 'applies option cache values to datastore' do
+ BeEF::Core::Models::OptionCache.create!(name: 'option1', value: 'cached_value')
+ command = described_class.new('test_get_variable')
+ command.instance_variable_set(:@datastore, [{ 'name' => 'option1', 'value' => 'default_value' }])
+ command.apply_defaults
+ expect(command.datastore[0]['value']).to eq('cached_value')
+ end
+
+ it 'keeps default value when option cache does not exist' do
+ command = described_class.new('test_get_variable')
+ command.instance_variable_set(:@datastore, [{ 'name' => 'option1', 'value' => 'default_value' }])
+ command.apply_defaults
+ expect(command.datastore[0]['value']).to eq('default_value')
+ end
end
end
diff --git a/spec/beef/core/main/configuration_spec.rb b/spec/beef/core/main/configuration_spec.rb
index caafdc88b..8289c1469 100644
--- a/spec/beef/core/main/configuration_spec.rb
+++ b/spec/beef/core/main/configuration_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.configure do |config|
end
diff --git a/spec/beef/core/main/crypto_spec.rb b/spec/beef/core/main/crypto_spec.rb
new file mode 100644
index 000000000..bfe4ccfbf
--- /dev/null
+++ b/spec/beef/core/main/crypto_spec.rb
@@ -0,0 +1,67 @@
+#
+# 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::Crypto' do
+ let(:config) { BeEF::Core::Configuration.instance }
+
+ describe '.secure_token' do
+ it 'generates a hex string of the specified length' do
+ token = BeEF::Core::Crypto.secure_token(20)
+ expect(token).to be_a(String)
+ expect(token.length).to eq(40) # 20 bytes = 40 hex chars
+ expect(token).to match(/\A[0-9a-f]+\z/)
+ end
+
+ it 'uses config default length when no length provided' do
+ default_length = config.get('beef.crypto_default_value_length').to_i
+ token = BeEF::Core::Crypto.secure_token
+ expect(token.length).to eq(default_length * 2) # bytes to hex conversion
+ end
+
+ it 'raises TypeError for length below minimum' do
+ expect { BeEF::Core::Crypto.secure_token(10) }.to raise_error(TypeError, /minimum length/)
+ end
+
+ it 'generates different tokens on each call' do
+ token1 = BeEF::Core::Crypto.secure_token(20)
+ token2 = BeEF::Core::Crypto.secure_token(20)
+ expect(token1).not_to eq(token2)
+ end
+ end
+
+ describe '.random_alphanum_string' do
+ it 'generates a string of the specified length' do
+ result = BeEF::Core::Crypto.random_alphanum_string(15)
+ expect(result).to be_a(String)
+ expect(result.length).to eq(15)
+ expect(result).to match(/\A[a-zA-Z0-9]+\z/)
+ end
+
+ it 'raises TypeError for invalid inputs' do
+ expect { BeEF::Core::Crypto.random_alphanum_string('invalid') }.to raise_error(TypeError)
+ expect { BeEF::Core::Crypto.random_alphanum_string(0) }.to raise_error(TypeError, /Invalid length/)
+ expect { BeEF::Core::Crypto.random_alphanum_string(-1) }.to raise_error(TypeError, /Invalid length/)
+ end
+ end
+
+ describe '.random_hex_string' do
+ it 'generates a hex string of the specified length' do
+ result = BeEF::Core::Crypto.random_hex_string(10)
+ expect(result).to be_a(String)
+ expect(result.length).to eq(10)
+ expect(result).to match(/\A[0-9a-f]+\z/)
+ end
+
+ 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/)
+ end
+ end
+
+ # NOTE: .dns_rule_id is not tested here as it requires database queries
+ # and is better suited for integration tests
+end
diff --git a/spec/beef/core/main/geoip_spec.rb b/spec/beef/core/main/geoip_spec.rb
new file mode 100644
index 000000000..b8cce35db
--- /dev/null
+++ b/spec/beef/core/main/geoip_spec.rb
@@ -0,0 +1,110 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::GeoIp do
+ let(:config) { BeEF::Core::Configuration.instance }
+ let(:geoip) { described_class.instance }
+
+ # Mock MaxMind module if not available
+ before do
+ unless defined?(MaxMind)
+ stub_const('MaxMind', Module.new)
+ stub_const('MaxMind::DB', Class.new)
+ end
+ # MODE_MEMORY is actually :MODE_MEMORY (not :memory) - use actual value if available
+ mode_memory = defined?(MaxMind::DB::MODE_MEMORY) ? MaxMind::DB::MODE_MEMORY : :MODE_MEMORY
+ stub_const('MaxMind::DB::MODE_MEMORY', mode_memory) unless defined?(MaxMind::DB::MODE_MEMORY)
+ end
+
+ before do
+ # Reset singleton instance for each test
+ described_class.instance_variable_set(:@singleton__instance__, nil)
+ # Allow config to receive other calls
+ allow(config).to receive(:get).and_call_original
+ end
+
+ describe '#initialize' do
+ it 'disables GeoIP when configuration is false' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(false)
+ expect(geoip.enabled?).to be false
+ end
+
+ it 'disables GeoIP when database file does not exist' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.geoip.database').and_return('/nonexistent/db.mmdb')
+ allow(File).to receive(:exist?).with('/nonexistent/db.mmdb').and_return(false)
+ expect(geoip.enabled?).to be false
+ end
+
+ it 'enables GeoIP when database file exists' do
+ # Set up stub BEFORE singleton is created
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.geoip.database').and_return('/path/to/db.mmdb')
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with('/path/to/db.mmdb').and_return(true)
+ mock_reader = double('MaxMind::DB')
+ allow(mock_reader).to receive(:freeze)
+ allow(MaxMind::DB).to receive(:new).with('/path/to/db.mmdb', { mode: MaxMind::DB::MODE_MEMORY }).and_return(mock_reader)
+ # Reset singleton so it reinitializes with our stubs
+ described_class.instance_variable_set(:@singleton__instance__, nil)
+ expect(geoip.enabled?).to be true
+ end
+
+ it 'disables GeoIP on initialization error' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.geoip.database').and_return('/path/to/db.mmdb')
+ allow(File).to receive(:exist?).with('/path/to/db.mmdb').and_return(true)
+ allow(MaxMind::DB).to receive(:new).and_raise(StandardError.new('Database error'))
+ expect(geoip.enabled?).to be false
+ end
+ end
+
+ describe '#enabled?' do
+ it 'returns false when GeoIP is disabled' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(false)
+ expect(geoip.enabled?).to be false
+ end
+
+ it 'returns true when GeoIP is enabled' do
+ # Set up stub BEFORE singleton is created - singleton initializes when let(:geoip) is called
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.geoip.database').and_return('/path/to/db.mmdb')
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with('/path/to/db.mmdb').and_return(true)
+ mock_reader = double('MaxMind::DB')
+ allow(mock_reader).to receive(:freeze)
+ # The actual call is: MaxMind::DB.new('/path/to/db.mmdb', mode: :MODE_MEMORY)
+ allow(MaxMind::DB).to receive(:new).with('/path/to/db.mmdb', { mode: :MODE_MEMORY }).and_return(mock_reader)
+ # Reset singleton so it reinitializes with our stubs
+ described_class.instance_variable_set(:@singleton__instance__, nil)
+ expect(geoip.enabled?).to be true
+ end
+ end
+
+ describe '#lookup' do
+ it 'raises TypeError for non-string IP' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(false)
+ expect { geoip.lookup(123) }.to raise_error(TypeError, /"ip" needs to be a string/)
+ end
+
+ it 'returns nil when GeoIP is disabled' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(false)
+ expect(geoip.lookup('192.168.1.1')).to be_nil
+ end
+
+ it 'returns lookup result when GeoIP is enabled' do
+ allow(config).to receive(:get).with('beef.geoip.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.geoip.database').and_return('/path/to/db.mmdb')
+ allow(File).to receive(:exist?).with('/path/to/db.mmdb').and_return(true)
+ mock_reader = double('MaxMind::DB')
+ allow(mock_reader).to receive(:freeze)
+ allow(mock_reader).to receive(:get).with('192.168.1.1').and_return({ 'city' => 'Test City' })
+ allow(MaxMind::DB).to receive(:new).with('/path/to/db.mmdb', anything).and_return(mock_reader)
+ result = geoip.lookup('192.168.1.1')
+ expect(result).to eq({ 'city' => 'Test City' })
+ end
+ end
+end
diff --git a/spec/beef/core/main/handlers/browserdetails_spec.rb b/spec/beef/core/main/handlers/browserdetails_spec.rb
new file mode 100644
index 000000000..637dc0e83
--- /dev/null
+++ b/spec/beef/core/main/handlers/browserdetails_spec.rb
@@ -0,0 +1,416 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Handlers::BrowserDetails do
+ let(:config) { BeEF::Core::Configuration.instance }
+ let(:session_id) { 'test_session_123' }
+ let(:mock_request) do
+ double('Request',
+ ip: '127.0.0.1',
+ referer: 'http://example.com',
+ env: { 'HTTP_USER_AGENT' => 'Mozilla/5.0' })
+ end
+ let(:data) do
+ {
+ 'beefhook' => session_id,
+ 'request' => mock_request,
+ 'results' => {
+ 'browser.name' => 'FF',
+ 'browser.version' => '91.0',
+ 'browser.window.hostname' => 'example.com',
+ 'browser.window.hostport' => '80'
+ }
+ }
+ end
+
+ before do
+ allow(config).to receive(:get).and_call_original
+ allow(config).to receive(:get).with('beef.dns_hostname_lookup').and_return(false)
+ allow(config).to receive(:get).with('beef.extension.network.enable').and_return(false)
+ allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false)
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).and_return(true)
+ allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([])
+ allow(BeEF::Core::Logger.instance).to receive(:register)
+ allow(BeEF::Core::GeoIp.instance).to receive(:enabled?).and_return(false)
+ # Stub NetworkHost if it exists, otherwise stub the constant
+ if defined?(BeEF::Core::Models::NetworkHost)
+ allow(BeEF::Core::Models::NetworkHost).to receive(:create)
+ else
+ stub_const('BeEF::Core::Models::NetworkHost', double('NetworkHost', create: nil))
+ end
+ end
+
+ describe '#initialize' do
+ it 'initializes with data and calls setup' do
+ expect_any_instance_of(described_class).to receive(:setup)
+ described_class.new(data)
+ end
+ end
+
+ describe '#err_msg' do
+ let(:handler) do
+ instance = described_class.allocate
+ instance.instance_variable_set(:@data, data)
+ instance
+ end
+
+ it 'calls print_error with prefixed message' do
+ expect(handler).to receive(:print_error).with('[Browser Details] test error')
+ handler.err_msg('test error')
+ end
+ end
+
+ describe '#get_param' do
+ let(:handler) do
+ # Create handler but prevent full setup execution
+ instance = described_class.allocate
+ instance.instance_variable_set(:@data, data)
+ instance
+ end
+
+ it 'returns value when key exists in hash' do
+ result = handler.get_param(data['results'], 'browser.name')
+ expect(result).to eq('FF')
+ end
+
+ it 'returns nil when key does not exist' do
+ result = handler.get_param(data['results'], 'nonexistent')
+ expect(result).to be_nil
+ end
+
+ it 'returns nil when query is not a hash' do
+ result = handler.get_param('not a hash', 'key')
+ expect(result).to be_nil
+ end
+
+ it 'converts value to string' do
+ result = handler.get_param({ 'key' => 123 }, 'key')
+ expect(result).to eq('123')
+ end
+ end
+
+ describe '#setup' do
+ it 'validates session id' do
+ invalid_data = data.dup
+ invalid_data['beefhook'] = 'invalid'
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).with('invalid').and_return(false)
+ expect { described_class.new(invalid_data) }.not_to raise_error
+ end
+
+ it 'skips setup if browser already registered' do
+ existing_browser = double('HookedBrowser', session: session_id)
+ allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([existing_browser])
+ expect(BeEF::Core::Models::HookedBrowser).not_to receive(:new)
+ described_class.new(data)
+ end
+
+ it 'creates new hooked browser when not registered' 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(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ 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!)
+ # Mock JSON.parse for proxy detection
+ allow(JSON).to receive(:parse).with('{}').and_return({})
+ allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
+ described_class.new(data)
+ expect(BeEF::Core::Models::HookedBrowser).to have_received(:new).with(ip: '127.0.0.1', session: session_id)
+ end
+
+ it 'extracts domain from referer when hostname is missing' do
+ referer_data = data.dup
+ referer_data['results'].delete('browser.window.hostname')
+ referer_data['results'].delete('browser.window.hostport')
+ referer_data['request'] = double('Request', ip: '127.0.0.1', referer: 'https://example.com/page', env: {})
+ 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(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ 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=).with('example.com')
+ allow(zombie).to receive(:port=).with(443)
+ 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)
+ described_class.new(referer_data)
+ expect(zombie).to have_received(:domain=).with('example.com')
+ expect(zombie).to have_received(:port=).with(443)
+ end
+
+ it 'falls back to unknown domain when hostname and referer are missing' do
+ unknown_data = data.dup
+ unknown_data['results'].delete('browser.window.hostname')
+ unknown_data['request'] = double('Request', ip: '127.0.0.1', referer: nil, env: {})
+ 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(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ 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=).with('unknown')
+ 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)
+ described_class.new(unknown_data)
+ expect(zombie).to have_received(:domain=).with('unknown')
+ end
+
+ it 'parses HTTP headers from request env' do
+ env_data = data.dup
+ env_data['request'] = double('Request',
+ ip: '127.0.0.1',
+ referer: 'http://example.com',
+ env: { 'HTTP_USER_AGENT' => 'Mozilla/5.0', 'HTTP_ACCEPT' => 'text/html' })
+ 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(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ 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=) do |headers|
+ parsed = JSON.parse(headers)
+ expect(parsed).to have_key('USER_AGENT')
+ expect(parsed).to have_key('ACCEPT')
+ expect(parsed['USER_AGENT']).to eq('Mozilla/5.0')
+ end
+ allow(zombie).to receive(:httpheaders).and_return('{}')
+ allow(zombie).to receive(:save!)
+ allow(JSON).to receive(:parse).and_call_original
+ allow(JSON).to receive(:parse).with('{}').and_return({})
+ allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
+ described_class.new(env_data)
+ end
+
+ it 'performs DNS hostname lookup when enabled' do
+ allow(config).to receive(:get).with('beef.dns_hostname_lookup').and_return(true)
+ 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_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ allow(BeEF::Core::Models::BrowserDetails).to receive(:set)
+ allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox')
+ allow(Resolv).to receive(:getname).with('127.0.0.1').and_return('localhost')
+ 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)
+ expect(Resolv).to receive(:getname).with('127.0.0.1')
+ expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'host.name', 'localhost')
+ described_class.new(data)
+ end
+
+ it 'handles GeoIP lookup when enabled' do
+ allow(BeEF::Core::GeoIp.instance).to receive(:enabled?).and_return(true)
+ geoip_data = {
+ 'city' => { 'names' => { 'en' => 'San Francisco' } },
+ 'country' => { 'names' => { 'en' => 'United States' }, 'iso_code' => 'US' },
+ 'registered_country' => { 'names' => { 'en' => 'United States' }, 'iso_code' => 'US' },
+ 'continent' => { 'names' => { 'en' => 'North America' }, 'code' => 'NA' },
+ 'location' => { 'latitude' => 37.7749, 'longitude' => -122.4194, 'time_zone' => 'America/Los_Angeles' }
+ }
+ allow(BeEF::Core::GeoIp.instance).to receive(:lookup).with('127.0.0.1').and_return(geoip_data)
+ 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(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ 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)
+ expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'location.city', 'San Francisco')
+ expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'location.country', 'United States')
+ described_class.new(data)
+ end
+
+ it 'detects and stores proxy information' do
+ proxy_data = data.dup
+ proxy_data['request'] = double('Request',
+ ip: '127.0.0.1',
+ referer: 'http://example.com',
+ env: { 'HTTP_X_FORWARDED_FOR' => '192.168.1.1', 'HTTP_VIA' => 'proxy.example.com' })
+ 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(true)
+ allow(BeEF::Filters).to receive(:is_valid_cookies?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_osname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hwname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_date_stamp?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagetitle?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_url?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_pagereferrer?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_hostname?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_port?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_browser_plugins?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_system_platform?).and_return(true)
+ allow(BeEF::Filters).to receive(:nums_only?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_yes_no?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_memory?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_gpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:is_valid_cpu?).and_return(true)
+ allow(BeEF::Filters).to receive(:alphanums_only?).and_return(true)
+ 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=)
+ headers_json = '{"X_FORWARDED_FOR":"192.168.1.1","VIA":"proxy.example.com"}'
+ allow(zombie).to receive(:httpheaders=)
+ allow(zombie).to receive(:httpheaders).and_return(headers_json)
+ allow(zombie).to receive(:save!)
+ allow(JSON).to receive(:parse).with(headers_json).and_return({ 'X_FORWARDED_FOR' => '192.168.1.1', 'VIA' => 'proxy.example.com' })
+ allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie)
+ expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'network.proxy', 'Yes')
+ expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'network.proxy.client', '192.168.1.1')
+ expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'network.proxy.server', 'proxy.example.com')
+ described_class.new(proxy_data)
+ end
+ end
+end
diff --git a/spec/beef/core/main/handlers/commands_spec.rb b/spec/beef/core/main/handlers/commands_spec.rb
new file mode 100644
index 000000000..e9bff082e
--- /dev/null
+++ b/spec/beef/core/main/handlers/commands_spec.rb
@@ -0,0 +1,180 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Handlers::Commands do
+ let(:mock_request) do
+ double('request',
+ params: { 'cid' => 123, 'beefhook' => 'test_session_id', 'results' => { 'data' => 'test' } },
+ env: { 'HTTP_USER_AGENT' => 'Mozilla/5.0' })
+ end
+
+ let(:data) do
+ {
+ 'request' => mock_request,
+ 'status' => 1,
+ 'results' => { 'data' => 'test' },
+ 'cid' => 123,
+ 'beefhook' => 'test_session_id'
+ }
+ end
+
+ let(:mock_command_class) do
+ Class.new do
+ def initialize(_key)
+ @friendlyname = 'Test Command'
+ end
+
+ attr_accessor :session_id
+
+ def friendlyname
+ @friendlyname
+ end
+
+ def build_callback_datastore(_result, _command_id, _beefhook, _http_params, _http_header); end
+
+ def post_execute; end
+ end
+ end
+
+ before do
+ allow(BeEF::Core::Command).to receive(:const_get).and_return(mock_command_class)
+ allow(BeEF::Module).to receive(:get_key_by_class).and_return('test_module')
+ allow(BeEF::Core::Models::Command).to receive(:save_result).and_return(true)
+ end
+
+ describe '#initialize' do
+ it 'initializes with data and class name' do
+ handler = described_class.new(data, 'test')
+ expect(handler.instance_variable_get(:@data)).to eq(data)
+ end
+ end
+
+ describe '#get_param' do
+ let(:handler) { described_class.new(data, 'test') }
+
+ it 'returns value when key exists' do
+ expect(handler.get_param(data, 'status')).to eq(1)
+ end
+
+ it 'returns nil when key does not exist' do
+ expect(handler.get_param(data, 'nonexistent')).to be_nil
+ end
+
+ it 'returns nil when query is not a hash' do
+ expect(handler.get_param('not a hash', 'key')).to be_nil
+ end
+ end
+
+ describe '#setup' do
+ context 'with valid parameters' do
+ it 'processes command successfully' do
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).and_return(true)
+ handler = described_class.new(data, 'test')
+ expect(BeEF::Core::Models::Command).to receive(:save_result).with(
+ 'test_session_id',
+ 123,
+ 'Test Command',
+ { 'data' => { 'data' => 'test' } },
+ 1
+ )
+ handler.setup
+ end
+ end
+
+ context 'with invalid command id' do
+ let(:invalid_data) do
+ {
+ 'request' => double('request', params: { 'cid' => 'not_an_integer' }, env: {}),
+ 'status' => 1,
+ 'results' => {}
+ }
+ end
+
+ it 'returns early without saving' do
+ handler = described_class.new(invalid_data, 'test')
+ expect(BeEF::Core::Models::Command).not_to receive(:save_result)
+ handler.setup
+ end
+ end
+
+ context 'with invalid session id' do
+ let(:invalid_data) do
+ {
+ 'request' => double('request', params: { 'cid' => 123, 'beefhook' => 'invalid' }, env: {}),
+ 'status' => 1,
+ 'results' => {}
+ }
+ end
+
+ it 'returns early without saving' do
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).and_return(false)
+ handler = described_class.new(invalid_data, 'test')
+ expect(BeEF::Core::Models::Command).not_to receive(:save_result)
+ handler.setup
+ end
+ end
+
+ context 'with empty friendly name' do
+ let(:empty_friendlyname_command) do
+ Class.new do
+ def initialize(_key)
+ @friendlyname = ''
+ end
+
+ attr_accessor :session_id
+
+ def friendlyname
+ @friendlyname
+ end
+
+ def build_callback_datastore(_result, _command_id, _beefhook, _http_params, _http_header); end
+ end
+ end
+
+ it 'returns early without saving' do
+ allow(BeEF::Core::Command).to receive(:const_get).and_return(empty_friendlyname_command)
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).and_return(true)
+ handler = described_class.new(data, 'test')
+ expect(BeEF::Core::Models::Command).not_to receive(:save_result)
+ handler.setup
+ end
+ end
+
+ context 'with invalid status' do
+ let(:invalid_status_data) do
+ {
+ 'request' => mock_request,
+ 'status' => 'not_an_integer',
+ 'results' => { 'data' => 'test' }
+ }
+ end
+
+ it 'returns early without saving' do
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).and_return(true)
+ handler = described_class.new(invalid_status_data, 'test')
+ expect(BeEF::Core::Models::Command).not_to receive(:save_result)
+ handler.setup
+ end
+ end
+
+ context 'with empty results' do
+ let(:empty_results_data) do
+ {
+ 'request' => mock_request,
+ 'status' => 1,
+ 'results' => {}
+ }
+ end
+
+ it 'returns early without saving' do
+ allow(BeEF::Filters).to receive(:is_valid_hook_session_id?).and_return(true)
+ handler = described_class.new(empty_results_data, 'test')
+ expect(BeEF::Core::Models::Command).not_to receive(:save_result)
+ handler.setup
+ end
+ end
+ end
+end
diff --git a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb
new file mode 100644
index 000000000..595b7f9e8
--- /dev/null
+++ b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb
@@ -0,0 +1,40 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Handlers::HookedBrowsers do
+ # Test the confirm_browser_user_agent logic directly
+ describe 'confirm_browser_user_agent logic' do
+ it 'matches legacy browser user agents' do
+ allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0'])
+
+ # Test the logic: browser_type = user_agent.split(' ').last
+ user_agent = 'Mozilla/5.0 IE 8.0'
+ browser_type = user_agent.split(' ').last
+
+ # Test the matching logic
+ matched = false
+ BeEF::Core::Models::LegacyBrowserUserAgents.user_agents.each do |ua_string|
+ matched = true if ua_string.include?(browser_type)
+ end
+
+ expect(matched).to be true
+ end
+
+ it 'does not match non-legacy browser user agents' do
+ allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return([])
+
+ user_agent = 'Chrome/91.0'
+ browser_type = user_agent.split(' ').last
+
+ matched = false
+ BeEF::Core::Models::LegacyBrowserUserAgents.user_agents.each do |ua_string|
+ matched = true if ua_string.include?(browser_type)
+ end
+
+ expect(matched).to be false
+ end
+ end
+end
diff --git a/spec/beef/core/main/migration_spec.rb b/spec/beef/core/main/migration_spec.rb
new file mode 100644
index 000000000..537625b23
--- /dev/null
+++ b/spec/beef/core/main/migration_spec.rb
@@ -0,0 +1,102 @@
+#
+# 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::Migration' do
+ let(:migration) { BeEF::Core::Migration.instance }
+ let(:config) { BeEF::Core::Configuration.instance }
+ let(:api_registrar) { BeEF::API::Registrar.instance }
+
+ describe '.instance' do
+ it 'returns a singleton instance' do
+ instance1 = BeEF::Core::Migration.instance
+ instance2 = BeEF::Core::Migration.instance
+ expect(instance1).to be(instance2)
+ end
+ end
+
+ describe '#update_db!' do
+ it 'calls update_commands!' do
+ expect(migration).to receive(:update_commands!)
+ expect(migration.update_db!).to be_nil
+ end
+ end
+
+ describe '#update_commands!' do
+ before do
+ # Clear existing modules from database
+ BeEF::Core::Models::CommandModule.destroy_all
+
+ # Mock API registrar to verify it's called
+ allow(api_registrar).to receive(:fire)
+ end
+
+ it 'creates new modules from config that are not in database' do
+ # Setup config with a new module
+ module_config = {
+ 'test_module' => { 'path' => 'modules/test/' }
+ }
+ allow(config).to receive(:get).with('beef.module').and_return(module_config)
+ allow(config).to receive(:get).with('beef.module.test_module').and_return({ 'path' => 'modules/test/' })
+
+ initial_count = BeEF::Core::Models::CommandModule.count
+ migration.update_commands!
+
+ expect(BeEF::Core::Models::CommandModule.count).to eq(initial_count + 1)
+ created_module = BeEF::Core::Models::CommandModule.find_by(name: 'test_module')
+ expect(created_module).not_to be_nil
+ expect(created_module.path).to eq('modules/test/module.rb')
+ end
+
+ it 'updates config with database IDs and paths for existing modules' do
+ # Create a module in the database first
+ existing_module = BeEF::Core::Models::CommandModule.create!(
+ name: 'existing_module',
+ path: 'modules/existing/module.rb'
+ )
+
+ # Setup config to include this existing module
+ module_config = {
+ 'existing_module' => { 'path' => 'modules/existing/' }
+ }
+ allow(config).to receive(:get).with('beef.module').and_return(module_config)
+ allow(config).to receive(:get).with('beef.module.existing_module').and_return({ 'path' => 'modules/existing/' })
+ allow(config).to receive(:set)
+
+ migration.update_commands!
+
+ expect(config).to have_received(:set).with('beef.module.existing_module.db.id', existing_module.id)
+ expect(config).to have_received(:set).with('beef.module.existing_module.db.path', 'modules/existing/module.rb')
+ end
+
+ it 'fires the migrate_commands API event' do
+ allow(config).to receive(:get).with('beef.module').and_return({})
+ migration.update_commands!
+ expect(api_registrar).to have_received(:fire).with(BeEF::API::Migration, 'migrate_commands')
+ end
+
+ it 'does not create modules that already exist in database' do
+ # Create a module in the database
+ BeEF::Core::Models::CommandModule.create!(
+ name: 'existing_module',
+ path: 'modules/existing/module.rb'
+ )
+
+ # Setup config with the same module
+ module_config = {
+ 'existing_module' => { 'path' => 'modules/existing/' }
+ }
+ allow(config).to receive(:get).with('beef.module').and_return(module_config)
+ allow(config).to receive(:get).with('beef.module.existing_module').and_return({ 'path' => 'modules/existing/' })
+
+ initial_count = BeEF::Core::Models::CommandModule.count
+ migration.update_commands!
+
+ # Should not create a duplicate
+ expect(BeEF::Core::Models::CommandModule.count).to eq(initial_count)
+ end
+ end
+end
diff --git a/spec/beef/core/main/models/browser_details_spec.rb b/spec/beef/core/main/models/browser_details_spec.rb
index dd2661f07..3ed884a35 100644
--- a/spec/beef/core/main/models/browser_details_spec.rb
+++ b/spec/beef/core/main/models/browser_details_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF BrowserDetails' do
before(:all) do
diff --git a/spec/beef/core/main/models/legacybrowseruseragents_spec.rb b/spec/beef/core/main/models/legacybrowseruseragents_spec.rb
new file mode 100644
index 000000000..23718c5b7
--- /dev/null
+++ b/spec/beef/core/main/models/legacybrowseruseragents_spec.rb
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Models::LegacyBrowserUserAgents do
+ describe '.user_agents' do
+ it 'returns an array' do
+ expect(described_class.user_agents).to be_a(Array)
+ end
+
+ it 'returns an array that can be iterated' do
+ result = described_class.user_agents.map { |ua| ua }
+
+ expect(result).to be_a(Array)
+ end
+ end
+end
diff --git a/spec/beef/core/main/models/optioncache_spec.rb b/spec/beef/core/main/models/optioncache_spec.rb
new file mode 100644
index 000000000..0591a52e4
--- /dev/null
+++ b/spec/beef/core/main/models/optioncache_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
+#
+
+RSpec.describe BeEF::Core::Models::OptionCache do
+ describe '.first_or_create' do
+ it 'creates a new option cache with a name' do
+ name = 'test_option'
+ option = described_class.first_or_create(name: name)
+
+ expect(option).to be_persisted
+ expect(option.name).to eq(name)
+ expect(option.value).to be_nil
+ end
+
+ it 'returns existing option cache if it already exists' do
+ name = 'existing_option'
+ existing = described_class.create!(name: name, value: 'existing_value')
+
+ option = described_class.first_or_create(name: name)
+
+ expect(option.id).to eq(existing.id)
+ expect(option.name).to eq(name)
+ expect(option.value).to eq('existing_value')
+ end
+ end
+
+ describe '.where' do
+ it 'finds option cache by name' do
+ name = 'findable_option'
+ described_class.create!(name: name, value: 'test_value')
+
+ option = described_class.where(name: name).first
+
+ expect(option).not_to be_nil
+ expect(option.name).to eq(name)
+ expect(option.value).to eq('test_value')
+ end
+
+ it 'returns nil when option cache does not exist' do
+ option = described_class.where(name: 'non_existent').first
+
+ expect(option).to be_nil
+ end
+ end
+
+ describe 'attributes' do
+ it 'can set and retrieve name' do
+ option = described_class.new(name: 'test_name')
+ expect(option.name).to eq('test_name')
+ end
+
+ it 'can set and retrieve value' do
+ option = described_class.new(value: 'test_value')
+ expect(option.value).to eq('test_value')
+ end
+ end
+end
diff --git a/spec/beef/core/main/models/result_spec.rb b/spec/beef/core/main/models/result_spec.rb
new file mode 100644
index 000000000..836c7eff3
--- /dev/null
+++ b/spec/beef/core/main/models/result_spec.rb
@@ -0,0 +1,52 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Models::Result do
+ describe 'associations' do
+ it 'has_one command' do
+ expect(described_class.reflect_on_association(:command)).not_to be_nil
+ expect(described_class.reflect_on_association(:command).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 '.create' do
+ let(:hooked_browser) { BeEF::Core::Models::HookedBrowser.create!(session: 'test_session', ip: '127.0.0.1') }
+ let(:command_module) { BeEF::Core::Models::CommandModule.create!(name: 'test_module', path: 'modules/test/') }
+ let(:command) { BeEF::Core::Models::Command.create!(hooked_browser_id: hooked_browser.id, command_module_id: command_module.id) }
+
+ it 'creates a result with required attributes' do
+ result = described_class.create!(
+ hooked_browser_id: hooked_browser.id,
+ command_id: command.id,
+ data: { 'test' => 'data' }.to_json,
+ status: 0,
+ date: Time.now.to_i
+ )
+
+ expect(result).to be_persisted
+ expect(result.hooked_browser_id).to eq(hooked_browser.id)
+ expect(result.command_id).to eq(command.id)
+ expect(result.status).to eq(0)
+ end
+
+ it 'can access command_id' do
+ result = described_class.create!(
+ hooked_browser_id: hooked_browser.id,
+ command_id: command.id,
+ data: {}.to_json,
+ status: 0,
+ date: Time.now.to_i
+ )
+
+ expect(result.command_id).to eq(command.id)
+ end
+ end
+end
diff --git a/spec/beef/core/main/network_stack/assethandler_spec.rb b/spec/beef/core/main/network_stack/assethandler_spec.rb
new file mode 100644
index 000000000..b6d6f0ae5
--- /dev/null
+++ b/spec/beef/core/main/network_stack/assethandler_spec.rb
@@ -0,0 +1,149 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::NetworkStack::Handlers::AssetHandler do
+ let(:handler) { described_class.instance }
+
+ before do
+ @mock_server = double('server', mount: true, unmount: true, remap: true)
+ allow(BeEF::Core::Server).to receive(:instance).and_return(@mock_server)
+ # Reset singleton state
+ handler.instance_variable_set(:@allocations, {})
+ handler.instance_variable_set(:@sockets, {})
+ handler.instance_variable_set(:@http_server, @mock_server)
+ end
+
+ describe '#initialize' do
+ it 'initializes with empty allocations and sockets' do
+ expect(handler.allocations).to eq({})
+ expect(handler.root_dir).to be_a(String)
+ end
+ end
+
+ describe '#build_url' do
+ it 'returns path when path is provided' do
+ expect(handler.build_url('/test', nil)).to eq('/test')
+ end
+
+ it 'appends extension when provided' do
+ expect(handler.build_url('/test', 'js')).to eq('/test.js')
+ end
+
+ it 'generates random URL when path is nil' do
+ url = handler.build_url(nil, nil)
+ expect(url).to start_with('/')
+ expect(url.length).to be > 1
+ end
+
+ it 'generates random URL with extension when path is nil' do
+ url = handler.build_url(nil, 'js')
+ expect(url).to end_with('.js')
+ expect(url).to start_with('/')
+ end
+ end
+
+ describe '#check' do
+ it 'returns false when URL is not allocated' do
+ expect(handler.check('/nonexistent')).to be false
+ end
+
+ it 'returns true when count is -1 (unlimited)' do
+ handler.instance_variable_set(:@allocations, { '/test' => { 'count' => -1 } })
+ expect(handler.check('/test')).to be true
+ end
+
+ it 'decrements count and returns true when count > 0' do
+ handler.instance_variable_set(:@allocations, { '/test' => { 'count' => 2 } })
+ expect(handler.check('/test')).to be true
+ expect(handler.allocations['/test']['count']).to eq(1)
+ end
+
+ it 'unbinds when count reaches 0' do
+ handler.instance_variable_set(:@allocations, { '/test' => { 'count' => 1 } })
+ expect(handler).to receive(:unbind).with('/test')
+ handler.check('/test')
+ end
+
+ it 'returns false when count is 0' do
+ handler.instance_variable_set(:@allocations, { '/test' => { 'count' => 0 } })
+ expect(handler.check('/test')).to be false
+ end
+ end
+
+ describe '#bind_redirect' do
+ it 'binds redirector to URL' do
+ expect(@mock_server).to receive(:mount)
+ expect(@mock_server).to receive(:remap)
+ url = handler.bind_redirect('http://example.com', '/redirect')
+ expect(url).to eq('/redirect')
+ expect(handler.allocations['/redirect']).to eq({ 'target' => 'http://example.com' })
+ end
+
+ it 'generates random URL when path is nil' do
+ expect(@mock_server).to receive(:mount)
+ expect(@mock_server).to receive(:remap)
+ url = handler.bind_redirect('http://example.com')
+ expect(url).to start_with('/')
+ expect(handler.allocations[url]).not_to be_nil
+ end
+ end
+
+ describe '#bind_raw' do
+ it 'binds raw HTTP response to URL' do
+ expect(@mock_server).to receive(:mount)
+ expect(@mock_server).to receive(:remap)
+ url = handler.bind_raw('200', { 'Content-Type' => 'text/html' }, '', '/raw')
+ expect(url).to eq('/raw')
+ expect(handler.allocations['/raw']).to eq({})
+ end
+ end
+
+ describe '#bind' do
+ let(:test_file) { '/spec/support/assets/test.txt' }
+ let(:test_file_path) { File.join(handler.root_dir, test_file) }
+
+ before do
+ FileUtils.mkdir_p(File.dirname(test_file_path))
+ File.write(test_file_path, 'test content')
+ end
+
+ after do
+ FileUtils.rm_f(test_file_path)
+ end
+
+ it 'binds file to URL when file exists' do
+ expect(@mock_server).to receive(:mount)
+ expect(@mock_server).to receive(:remap)
+ url = handler.bind(test_file, '/test')
+ expect(url).to eq('/test')
+ expect(handler.allocations['/test']['file']).to include(test_file)
+ end
+
+ it 'returns nil when file does not exist' do
+ expect(@mock_server).not_to receive(:mount)
+ result = handler.bind('/nonexistent/file.txt', '/test')
+ expect(result).to be_nil
+ end
+
+ it 'uses text/plain content type when extension is nil' do
+ expect(@mock_server).to receive(:mount) do |_url, handler_obj|
+ expect(handler_obj.instance_variable_get(:@header)['Content-Type']).to eq('text/plain')
+ end
+ expect(@mock_server).to receive(:remap)
+ handler.bind(test_file, '/test', nil)
+ end
+ end
+
+ describe '#unbind' do
+ it 'removes allocation and unmounts URL' do
+ handler.instance_variable_set(:@allocations, { '/test' => {} })
+ expect(@mock_server).to receive(:unmount).with('/test')
+ expect(@mock_server).to receive(:remap)
+ handler.unbind('/test')
+ expect(handler.allocations).not_to have_key('/test')
+ end
+ end
+end
diff --git a/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb b/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb
index 4a0899648..58d908991 100644
--- a/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb
+++ b/spec/beef/core/main/network_stack/handlers/dynamic_reconstruction_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF Dynamic Reconsturction' do
before(:all) do
diff --git a/spec/beef/core/main/network_stack/handlers/raw_spec.rb b/spec/beef/core/main/network_stack/handlers/raw_spec.rb
new file mode 100644
index 000000000..93c08fe14
--- /dev/null
+++ b/spec/beef/core/main/network_stack/handlers/raw_spec.rb
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::NetworkStack::Handlers::Raw do
+ describe '#initialize' do
+ it 'initializes with status, header, and body' do
+ handler = described_class.new('200', { 'Content-Type' => 'text/html' }, '')
+ expect(handler.instance_variable_get(:@status)).to eq('200')
+ expect(handler.instance_variable_get(:@header)).to eq({ 'Content-Type' => 'text/html' })
+ expect(handler.instance_variable_get(:@body)).to eq('')
+ end
+
+ it 'initializes with default empty header and nil body' do
+ handler = described_class.new('404')
+ expect(handler.instance_variable_get(:@status)).to eq('404')
+ expect(handler.instance_variable_get(:@header)).to eq({})
+ expect(handler.instance_variable_get(:@body)).to be_nil
+ end
+ end
+
+ describe '#call' do
+ it 'returns Rack::Response with correct status, header, and body' do
+ handler = described_class.new('200', { 'Content-Type' => 'text/html' }, '')
+ response = handler.call({})
+
+ expect(response).to be_a(Rack::Response)
+ expect(response.status).to eq(200)
+ expect(response.headers['Content-Type']).to eq('text/html')
+ expect(response.body).to eq([''])
+ end
+
+ it 'handles different status codes' do
+ handler = described_class.new('404', {}, 'Not Found')
+ response = handler.call({})
+
+ expect(response.status).to eq(404)
+ expect(response.body).to eq(['Not Found'])
+ end
+
+ it 'handles nil body' do
+ handler = described_class.new('204', {})
+ response = handler.call({})
+
+ expect(response.status).to eq(204)
+ expect(response.body).to eq([])
+ end
+ end
+end
diff --git a/spec/beef/core/main/network_stack/handlers/redirector_spec.rb b/spec/beef/core/main/network_stack/handlers/redirector_spec.rb
index df78232ca..490642da4 100644
--- a/spec/beef/core/main/network_stack/handlers/redirector_spec.rb
+++ b/spec/beef/core/main/network_stack/handlers/redirector_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF Redirector' do
before(:all) do
diff --git a/spec/beef/core/main/router/router_spec.rb b/spec/beef/core/main/router/router_spec.rb
new file mode 100644
index 000000000..e2abf79fc
--- /dev/null
+++ b/spec/beef/core/main/router/router_spec.rb
@@ -0,0 +1,149 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Router::Router do
+ let(:config) { BeEF::Core::Configuration.instance }
+
+ # Create a test instance that we can call private methods on
+ let(:router_instance) do
+ instance = described_class.allocate
+ instance.instance_variable_set(:@config, config)
+ instance
+ end
+
+ describe '#response_headers' do
+ it 'returns default headers when web server imitation is disabled' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(false)
+ headers = router_instance.send(:response_headers)
+ expect(headers['Server']).to eq('')
+ expect(headers['Content-Type']).to eq('text/html')
+ end
+
+ it 'returns Apache headers when type is apache' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('apache')
+ headers = router_instance.send(:response_headers)
+ expect(headers['Server']).to eq('Apache/2.2.3 (CentOS)')
+ expect(headers['Content-Type']).to eq('text/html; charset=UTF-8')
+ end
+
+ it 'returns IIS headers when type is iis' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('iis')
+ headers = router_instance.send(:response_headers)
+ expect(headers['Server']).to eq('Microsoft-IIS/6.0')
+ expect(headers['X-Powered-By']).to eq('ASP.NET')
+ expect(headers['Content-Type']).to eq('text/html; charset=UTF-8')
+ end
+
+ it 'returns nginx headers when type is nginx' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('nginx')
+ headers = router_instance.send(:response_headers)
+ expect(headers['Server']).to eq('nginx')
+ expect(headers['Content-Type']).to eq('text/html')
+ end
+
+ it 'returns default headers for invalid type' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('invalid')
+ headers = router_instance.send(:response_headers)
+ expect(headers['Server']).to eq('')
+ expect(headers['Content-Type']).to eq('text/html')
+ end
+ end
+
+ describe '#index_page' do
+ it 'returns empty string when web server imitation is disabled' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(false)
+ result = router_instance.send(:index_page)
+ expect(result).to eq('')
+ end
+
+ it 'returns Apache index page when enabled and type is apache' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('apache')
+ allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_root').and_return(false)
+ result = router_instance.send(:index_page)
+ expect(result).to include('Apache HTTP Server Test Page')
+ expect(result).to include('powered by CentOS')
+ end
+
+ it 'returns IIS index page when enabled and type is iis' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('iis')
+ allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_root').and_return(false)
+ result = router_instance.send(:index_page)
+ expect(result).to include('Under Construction')
+ end
+
+ it 'returns nginx index page when enabled and type is nginx' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('nginx')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_root').and_return(false)
+ # nginx doesn't use base_path, but the method might check it
+ allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
+ result = router_instance.send(:index_page)
+ expect(result).to include('Welcome to nginx!')
+ end
+
+ it 'includes hook script when hook_root is enabled' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('apache')
+ allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_root').and_return(true)
+ allow(config).to receive(:get).with('beef.http.hook_file').and_return('/hook.js')
+ result = router_instance.send(:index_page)
+ expect(result).to include("")
+ end
+ end
+
+ describe '#error_page_404' do
+ it 'returns simple message when web server imitation is disabled' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(false)
+ result = router_instance.send(:error_page_404)
+ expect(result).to eq('Not Found.')
+ end
+
+ it 'returns Apache 404 page when enabled and type is apache' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('apache')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_404').and_return(false)
+ result = router_instance.send(:error_page_404)
+ expect(result).to include('404 Not Found')
+ expect(result).to include('Apache/2.2.3 (CentOS)')
+ end
+
+ it 'returns IIS 404 page when enabled and type is iis' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('iis')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_404').and_return(false)
+ result = router_instance.send(:error_page_404)
+ expect(result).to include('The page cannot be found')
+ end
+
+ it 'returns nginx 404 page when enabled and type is nginx' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('nginx')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_404').and_return(false)
+ result = router_instance.send(:error_page_404)
+ expect(result).to include('404 Not Found')
+ expect(result).to include('nginx')
+ end
+
+ it 'includes hook script when hook_404 is enabled' do
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.type').and_return('apache')
+ allow(config).to receive(:get).with('beef.http.web_server_imitation.hook_404').and_return(true)
+ allow(config).to receive(:get).with('beef.http.hook_file').and_return('/hook.js')
+ result = router_instance.send(:error_page_404)
+ expect(result).to include("")
+ end
+ end
+
+end
diff --git a/spec/beef/core/main/server_spec.rb b/spec/beef/core/main/server_spec.rb
new file mode 100644
index 000000000..b694c9bb1
--- /dev/null
+++ b/spec/beef/core/main/server_spec.rb
@@ -0,0 +1,113 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Core::Server do
+ let(:config) { BeEF::Core::Configuration.instance }
+ let(:server) { described_class.instance }
+
+ before do
+ # Reset singleton instance for each test
+ described_class.instance_variable_set(:@singleton__instance__, nil)
+ end
+
+ describe '#initialize' do
+ it 'initializes with configuration' do
+ expect(server.configuration).to eq(config)
+ end
+
+ it 'sets root_dir' do
+ expect(server.root_dir).to be_a(String)
+ expect(server.root_dir).to be_a(Pathname).or(be_a(String))
+ end
+
+ it 'initializes empty mounts hash' do
+ expect(server.mounts).to eq({})
+ end
+
+ it 'initializes empty command_urls hash' do
+ expect(server.command_urls).to eq({})
+ end
+
+ it 'creates a semaphore' do
+ expect(server.semaphore).to be_a(Mutex)
+ end
+ end
+
+ describe '#to_h' do
+ it 'returns a hash with server information' do
+ result = server.to_h
+ expect(result).to be_a(Hash)
+ expect(result).to have_key('beef_url')
+ expect(result).to have_key('beef_root_dir')
+ expect(result).to have_key('beef_host')
+ expect(result).to have_key('beef_port')
+ end
+
+ it 'includes hook file path' do
+ # The to_h method calls config.get, so we need to allow it
+ allow(config).to receive(:get).and_call_original
+ allow(config).to receive(:get).with('beef.http.hook_file').and_return('/hook.js')
+ result = server.to_h
+ expect(result['beef_hook']).to eq('/hook.js')
+ end
+ end
+
+ describe '#mount' do
+ it 'mounts a handler without arguments' do
+ handler_class = Class.new
+ server.mount('/test', handler_class)
+ expect(server.mounts['/test']).to eq(handler_class)
+ end
+
+ it 'mounts a handler with arguments' do
+ handler_class = Class.new
+ server.mount('/test', handler_class, 'arg1')
+ expect(server.mounts['/test']).to eq([handler_class, 'arg1'])
+ end
+
+ it 'raises TypeError for non-string URL' do
+ handler_class = Class.new
+ expect { server.mount(123, handler_class) }.to raise_error(TypeError, /"url" needs to be a string/)
+ end
+
+ it 'overwrites existing mount' do
+ handler1 = Class.new
+ handler2 = Class.new
+ server.mount('/test', handler1)
+ server.mount('/test', handler2)
+ expect(server.mounts['/test']).to eq(handler2)
+ end
+ end
+
+ describe '#unmount' do
+ it 'removes a mounted handler' do
+ handler_class = Class.new
+ server.mount('/test', handler_class)
+ server.unmount('/test')
+ expect(server.mounts).not_to have_key('/test')
+ end
+
+ it 'raises TypeError for non-string URL' do
+ expect { server.unmount(123) }.to raise_error(TypeError, /"url" needs to be a string/)
+ end
+
+ it 'does nothing if URL is not mounted' do
+ expect { server.unmount('/nonexistent') }.not_to raise_error
+ expect(server.mounts).not_to have_key('/nonexistent')
+ end
+ end
+
+ describe '#remap' do
+ it 'calls remap on rack_app with mounts' do
+ handler_class = Class.new
+ server.mount('/test', handler_class)
+ mock_rack_app = double('Rack::URLMap')
+ server.instance_variable_set(:@rack_app, mock_rack_app)
+ expect(mock_rack_app).to receive(:remap).with(server.mounts)
+ server.remap
+ end
+ end
+end
diff --git a/spec/beef/core/module_spec.rb b/spec/beef/core/module_spec.rb
new file mode 100644
index 000000000..bc9d511e4
--- /dev/null
+++ b/spec/beef/core/module_spec.rb
@@ -0,0 +1,189 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
+RSpec.describe BeEF::Module do
+ let(:config) { BeEF::Core::Configuration.instance }
+
+ describe '.is_present' do
+ it 'returns true when module exists in configuration' do
+ allow(config).to receive(:get).with('beef.module').and_return({ 'test_module' => {} })
+ expect(described_class.is_present('test_module')).to be true
+ end
+
+ it 'returns false when module does not exist' do
+ allow(config).to receive(:get).with('beef.module').and_return({})
+ expect(described_class.is_present('nonexistent')).to be false
+ end
+ end
+
+ describe '.is_enabled' do
+ it 'returns true when module is present and enabled' do
+ allow(config).to receive(:get).with('beef.module').and_return({ 'test_module' => {} })
+ allow(config).to receive(:get).with('beef.module.test_module.enable').and_return(true)
+ expect(described_class.is_enabled('test_module')).to be true
+ end
+
+ it 'returns false when module is not present' do
+ allow(config).to receive(:get).with('beef.module').and_return({})
+ expect(described_class.is_enabled('nonexistent')).to be false
+ end
+
+ it 'returns false when module is disabled' do
+ allow(config).to receive(:get).with('beef.module').and_return({ 'test_module' => {} })
+ allow(config).to receive(:get).with('beef.module.test_module.enable').and_return(false)
+ expect(described_class.is_enabled('test_module')).to be false
+ end
+ end
+
+ describe '.is_loaded' do
+ it 'returns true when module is enabled and loaded' do
+ allow(config).to receive(:get).with('beef.module').and_return({ 'test_module' => {} })
+ allow(config).to receive(:get).with('beef.module.test_module.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.module.test_module.loaded').and_return(true)
+ expect(described_class.is_loaded('test_module')).to be true
+ end
+
+ it 'returns false when module is not loaded' do
+ allow(config).to receive(:get).with('beef.module').and_return({ 'test_module' => {} })
+ allow(config).to receive(:get).with('beef.module.test_module.enable').and_return(true)
+ allow(config).to receive(:get).with('beef.module.test_module.loaded').and_return(false)
+ expect(described_class.is_loaded('test_module')).to be false
+ end
+ end
+
+ describe '.get_key_by_database_id' do
+ it 'returns module key for matching database id' do
+ modules = {
+ 'module1' => { 'db' => { 'id' => 1 } },
+ 'module2' => { 'db' => { 'id' => 2 } }
+ }
+ allow(config).to receive(:get).with('beef.module').and_return(modules)
+ expect(described_class.get_key_by_database_id(2)).to eq('module2')
+ end
+
+ it 'returns nil when no module matches' do
+ allow(config).to receive(:get).with('beef.module').and_return({})
+ expect(described_class.get_key_by_database_id(999)).to be_nil
+ end
+ end
+
+ describe '.get_key_by_class' do
+ it 'returns module key for matching class' do
+ modules = {
+ 'module1' => { 'class' => 'TestClass1' },
+ 'module2' => { 'class' => 'TestClass2' }
+ }
+ allow(config).to receive(:get).with('beef.module').and_return(modules)
+ expect(described_class.get_key_by_class('TestClass2')).to eq('module2')
+ end
+ end
+
+ describe '.exists?' do
+ it 'returns true when class exists' do
+ test_class = Class.new
+ BeEF::Core::Command.const_set(:Testmodule, test_class)
+ expect(described_class.exists?('testmodule')).to be true
+ BeEF::Core::Command.send(:remove_const, :Testmodule)
+ end
+
+ it 'returns false when class does not exist' do
+ expect(described_class.exists?('NonexistentClass')).to be false
+ end
+ end
+
+ describe '.match_target_browser' do
+ it 'returns browser constant for valid browser string' do
+ result = described_class.match_target_browser('FF')
+ expect(result).to eq(BeEF::Core::Constants::Browsers::FF)
+ end
+
+ it 'returns false for invalid browser string' do
+ expect(described_class.match_target_browser('InvalidBrowser')).to be false
+ end
+
+ it 'returns false for non-string input' do
+ expect(described_class.match_target_browser(123)).to be false
+ end
+ end
+
+ describe '.match_target_os' do
+ it 'returns OS constant for valid OS string' do
+ result = described_class.match_target_os('Linux')
+ expect(result).to eq(BeEF::Core::Constants::Os::OS_LINUX_UA_STR)
+ end
+
+ it 'returns false for invalid OS string' do
+ expect(described_class.match_target_os('InvalidOS')).to be false
+ end
+
+ it 'returns false for non-string input' do
+ expect(described_class.match_target_os(123)).to be false
+ end
+ end
+
+ describe '.match_target_browser_spec' do
+ it 'returns hash with max_ver and min_ver' do
+ spec = { 'max_ver' => 10, 'min_ver' => 5 }
+ result = described_class.match_target_browser_spec(spec)
+ expect(result['max_ver']).to eq(10)
+ expect(result['min_ver']).to eq(5)
+ end
+
+ it 'handles latest as max_ver' do
+ spec = { 'max_ver' => 'latest' }
+ result = described_class.match_target_browser_spec(spec)
+ expect(result['max_ver']).to eq('latest')
+ end
+
+ it 'returns empty hash for non-hash input' do
+ expect(described_class.match_target_browser_spec('invalid')).to eq({})
+ end
+
+ it 'includes OS when specified' do
+ spec = { 'max_ver' => 10, 'os' => 'Linux' }
+ result = described_class.match_target_browser_spec(spec)
+ expect(result['os']).to eq(BeEF::Core::Constants::Os::OS_LINUX_UA_STR)
+ end
+ end
+
+ describe '.merge_options' do
+ it 'returns nil when module is not present' do
+ allow(config).to receive(:get).with('beef.module').and_return({})
+ expect(described_class.merge_options('nonexistent', [])).to be_nil
+ end
+
+ it 'merges default options with custom options' do
+ allow(config).to receive(:get).with('beef.module').and_return({ 'test_module' => {} })
+ allow(described_class).to receive(:is_present).and_return(true)
+ allow(described_class).to receive(:check_hard_load).and_return(true)
+ allow(described_class).to receive(:get_options).and_return(
+ [
+ { 'name' => 'option1', 'value' => 'default1' },
+ { 'name' => 'option2', 'value' => 'default2' }
+ ]
+ )
+ custom_opts = [{ 'name' => 'option1', 'value' => 'custom1' }]
+ result = described_class.merge_options('test_module', custom_opts)
+
+ expect(result.length).to eq(2)
+ expect(result.find { |o| o['name'] == 'option1' }['value']).to eq('custom1')
+ expect(result.find { |o| o['name'] == 'option2' }['value']).to eq('default2')
+ end
+ end
+
+ describe '.check_hard_load' do
+ it 'returns true when module is already loaded' do
+ allow(described_class).to receive(:is_loaded).and_return(true)
+ expect(described_class.check_hard_load('test_module')).to be true
+ end
+
+ it 'calls hard_load when module is not loaded' do
+ allow(described_class).to receive(:is_loaded).and_return(false)
+ expect(described_class).to receive(:hard_load).with('test_module')
+ described_class.check_hard_load('test_module')
+ end
+ end
+end
diff --git a/spec/beef/core/modules_spec.rb b/spec/beef/core/modules_spec.rb
index f9cb31937..c571cc922 100644
--- a/spec/beef/core/modules_spec.rb
+++ b/spec/beef/core/modules_spec.rb
@@ -1,24 +1,57 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF Modules' do
-
it 'loaded successfully' do
- expect {
- BeEF::Modules.load if BeEF::Core::Configuration.instance.get('beef.module').nil?
- }.to_not raise_error
+ config = BeEF::Core::Configuration.instance
- modules = BeEF::Core::Configuration.instance.get('beef.module').select do |k,v|
- v['enable'] == true and v['category'] != nil
+ # Force reload modules to ensure fresh state
+ BeEF::Modules.load
+
+ # Verify modules were loaded
+ all_modules = config.get('beef.module')
+ expect(all_modules).not_to be_nil, 'Modules should be loaded'
+ expect(all_modules).to be_a(Hash), 'Modules should be a hash'
+ expect(all_modules.length).to be > 0, 'At least one module should be loaded'
+
+ # Find enabled modules with categories
+ modules = all_modules.select do |_k, v|
+ v['enable'] == true && !v['category'].nil?
end
- expect(modules.length).to be > 0
- modules.each do |k,v|
+ # Provide helpful error message if no enabled modules found
+ if modules.empty?
+ enabled_count = all_modules.count { |_k, v| v['enable'] == true }
+ with_category = all_modules.count { |_k, v| !v['category'].nil? }
+ raise "No enabled modules with categories found. Total modules: #{all_modules.length}, " \
+ "Enabled: #{enabled_count}, With category: #{with_category}"
+ end
+
+ expect(modules.length).to be > 0, 'At least one enabled module with category should exist'
+
+ modules.each_key do |k|
expect(BeEF::Module.is_present(k)).to be(true)
expect(BeEF::Module.is_enabled(k)).to be(true)
- expect {
- BeEF::Module.hard_load(k)
- }.to_not raise_error
- expect(BeEF::Module.is_loaded(k)).to be(true)
- BeEF::Core::Configuration.instance.get("beef.module.#{k}.target").each do |k,v|
- expect(v).to_not be_empty
+
+ # Skip hard_load if module file doesn't exist (e.g., test modules)
+ mod_path = config.get("beef.module.#{k}.path")
+ mod_file = "#{$root_dir}/#{mod_path}/module.rb" # rubocop:disable Style/GlobalVars
+ if File.exist?(mod_file)
+ expect do
+ BeEF::Module.hard_load(k)
+ end.to_not raise_error
+ expect(BeEF::Module.is_loaded(k)).to be(true)
+ end
+
+ # Only check target if it exists
+ target = config.get("beef.module.#{k}.target")
+ next unless target.is_a?(Hash)
+
+ target.each_value do |target_value|
+ expect(target_value).to_not be_empty
end
end
end
@@ -26,9 +59,10 @@ RSpec.describe 'BeEF Modules' do
it 'safe client debug log' do
Dir['../../modules/**/*.js'].each do |path|
next unless File.file?(path)
+
File.open(path) do |f|
- f.grep(/\bconsole\.log\W*\(/m) do |line|
- fail "Function 'console.log' instead of 'beef.debug' inside\n Path: #{path}\nLine: #{line}"
+ f.grep(/\bconsole\.log\W*\(/m) do |line| # rubocop:disable Lint/UnreachableLoop -- false positive
+ raise "Function 'console.log' instead of 'beef.debug' inside\n Path: #{path}\nLine: #{line}"
end
end
end
@@ -37,12 +71,12 @@ RSpec.describe 'BeEF Modules' do
it 'safe variable decleration' do
Dir['../../modules/**/*.js'].each do |path|
next unless File.file?(path)
+
File.open(path) do |f|
- f.grep(/\blet\W+[a-zA-Z0-9_\.]+\W*=/) do |line|
- fail "Variable declared with 'let' instead of 'var' inside\n Path: #{path}\nLine: #{line}"
+ f.grep(/\blet\W+[a-zA-Z0-9_.]+\W*=/) do |line| # rubocop:disable Lint/UnreachableLoop -- false positive
+ raise "Variable declared with 'let' instead of 'var' inside\n Path: #{path}\nLine: #{line}"
end
end
end
end
-
end
diff --git a/spec/beef/core/ruby/hash_spec.rb b/spec/beef/core/ruby/hash_spec.rb
new file mode 100644
index 000000000..58805941b
--- /dev/null
+++ b/spec/beef/core/ruby/hash_spec.rb
@@ -0,0 +1,99 @@
+#
+# 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 'Hash#deep_merge' do
+ it 'merges two simple hashes' do
+ hash1 = { a: 1, b: 2 }
+ hash2 = { c: 3, d: 4 }
+ result = hash1.deep_merge(hash2)
+
+ expect(result).to eq({ a: 1, b: 2, c: 3, d: 4 })
+ end
+
+ it 'overwrites duplicate keys with values from calling hash' do
+ hash1 = { a: 1, b: 2 }
+ hash2 = { b: 3, c: 4 }
+ result = hash1.deep_merge(hash2)
+
+ expect(result[:a]).to eq(1)
+ expect(result[:b]).to eq(3) # hash2 value overwrites
+ expect(result[:c]).to eq(4)
+ end
+
+ it 'recursively merges nested hashes' do
+ hash1 = { a: { b: 1, c: 2 }, d: 3 }
+ hash2 = { a: { c: 4, e: 5 }, f: 6 }
+ result = hash1.deep_merge(hash2)
+
+ expect(result[:a][:b]).to eq(1)
+ expect(result[:a][:c]).to eq(4) # hash2 value overwrites
+ expect(result[:a][:e]).to eq(5)
+ expect(result[:d]).to eq(3)
+ expect(result[:f]).to eq(6)
+ end
+
+ it 'handles deeply nested hashes' do
+ hash1 = { a: { b: { c: 1 } } }
+ hash2 = { a: { b: { d: 2 } } }
+ result = hash1.deep_merge(hash2)
+
+ expect(result[:a][:b][:c]).to eq(1)
+ expect(result[:a][:b][:d]).to eq(2)
+ end
+
+ it 'does not modify the original hash' do
+ hash1 = { a: 1 }
+ hash2 = { b: 2 }
+ original_hash1 = hash1.dup
+
+ hash1.deep_merge(hash2)
+
+ expect(hash1).to eq(original_hash1)
+ end
+
+ it 'handles empty hashes' do
+ hash1 = {}
+ hash2 = { a: 1 }
+ result = hash1.deep_merge(hash2)
+
+ expect(result).to eq({ a: 1 })
+ end
+
+ it 'handles merging with empty hash' do
+ hash1 = { a: 1 }
+ hash2 = {}
+ result = hash1.deep_merge(hash2)
+
+ expect(result).to eq({ a: 1 })
+ end
+
+ it 'handles non-hash values in nested structure' do
+ hash1 = { a: { b: 1 } }
+ hash2 = { a: 2 } # a is not a hash in hash2
+ result = hash1.deep_merge(hash2)
+
+ expect(result[:a]).to eq(2) # Should overwrite with non-hash value
+ end
+
+ it 'handles nil values in source hash' do
+ hash1 = { a: nil, b: 1 }
+ hash2 = { a: 2, c: 3 }
+ result = hash1.deep_merge(hash2)
+
+ expect(result[:a]).to eq(2) # Should overwrite nil
+ expect(result[:b]).to eq(1)
+ expect(result[:c]).to eq(3)
+ end
+
+ it 'handles nil values when merging nested hashes' do
+ hash1 = { a: nil }
+ hash2 = { a: { b: 1 } }
+ result = hash1.deep_merge(hash2)
+
+ expect(result[:a]).to eq({ b: 1 }) # Should overwrite nil with hash
+ end
+end
diff --git a/spec/beef/core/ruby/module_spec.rb b/spec/beef/core/ruby/module_spec.rb
new file mode 100644
index 000000000..bc9b44074
--- /dev/null
+++ b/spec/beef/core/ruby/module_spec.rb
@@ -0,0 +1,94 @@
+#
+# 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 'Module extensions' do
+ # Create a test module to use in tests
+ let(:test_module) do
+ Module.new do
+ def test_method
+ 'test'
+ end
+ end
+ end
+
+ describe '#included_in_classes' do
+ it 'returns an array' do
+ result = test_module.included_in_classes
+ expect(result).to be_an(Array)
+ end
+
+ it 'finds classes that include the module' do
+ mod = test_module
+ test_class = Class.new do
+ include mod
+ end
+
+ # Force class to be created
+ test_class.new
+
+ included_classes = mod.included_in_classes
+ expect(included_classes.map(&:to_s)).to include(test_class.to_s)
+ end
+
+ it 'returns unique classes only' do
+ mod = test_module
+ test_class = Class.new do
+ include mod
+ end
+
+ # Force class to be created multiple times
+ test_class.new
+ test_class.new
+
+ included_classes = mod.included_in_classes
+ unique_class_names = included_classes.map(&:to_s)
+ expect(unique_class_names.count(test_class.to_s)).to eq(1)
+ end
+
+ it 'returns empty array when module is not included anywhere' do
+ isolated_module = Module.new
+ result = isolated_module.included_in_classes
+ expect(result).to be_an(Array)
+ # May or may not be empty depending on what's loaded, but should be an array
+ end
+ end
+
+ describe '#included_in_modules' do
+ it 'returns an array' do
+ result = test_module.included_in_modules
+ expect(result).to be_an(Array)
+ end
+
+ it 'finds modules that include the module' do
+ mod = test_module
+ including_module = Module.new do
+ include mod
+ end
+
+ # Force module to be created
+ Class.new { include including_module }
+
+ included_modules = mod.included_in_modules
+ expect(included_modules.map(&:to_s)).to include(including_module.to_s)
+ end
+
+ it 'returns unique modules only' do
+ mod = test_module
+ including_module = Module.new do
+ include mod
+ end
+
+ # Force module to be created multiple times
+ Class.new { include including_module }
+ Class.new { include including_module }
+
+ included_modules = mod.included_in_modules
+ unique_module_names = included_modules.map(&:to_s)
+ expect(unique_module_names.count(including_module.to_s)).to eq(1)
+ end
+ end
+end
diff --git a/spec/beef/core/ruby/print_spec.rb b/spec/beef/core/ruby/print_spec.rb
new file mode 100644
index 000000000..b268dd9fa
--- /dev/null
+++ b/spec/beef/core/ruby/print_spec.rb
@@ -0,0 +1,191 @@
+#
+# 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 'Print functions' do
+ let(:logger) { BeEF.logger }
+ let(:test_message) { 'test message' }
+
+ before(:each) do
+ # Mock stdout to avoid cluttering test output
+ allow($stdout).to receive(:puts)
+ allow($stdout).to receive(:print)
+
+ # Mock logger methods
+ allow(logger).to receive(:error)
+ allow(logger).to receive(:info)
+ allow(logger).to receive(:warn)
+ allow(logger).to receive(:debug)
+ end
+
+ describe '#print_error' do
+ it 'calls logger.error with the message' do
+ expect(logger).to receive(:error).with(test_message)
+ print_error(test_message)
+ end
+
+ it 'outputs to stdout with timestamp and error prefix' do
+ expect($stdout).to receive(:puts).with(match(/\[!\] #{test_message}/))
+ print_error(test_message)
+ end
+
+ it 'converts non-string arguments to string' do
+ expect(logger).to receive(:error).with('123')
+ print_error(123)
+ end
+ end
+
+ describe '#print_info' do
+ it 'calls logger.info with the message' do
+ expect(logger).to receive(:info).with(test_message)
+ print_info(test_message)
+ end
+
+ it 'outputs to stdout with timestamp and info prefix' do
+ expect($stdout).to receive(:puts).with(match(/\[\*\] #{test_message}/))
+ print_info(test_message)
+ end
+ end
+
+ describe '#print_status' do
+ it 'calls print_info' do
+ expect(logger).to receive(:info).with(test_message)
+ print_status(test_message)
+ end
+ end
+
+ describe '#print_warning' do
+ it 'calls logger.warn with the message' do
+ expect(logger).to receive(:warn).with(test_message)
+ print_warning(test_message)
+ end
+
+ it 'outputs to stdout with timestamp and warning prefix' do
+ expect($stdout).to receive(:puts).with(match(/\[!\] #{test_message}/))
+ print_warning(test_message)
+ end
+ end
+
+ describe '#print_debug' do
+ let(:config) { BeEF::Core::Configuration.instance }
+
+ context 'when debug is enabled' do
+ before do
+ allow(config).to receive(:get).with('beef.debug').and_return(true)
+ allow(BeEF::Core::Console::CommandLine).to receive(:parse).and_return({})
+ end
+
+ it 'calls logger.debug with the message' do
+ expect(logger).to receive(:debug).with(test_message)
+ print_debug(test_message)
+ end
+
+ it 'outputs to stdout with timestamp and debug prefix' do
+ expect($stdout).to receive(:puts).with(match(/\[>\] #{test_message}/))
+ print_debug(test_message)
+ end
+ end
+
+ context 'when verbose flag is set' do
+ before do
+ allow(config).to receive(:get).with('beef.debug').and_return(false)
+ allow(BeEF::Core::Console::CommandLine).to receive(:parse).and_return({ verbose: true })
+ end
+
+ it 'calls logger.debug with the message' do
+ expect(logger).to receive(:debug).with(test_message)
+ print_debug(test_message)
+ end
+ end
+
+ context 'when debug is disabled and verbose is not set' do
+ before do
+ allow(config).to receive(:get).with('beef.debug').and_return(false)
+ allow(BeEF::Core::Console::CommandLine).to receive(:parse).and_return({})
+ end
+
+ it 'does not call logger.debug' do
+ expect(logger).not_to receive(:debug)
+ print_debug(test_message)
+ end
+
+ it 'does not output to stdout' do
+ expect($stdout).not_to receive(:puts)
+ print_debug(test_message)
+ end
+ end
+ end
+
+ describe '#print_success' do
+ it 'calls logger.info with the message' do
+ expect(logger).to receive(:info).with(test_message)
+ print_success(test_message)
+ end
+
+ it 'outputs to stdout with timestamp and success prefix' do
+ expect($stdout).to receive(:puts).with(match(/\[\+\] #{test_message}/))
+ print_success(test_message)
+ end
+ end
+
+ describe '#print_good' do
+ it 'calls print_success' do
+ expect(logger).to receive(:info).with(test_message)
+ print_good(test_message)
+ end
+ end
+
+ describe '#print_more' do
+ context 'with string input' do
+ it 'splits string by newlines and formats each line' do
+ multi_line = "line1\nline2\nline3"
+ expect($stdout).to receive(:puts).with(match(/line1/))
+ expect($stdout).to receive(:puts).with(match(/line2/))
+ expect($stdout).to receive(:puts).with(match(/\|_ line3/)) # Last line has "|_"
+ expect(logger).to receive(:info).exactly(3).times
+ print_more(multi_line)
+ end
+
+ it 'formats last line with |_ prefix' do
+ single_line = 'single line'
+ expect($stdout).to receive(:puts).with(match(/\|_ single line/))
+ expect(logger).to receive(:info).with(match(/\|_ single line/))
+ print_more(single_line)
+ end
+ end
+
+ context 'with array input' do
+ it 'formats each array element as a line' do
+ lines_array = %w[line1 line2 line3]
+ expect($stdout).to receive(:puts).exactly(3).times
+ expect(logger).to receive(:info).exactly(3).times
+ print_more(lines_array)
+ end
+
+ it 'formats last array element with |_ prefix' do
+ lines_array = %w[line1 line2]
+ expect($stdout).to receive(:puts).with(match(/\| line1/))
+ expect($stdout).to receive(:puts).with(match(/\|_ line2/))
+ print_more(lines_array)
+ end
+ end
+ end
+
+ describe '#print_over' do
+ it 'calls logger.info with the message' do
+ expect(logger).to receive(:info).with(test_message)
+ print_over(test_message)
+ end
+
+ it 'outputs formatted message to stdout' do
+ # print is a private Kernel method, hard to stub directly
+ # We verify the function executes and calls logger
+ # The actual output includes ANSI color codes and carriage return
+ expect { print_over(test_message) }.not_to raise_error
+ expect(logger).to have_received(:info).with(test_message)
+ end
+ end
+end
diff --git a/spec/beef/core/ruby/security_spec.rb b/spec/beef/core/ruby/security_spec.rb
new file mode 100644
index 000000000..ba257bfcd
--- /dev/null
+++ b/spec/beef/core/ruby/security_spec.rb
@@ -0,0 +1,28 @@
+#
+# 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 'Security method overrides' do
+ it 'overrides exec method' do
+ # The exec method should be overridden to prevent usage
+ # We can't easily test the exit behavior without forking
+ # so we just check that the method is overridden
+ expect(method(:exec).source_location).not_to be_nil
+ expect(method(:exec).source_location[0]).to include('core/ruby/security.rb')
+ end
+
+ it 'overrides system method' do
+ # The system method should be overridden
+ expect(method(:system).source_location).not_to be_nil
+ expect(method(:system).source_location[0]).to include('core/ruby/security.rb')
+ end
+
+ it 'overrides Kernel.system method' do
+ # Kernel.system should be overridden
+ expect(Kernel.method(:system).source_location).not_to be_nil
+ expect(Kernel.method(:system).source_location[0]).to include('core/ruby/security.rb')
+ end
+end
diff --git a/spec/beef/core/ruby/string_spec.rb b/spec/beef/core/ruby/string_spec.rb
new file mode 100644
index 000000000..c361f75eb
--- /dev/null
+++ b/spec/beef/core/ruby/string_spec.rb
@@ -0,0 +1,27 @@
+#
+# 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 'String colorization' do
+ it 'includes Term::ANSIColor module' do
+ expect(String.included_modules).to include(Term::ANSIColor)
+ end
+
+ it 'can use color methods on strings' do
+ string = 'test'
+ expect(string.respond_to?(:red)).to be(true)
+ expect(string.respond_to?(:green)).to be(true)
+ expect(string.respond_to?(:blue)).to be(true)
+ end
+
+ it 'applies color methods correctly' do
+ string = 'hello'
+ colored = string.red
+
+ expect(colored).to be_a(String)
+ expect(colored).not_to eq(string) # should now be: "\e[31mhello\e[0m" (red colored hello)
+ end
+end
diff --git a/spec/beef/core/settings_spec.rb b/spec/beef/core/settings_spec.rb
new file mode 100644
index 000000000..d554ef1c1
--- /dev/null
+++ b/spec/beef/core/settings_spec.rb
@@ -0,0 +1,32 @@
+#
+# 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::Settings' do
+ describe '.extension_exists?' do
+ it 'returns true for existing extensions and false for non-existing ones' do
+ # Test with a known extension if available
+ expect(BeEF::Settings.extension_exists?('AdminUI')).to be(true) if BeEF::Extension.const_defined?('AdminUI')
+
+ expect(BeEF::Settings.extension_exists?('NonExistentExtension')).to be(false)
+ end
+
+ it 'raises errors for invalid inputs' do
+ expect { BeEF::Settings.extension_exists?(nil) }.to raise_error(TypeError)
+ expect { BeEF::Settings.extension_exists?('') }.to raise_error(NameError)
+ end
+ end
+
+ describe '.console?' do
+ it 'delegates to extension_exists? with Console' do
+ allow(BeEF::Settings).to receive(:extension_exists?).with('Console').and_return(true)
+ expect(BeEF::Settings.console?).to be(true)
+
+ allow(BeEF::Settings).to receive(:extension_exists?).with('Console').and_return(false)
+ expect(BeEF::Settings.console?).to be(false)
+ end
+ end
+end
diff --git a/spec/beef/filesystem_checks_spec.rb b/spec/beef/filesystem_checks_spec.rb
index 75248bea7..9454400a7 100644
--- a/spec/beef/filesystem_checks_spec.rb
+++ b/spec/beef/filesystem_checks_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF Filesystem' do
def file_test(file)
expect(File.file?(file)).to be(true)
diff --git a/spec/beef/security_checks_spec.rb b/spec/beef/security_checks_spec.rb
index c05832311..181cfeb07 100644
--- a/spec/beef/security_checks_spec.rb
+++ b/spec/beef/security_checks_spec.rb
@@ -1,3 +1,9 @@
+#
+# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net
+# Browser Exploitation Framework (BeEF) - https://beefproject.com
+# See the file 'doc/COPYING' for copying permission
+#
+
RSpec.describe 'BeEF Security Checks' do
it 'dangerous eval usage' do
Dir['**/*.rb'].each do |path|