From 82c94180d2cf09a5e5d40145b69012baf70d23f0 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Thu, 25 Jun 2026 03:22:25 +0300 Subject: [PATCH 01/27] command dispatcher arch design --- docs/architecture/01-command-dispatcher.md | 154 +++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 docs/architecture/01-command-dispatcher.md diff --git a/docs/architecture/01-command-dispatcher.md b/docs/architecture/01-command-dispatcher.md new file mode 100644 index 0000000000000..32075f0fa94ad --- /dev/null +++ b/docs/architecture/01-command-dispatcher.md @@ -0,0 +1,154 @@ +# Command Dispatcher Architecture + +## What This Document Is +This defines how the `test_env` command will be added to msfconsole. + +## How Commands Work in Metasploit (From Source Code I Read) + +### 1. Plugin Registration +From `plugins/sample.rb`, I saw: +- Plugin inherits from `Msf::Plugin` +- Plugin has an inner `ConsoleCommandDispatcher` class +- Dispatcher `include Msf::Ui::Console::CommandDispatcher` +- `commands` method returns a hash: `{ 'command_name' => 'description' }` +- `initialize` calls `add_console_dispatcher(ConsoleCommandDispatcher)` +- `cleanup` calls `remove_console_dispatcher('Name')` + +### 2. Command Routing +From `lib/rex/ui/text/dispatcher_shell.rb` line 576, I saw: +```ruby +def run_command(dispatcher, method, arguments) +``` +When I type `test_env build`, this happens: +1. Shell parses the line into `["test_env", "build"]` +2. Shell finds my plugin's dispatcher in `dispatcher_stack` +3. Shell calls `run_command(my_dispatcher, "test_env", ["build"])` +4. Which calls `my_dispatcher.cmd_test_env("build")` + +### 3. Multi-Command Pattern +From `lib/msf/ui/console/command_dispatcher/jobs.rb`, I saw: +- One dispatcher can handle multiple commands via `commands` hash +- `cmd_jobs(*args)` uses `args.shift` to get the subcommand +- `cmd_rename_job_tabs` provides tab completion +- `cmd_jobs_help` prints usage information + +## My Design: test_env Command Dispatcher + +### Class Structure +``` +Msf::Plugin +└── Msf::Plugin::VulnEnv + └── Msf::Plugin::VulnEnv::ConsoleCommandDispatcher + └── (includes Msf::Ui::Console::CommandDispatcher) +``` + +### Commands Hash +| Command | Description | +|---------|-------------| +| `test_env` | Manage vulnerable test environments | + +### Subcommands (Handled Inside cmd_test_env) +| Subcommand | Handler Method | What It Does | +|-----------|---------------|--------------| +| `build` | `cmd_test_env_build(args)` | Build and launch environment for active module | +| `list` | `cmd_test_env_list(args)` | Show all tracked environments | +| `stop ` | `cmd_test_env_stop(args)` | Stop a running container | +| `start ` | `cmd_test_env_start(args)` | Restart a stopped container | +| `remove ` | `cmd_test_env_remove(args)` | Tear down a container | +| `remove-all` | `cmd_test_env_remove_all(args)` | Tear down all containers | +| `exec ` | `cmd_test_env_exec(args)` | Run exploit against environment | +| `help` | `cmd_test_env_help` | Show usage | + +## Sample code for solid clarification +### Argument Parsing Logic +```ruby +def cmd_test_env(*args) + # If no args or help requested, show help + if args.empty? || args.first == '-h' || args.first == '--help' + cmd_test_env_help + return + end + + # First argument is the subcommand + subcommand = args.shift + + # Route to appropriate handler + case subcommand + when 'build' then cmd_test_env_build(args) + when 'list' then cmd_test_env_list(args) + when 'stop' then cmd_test_env_stop(args) + when 'start' then cmd_test_env_start(args) + when 'remove' then cmd_test_env_remove(args) + when 'remove-all' then cmd_test_env_remove_all(args) + when 'exec' then cmd_test_env_exec(args) + when 'help' then cmd_test_env_help + else + print_error("Unknown subcommand: #{subcommand}") + cmd_test_env_help + end +end +``` + +### Tab Completion +```ruby +def cmd_test_env_tabs(str, words) + # If only "test_env" has been typed, suggest subcommands + if words.length == 1 + return %w[build list stop start remove remove-all exec help] + end + + # If subcommand is stop/start/remove/exec, suggest environment IDs + if words.length == 2 + case words[0] + when 'stop', 'start', 'remove', 'exec' + # TODO: Return IDs from registry (Week 6) + return [] + end + end + + [] +end +``` + +### Error Handling Pattern +Every subcommand follows this pattern: +```ruby +def cmd_test_env_build(args) + begin + # 1. Validate preconditions + mod = driver.active_module + raise "No active module. Use 'use ' first." unless mod + + # 2. Execute logic + # ... (implementation in later weeks) + + # 3. Report success + print_good("Environment built successfully") + + rescue => e + # 4. Report error + print_error("test_env build failed: #{e.message}") + elog("test_env build error: #{e.class} - #{e.message}") + elog(e.backtrace.join("\n")) + end +end +``` + +## Integration Points + +| What I Need | Where It Comes From | How I Access It | +|-------------|-------------------|---------------| +| Framework instance | `Msf::Plugin#initialize` | `framework` (instance variable) | +| Active module | `Msf::Ui::Console::Driver#active_module` | `driver.active_module` | +| Database | `framework.db.active` | Check before DB operations | +| Console output | `Msf::Ui::Console::CommandDispatcher` | `print_status`, `print_error`, `print_good` | + +## Decisions Made + +| Decision | Choice | Reason | +|----------|--------|--------| +| Single command or multiple? | Single `test_env` with subcommands | Matches `jobs` pattern; cleaner namespace | +| How to parse subcommands? | `case` statement on `args.shift` | Same as `cmd_jobs` | +| Tab completion? | `cmd_test_env_tabs` method | For good UX | +| Error handling? | `begin/rescue` with `print_error` | Consistent with framework style | + From bb41989cc9c5a8935bef11c14c4d390f44d97e46 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Thu, 25 Jun 2026 17:00:47 +0300 Subject: [PATCH 02/27] verify module metadata access --- docs/architecture/02-module-metadata.md | 209 ++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 docs/architecture/02-module-metadata.md diff --git a/docs/architecture/02-module-metadata.md b/docs/architecture/02-module-metadata.md new file mode 100644 index 0000000000000..bcf8ed09fb41c --- /dev/null +++ b/docs/architecture/02-module-metadata.md @@ -0,0 +1,209 @@ +# Module Metadata Integration + +## What I Verified + +I wrote and ran `test_final.rb` to verify how to access module metadata. Here is the test script I used: + +```ruby +#!/usr/bin/env ruby + +$LOAD_PATH.unshift(File.expand_path('lib', __dir__)) +require 'msfenv' +require 'msf/core' + +framework = Msf::Simple::Framework.create +mod = framework.modules.create('exploit/multi/http/jenkins_script_console') + +puts "Module: #{mod.fullname}" +puts "" + +# Test: Can we read module_info via send? +puts "=== Reading module_info via send ===" +info = mod.send(:module_info) +puts "Type: #{info.class}" +puts "Keys count: #{info.keys.length}" +puts "Has Name? #{info.key?('Name')}" +puts "Name: #{info['Name']}" +puts "" + +# Test: Can we add a custom key? +puts "=== Adding custom key ===" +info['VulnEnv'] = { + 'definition' => 'jenkins', + 'default_version' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' } +} + +puts "Added VulnEnv" +puts "Has VulnEnv? #{info.key?('VulnEnv')}" +puts "VulnEnv: #{info['VulnEnv'].inspect}" +puts "" + +# Test: Can we read it back? +puts "=== Reading back ===" +info2 = mod.send(:module_info) +puts "Same object? #{info.equal?(info2)}" +puts "Has VulnEnv? #{info2.key?('VulnEnv')}" +puts "VulnEnv: #{info2['VulnEnv'].inspect}" +``` + +### Output I Got + +``` +Module: exploit/multi/http/jenkins_script_console + +=== Reading module_info via send === +Type: Hash +Keys count: 18 +Has Name? true +Name: Jenkins-CI Script-Console Java Execution + +=== Adding custom key === +Added VulnEnv +Has VulnEnv? true +VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} + +=== Reading back === +Same object? true +Has VulnEnv? true +VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} +``` + +### What This Proves + +| Test | Result | +|------|--------| +| `mod.send(:module_info)` returns a Hash | ✅ Yes | +| The Hash contains standard keys like `Name` | ✅ Yes | +| I can write a custom key `VulnEnv` to it | ✅ Yes | +| The custom key persists when read back | ✅ Yes | +| It's the same object (not a copy) | ✅ Yes | + +## Source Code Evidence + +From `lib/msf/core/module/module_info.rb` line 69: + +```ruby +protected + +# @!attribute module_info +attr_accessor :module_info +``` + +`module_info` is a **protected** `attr_accessor`. That is why: +- `mod.module_info` raises `NoMethodError` (protected method) +- `mod.send(:module_info)` works (bypasses access control) + +The framework itself uses `module_info` internally in `lib/msf/core/module.rb`: + +```ruby +# Line 116 +self.module_info = info + +# Line 129-132 +self.author = Msf::Author.transform(merge_module_info_with_target_info(module_info, 'Author')) +self.arch = Rex::Transformer.transform(merge_module_info_with_target_info(module_info, 'Arch'), Array, [ String ], 'Arch') +``` + +And in `lib/msf/core/module/module_info.rb`: + +```ruby +# Line 41 +def name + module_info['Name'] +end +``` + +## The Correct Way to Access Module Metadata + +```ruby +# In plugin's command dispatcher: +def cmd_test_env_build(args) + # 1. Get active module from driver + mod = driver.active_module + raise "No active module. Use 'use ' first." unless mod + + # 2. Access module_info via send (protected accessor) + info = mod.send(:module_info) + + # 3. Read VulnEnv configuration + vuln_env = info['VulnEnv'] + raise "Module has no VulnEnv config" unless vuln_env + + # 4. Extract values + definition_name = vuln_env['definition'] # 'jenkins' + version = vuln_env['default_version'] # '2.361' + port_mapping = vuln_env['port_mapping'] # {8080 => 'RPORT'} + + # 5. Load YAML definition file + yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{definition_name}.yml") +end +``` + +## Why send(:module_info) Is Acceptable + +- `module_info` is **protected**, not private — meant for subclass/extension access +- Metasploit plugins are **framework extensions**, not external code +- The framework itself accesses `module_info` directly in the `ModuleInfo` mixin +- This is a **standard Ruby pattern** for working with protected framework internals + +## Alternative: Encapsulate in Helper Method (Is it better to do or not?) + +If It's not preferable not to use `send` directly everywhere: + +```ruby +class Plugin::VulnEnv < Msf::Plugin + class ConsoleCommandDispatcher + # Encapsulate the send call for clarity + def get_module_vuln_env(mod) + mod.send(:module_info)['VulnEnv'] + end + + def cmd_test_env_build(args) + mod = driver.active_module + vuln_env = get_module_vuln_env(mod) + # ... + end + end +end +``` + +## VulnEnv Schema + +```ruby +'VulnEnv' => { + 'definition' => String, # e.g., 'jenkins' → data/vuln_envs/jenkins.yml + 'default_version' => String, # e.g., '2.361' + 'port_mapping' => Hash, # { container_port => 'RPORT' } + 'datastore_overrides' => Hash # optional: { 'TARGETURI' => '/script' } +} +``` + +## Resolution Flow + +``` +test_env build called + ↓ +driver.active_module → Msf::Module instance + ↓ +mod.send(:module_info)['VulnEnv'] → Hash or nil + ↓ +if nil: print_error("Module has no VulnEnv configuration") + ↓ +if present: + definition = vuln_env['definition'] # 'jenkins' + yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{definition}.yml") + definition_data = YAML.load_file(yaml_path) + version = vuln_env['default_version'] # '2.361' + env_config = definition_data['versions'][version] + shared_config = definition_data['shared'] +``` + +## Error Cases + +| Condition | Error Message | +|-----------|--------------| +| No active module | "No active module. Use 'use ' first." | +| Module has no VulnEnv | "Module does not define a vulnerable environment configuration." | +| Definition file not found | "Environment definition not found: data/vuln_envs/{name}.yml" | +| Version not found in definition | "Version '{version}' not defined for '{name}'" | From 1666a9980168e84d632edc36aa2c73d67781811d Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Thu, 25 Jun 2026 18:40:03 +0300 Subject: [PATCH 03/27] design in-memory registry with phase 2 database plan --- docs/architecture/03-database-schema.md | 287 ++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 docs/architecture/03-database-schema.md diff --git a/docs/architecture/03-database-schema.md b/docs/architecture/03-database-schema.md new file mode 100644 index 0000000000000..af3a02693260f --- /dev/null +++ b/docs/architecture/03-database-schema.md @@ -0,0 +1,287 @@ +# Database Schema & Persistence + +## What I Learned From Metasploit Source + +I investigated the database architecture and found: + +### Migration System +- `db/migrate/` exists but is **empty** in the framework repo +- Migrations are gathered from **Rails engines** via `gather_engine_migration_paths` +- `lib/msf/core/db_manager/migration.rb` uses `ActiveRecord::MigrationContext` +- `schema.rb` is auto-generated, not edited directly + +### Key Code From `lib/msf/core/db_manager/migration.rb` + +```ruby +def gather_engine_migration_paths + paths = ActiveRecord::Migrator.migrations_paths + ::Rails::Engine.subclasses.map(&:instance).each do |engine| + migrations_paths = engine.paths['db/migrate'].existent_directories + migrations_paths.each do |migrations_path| + unless paths.include? migrations_path + paths << migrations_path + end + end + end + paths +end +``` + +### Database Configuration +- `config/database.yml` does not exist in the framework +- Database config is passed via `DatabaseYAML` option +- `framework.db.active` checks if database is connected + +## Phase 1: Plugin-Only (Weeks 1-6) — In-Memory Registry + +**Decision:** For the initial plugin implementation, use **in-memory storage only**. +No database migrations, no schema changes. + +### Why In-Memory First? +1. No framework modifications required +2. Plugin loads/unloads cleanly +3. Container labels provide cross-session identification +4. Database integration is Phase 2 (Week 6) + +### In-Memory Registry Design + +```ruby +class BuiltEnvironmentRegistry + attr_reader :environments, :framework + + def initialize(framework) + @framework = framework + @environments = {} # local_id => Hash + @next_id = 1 + end + + def register(container_id:, module_fullname:, rhost:, rport:, + version: nil, runtime: 'docker', image_ref:, + exploit_command:, datastore: {}) + id = @next_id + @next_id += 1 + + @environments[id] = { + local_id: id, + container_id: container_id, + module_fullname: module_fullname, + env_version: version, + rhost: rhost, + rport: rport, + runtime: runtime, + image_ref: image_ref, + status: 'running', + exploit_command: exploit_command, + datastore: datastore, + created_at: Time.now, + started_at: Time.now + } + + id + end + + def get(id) + @environments[id] + end + + def list + @environments.values.sort_by { |e| e[:local_id] } + end + + def update_status(id, status) + return unless @environments[id] + @environments[id][:status] = status + @environments[id][:updated_at] = Time.now + @environments[id][:stopped_at] = Time.now if status == 'stopped' + @environments[id][:started_at] = Time.now if status == 'running' + end + + def remove(id) + return unless @environments[id] + @environments[id][:status] = 'removed' + @environments[id][:removed_at] = Time.now + @environments.delete(id) + end + + def remove_all + @environments.each_value do |env| + env[:status] = 'removed' + env[:removed_at] = Time.now + end + @environments.clear + @next_id = 1 + end + + def find_by_container(container_id) + @environments.values.find { |e| e[:container_id] == container_id } + end + + def find_by_module(module_fullname) + @environments.values.select { |e| e[:module_fullname] == module_fullname } + end + + def used_ports + @environments.values.map { |e| e[:rport] } + end + + def running? + @environments.values.any? { |e| e[:status] == 'running' } + end +end +``` + +### Container Labels (Cross-Session Identification) + +Since in-memory data is lost on msfconsole restart, use **OCI container labels** +to identify and reconstruct environments: + +```bash +docker run -d \ + --label "msf.vulnenv.instance_id=msf-$(hostname)-$$" \ + --label "msf.vulnenv.module=exploit/multi/http/jenkins_script_console" \ + --label "msf.vulnenv.version=2.361" \ + --label "msf.vulnenv.env_id=1" \ + --label "msf.vulnenv.created_at=2024-06-25T17:37:00Z" \ + vulnhub/jenkins:2.361 +``` + +**Label Schema:** +| Label | Value | Purpose | +|-------|-------|---------| +| `msf.vulnenv.instance_id` | `msf-{hostname}-{pid}` | Identify msfconsole instance | +| `msf.vulnenv.module` | Module fullname | Link to exploit module | +| `msf.vulnenv.version` | Environment version | Track which version | +| `msf.vulnenv.env_id` | Internal registry ID | Cross-reference | +| `msf.vulnenv.created_at` | ISO8601 timestamp | Audit trail | +| `msf.vulnenv.managed_by` | `test_env` | Identify framework-managed | + +### State Reconstruction From Labels (Future Enhancement) + +```ruby +def reconstruct_from_labels(runtime) + containers = runtime.list(filters: { 'label' => 'msf.vulnenv.managed_by=test_env' }) + containers.each do |container| + labels = container['Labels'] + # Rebuild registry entry from labels + # (Week 6 enhancement) + end +end +``` + +## Phase 2: Database Integration (Week 6+) + +When adding PostgreSQL persistence: + +### Migration File +```ruby +# db/migrate/20240624000001_create_vuln_environments.rb +class CreateVulnEnvironments < ActiveRecord::Migration[8.0] + def change + create_table :vuln_environments, id: :serial do |t| + t.string :container_id, null: false + t.string :image_ref, null: false + t.string :module_fullname, null: false + t.string :env_version + t.string :rhost, default: '127.0.0.1' + t.integer :rport, null: false + t.text :datastore + t.string :runtime, default: 'docker', null: false + t.string :msf_instance_id + t.string :status, null: false, default: 'running' + t.text :exploit_command + t.timestamps + t.datetime :started_at + t.datetime :stopped_at + t.datetime :removed_at + end + + add_index :vuln_environments, :module_fullname + add_index :vuln_environments, :status + add_index :vuln_environments, :container_id, unique: true + add_index :vuln_environments, :msf_instance_id + add_index :vuln_environments, [:status, :module_fullname] + end +end +``` + +### ActiveRecord Model +```ruby +class VulnEnvironment < ActiveRecord::Base + self.table_name = 'vuln_environments' + serialize :datastore, JSON + + scope :active, -> { where(status: ['running', 'stopped']) } + scope :running, -> { where(status: 'running') } + scope :by_module, ->(name) { where(module_fullname: name) } + + validates :container_id, presence: true, uniqueness: true + validates :module_fullname, presence: true + validates :rport, presence: true, numericality: { only_integer: true } + validates :status, inclusion: { in: %w[running stopped removed orphaned error] } +end +``` + +### Integration With In-Memory Registry + +```ruby +class BuiltEnvironmentRegistry + def initialize(framework) + @framework = framework + @environments = {} + @next_id = 1 + load_from_database if database_available? + end + + private + + def database_available? + framework.db.active && defined?(VulnEnvironment) + end + + def load_from_database + VulnEnvironment.active.each do |db_env| + @environments[@next_id] = { + local_id: @next_id, + db_id: db_env.id, + container_id: db_env.container_id, + # ... map all fields ... + } + @next_id += 1 + end + end + + def persist_to_database(record) + VulnEnvironment.create!(...) + end +end +``` + +## Reference: sessions Table Pattern + +From `db/schema.rb`: +```ruby +create_table "sessions", id: :serial, force: :cascade do |t| + t.integer "host_id" + t.string "stype" + t.string "via_exploit" # Module association + t.string "via_payload" + t.string "desc" + t.integer "port" + t.string "platform" + t.text "datastore" # Serialized hash + t.datetime "opened_at", precision: nil, null: false + t.datetime "closed_at", precision: nil + t.string "close_reason" + t.integer "local_id" # In-memory mapping + t.datetime "last_seen", precision: nil + t.integer "module_run_id" + t.index ["module_run_id"], name: "index_sessions_on_module_run_id" +end +``` + +My `vuln_environments` table follows this exact pattern: +- `id: :serial` primary key +- `module_fullname` like `via_exploit` +- `datastore` serialized text +- `local_id` equivalent via `env_id` label +- Lifecycle timestamps (`created_at`, `started_at`, `stopped_at`, `removed_at`) From d64786e797da04682c619e2665e25e6e4be858ab Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Thu, 25 Jun 2026 20:56:28 +0300 Subject: [PATCH 04/27] add jenkins environment definition and schema docs --- data/vuln_envs/jenkins.yml | 49 +++++ docs/architecture/04-environment-schema.md | 233 +++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 data/vuln_envs/jenkins.yml create mode 100644 docs/architecture/04-environment-schema.md diff --git a/data/vuln_envs/jenkins.yml b/data/vuln_envs/jenkins.yml new file mode 100644 index 0000000000000..b72e66fc3539a --- /dev/null +++ b/data/vuln_envs/jenkins.yml @@ -0,0 +1,49 @@ +name: jenkins +description: Jenkins CI server with Groovy Script Console enabled + +versions: + "2.361": + image: vulnhub/jenkins:2.361 + build_args: + JENKINS_VERSION: "2.361" + "2.375": + image: vulnhub/jenkins:2.375 + build_args: + JENKINS_VERSION: "2.375" + +shared: + ports: + http: 8080 + + volumes: + jenkins_home: + container_path: /var/jenkins_home + persist: false + + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 + + credentials: + default: + username: admin + password: admin + + datastore_defaults: + TARGETURI: /script + + ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 diff --git a/docs/architecture/04-environment-schema.md b/docs/architecture/04-environment-schema.md new file mode 100644 index 0000000000000..87822fa797053 --- /dev/null +++ b/docs/architecture/04-environment-schema.md @@ -0,0 +1,233 @@ +# Environment Definition YAML Schema + +## What I Verified + +I created `data/vuln_envs/jenkins.yml` and validated it with Ruby: + +```bash +ruby -e " +require 'yaml' +data = YAML.safe_load(File.read('data/vuln_envs/jenkins.yml'), permitted_classes: [Symbol]) +puts 'Name: ' + data['name'] +puts 'Versions: ' + data['versions'].keys.inspect +puts 'Ports: ' + data['shared']['ports'].inspect +puts 'Health check type: ' + data['shared']['health_check']['type'] +" +``` + +Output: +``` +Name: jenkins +Versions: ["2.361", "2.375"] +Ports: {"http"=>8080} +Health check type: http +``` + +## Directory Structure + +``` +data/ + vuln_envs/ + README.md # Schema documentation + jenkins.yml # Jenkins environments (reference implementation) +``` + +## File Location +`data/vuln_envs/{name}.yml` + +The `{name}` must match the `name` field inside the file. + +## Schema + +### Top-Level Keys + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `name` | String | Yes | Machine-friendly identifier (matches filename) | +| `description` | String | Yes | Human-readable description | +| `versions` | Hash | Yes | Map of version strings to configurations | +| `shared` | Hash | Yes | Configuration shared across all versions | + +### versions Section + +Each version is a key-value pair: +- **Key**: Version string (e.g., `"2.361"`) +- **Value**: Hash with version-specific configuration + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `image` | String | Yes | OCI image reference | +| `build_args` | Hash | No | Docker build arguments | + +Example: +```yaml +versions: + "2.361": + image: vulnhub/jenkins:2.361 + build_args: + JENKINS_VERSION: "2.361" +``` + +### shared Section + +#### ports (Required) +```yaml +shared: + ports: + http: 8080 +``` + +#### health_check (Required) +```yaml +shared: + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 +``` + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `type` | String | Yes | `http`, `tcp`, or `command` | +| `path` | String | If type=http | HTTP path to check | +| `expected_status` | Integer | No | Default: 200 | +| `command` | String | If type=command | Command to execute | +| `expected_output` | String | If type=command | Substring to match | +| `interval` | Integer | No | Seconds between checks. Default: 5 | +| `timeout` | Integer | No | Seconds to wait. Default: 2 | +| `retries` | Integer | No | Max attempts. Default: 12 | + +#### credentials (Optional) +```yaml +shared: + credentials: + default: + username: admin + password: admin +``` + +#### datastore_defaults (Optional) +```yaml +shared: + datastore_defaults: + TARGETURI: /script +``` + +#### ci (Optional) +```yaml +shared: + ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` + +## Validation Rules + +1. `name` must match filename (without `.yml`) +2. `versions` must have at least one entry +3. Each version must have an `image` +4. `shared.ports` must have at least one entry +5. `shared.health_check` must have valid `type` +6. If `type` is `http`, `path` is required +7. If `type` is `command`, `command` and `expected_output` are required + +## Loader Implementation (Week 3) + +```ruby +class EnvironmentDefinitionLoader + DEFINITIONS_PATH = File.join(Msf::Config.data_directory, 'vuln_envs') + + def self.load(name) + require 'yaml' + path = File.join(DEFINITIONS_PATH, "#{name}.yml") + raise "Definition not found: #{path}" unless File.exist?(path) + + begin + YAML.safe_load(File.read(path), permitted_classes: [Symbol]) + rescue Psych::SyntaxError => e + raise "Invalid YAML in #{path}: #{e.message}" + end + end + + def self.available_definitions + Dir.glob(File.join(DEFINITIONS_PATH, '*.yml')).map do |f| + File.basename(f, '.yml') + end.sort + end +end +``` + +## Integration With Registry + +Environment definitions are loaded by the plugin and used to: +1. Build/pull container images +2. Map container ports to host ports +3. Configure health checks +4. Set module datastore defaults + +See [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/03-database-schema.md) for registry design. + +## Reference: jenkins.yml + +```yaml +name: jenkins +description: Jenkins CI server with Groovy Script Console enabled + +versions: + "2.361": + image: vulnhub/jenkins:2.361 + build_args: + JENKINS_VERSION: "2.361" + "2.375": + image: vulnhub/jenkins:2.375 + build_args: + JENKINS_VERSION: "2.375" + +shared: + ports: + http: 8080 + + volumes: + jenkins_home: + container_path: /var/jenkins_home + persist: false + + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 + + credentials: + default: + username: admin + password: admin + + datastore_defaults: + TARGETURI: /script + + ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` From 8e1c633d454b7d5ea353c7d3002a343cd01c1639 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Fri, 26 Jun 2026 17:46:40 +0300 Subject: [PATCH 05/27] complete runtime adapter with dynamic port allocation --- docs/architecture/05-runtime-adapter.md | 450 ++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 docs/architecture/05-runtime-adapter.md diff --git a/docs/architecture/05-runtime-adapter.md b/docs/architecture/05-runtime-adapter.md new file mode 100644 index 0000000000000..bc49a2b5f61ff --- /dev/null +++ b/docs/architecture/05-runtime-adapter.md @@ -0,0 +1,450 @@ +Runtime Adapter & Port Allocation + +## What I Verified on My Machine + +### Docker and Podman Availability + +**Both runtimes are installed.** Docker will be primary, Podman fallback. + +### Docker Inspect Test + +I ran a test container (port binding failed due to conflict, but inspect worked): + +```bash +$ docker run -d --name test-nginx --label msf.test=1 -p 127.0.0.1:8080:80 nginx +# Port 8080 was in use — container created but not started +# This proves: FIXED PORTS FAIL, dynamic allocation is required + +$ docker inspect test-nginx > /tmp/docker-inspect.json +``` + +Key fields from `docker inspect` output: + +```json +{ + "Id": "59721d248c4ff4be689ae3fedf8dc947f53d06c657ac0b1418205621a1ee6f44", + "State": { + "Status": "created", + "Running": false + }, + "Config": { + "Labels": { + "msf.test": "1" + } + }, + "HostConfig": { + "PortBindings": { + "80/tcp": [ + { + "HostIp": "127.0.0.1", + "HostPort": "8080" + } + ] + } + } +} +``` + +**Important findings:** +- Labels are stored in `Config.Labels`, not top-level +- `State.Status` shows "created" when container fails to start +- `HostConfig.PortBindings` shows the requested mapping +- **Port 8080 was already in use — fixed ports are unreliable** + +### Port Allocation Test + +I wrote and ran `test_port.rb`: + +```ruby +#!/usr/bin/env ruby + +require 'socket' + +def port_available?(port) + server = TCPServer.new('127.0.0.1', port) + server.close + true +rescue Errno::EADDRINUSE + false +end + +def find_port(preferred = nil, used = []) + if preferred + return preferred if port_available?(preferred) && !used.include?(preferred) + puts "Port #{preferred} unavailable, finding alternative..." + end + + (49152..65535).each do |p| + next if used.include?(p) + return p if port_available?(p) + end + + raise "No available ports" +end + +# Test +puts "Port 8080 available? #{port_available?(8080)}" +puts "Found port: #{find_port}" +puts "Found port (prefer 9999): #{find_port(9999)}" +``` + +Output: +``` +Port 8080 available? true +Found port: 49152 +Found port (prefer 9999): 9999 +``` + +**Port allocation works.** `TCPServer.new('127.0.0.1', port)` correctly tests availability. + +## Critical Design Decision: Dynamic Port Allocation + +From the Docker test failure: +- **Never assume a port is available** +- **Always test before binding** +- **Fallback to ephemeral range (49152-65535)** +- **Inform user when fallback occurs** + +## Runtime Adapter Design + +### Interface + +```ruby +module RuntimeAdapter + def self.detect + return DockerRuntime.new if DockerRuntime.available? + return PodmanRuntime.new if PodmanRuntime.available? + nil + end +end + +class BaseRuntime + def available?; raise NotImplementedError; end + def name; raise NotImplementedError; end + + def pull(image); raise NotImplementedError; end + def run(image:, ports:, labels:, volumes: [], env: {}, name: nil) + raise NotImplementedError + end + def stop(container_id); raise NotImplementedError; end + def start(container_id); raise NotImplementedError; end + def remove(container_id); raise NotImplementedError; end + def inspect(container_id); raise NotImplementedError; end + def exec(container_id, command); raise NotImplementedError; end + def list(filters: {}); raise NotImplementedError; end +end +``` + +### Docker Implementation + +```ruby +class DockerRuntime < BaseRuntime + def available? + system('docker version > /dev/null 2>&1') + end + + def name; 'docker'; end + + def pull(image) + system("docker pull #{image}") + $? == 0 + end + + def run(image:, ports:, labels:, volumes: [], env: {}, name: nil) + cmd = ['docker', 'run', '-d'] + + # Port mappings: -p 127.0.0.1:HOST:CONTAINER + ports.each do |host_port, container_port| + cmd += ['-p', "127.0.0.1:#{host_port}:#{container_port}"] + end + + # Labels: --label key=value + labels.each do |k, v| + cmd += ['--label', "#{k}=#{v}"] + end + + # Volumes: -v HOST:CONTAINER + volumes.each do |host_path, container_path| + cmd += ['-v', "#{host_path}:#{container_path}"] + end + + # Environment: -e KEY=VALUE + env.each do |k, v| + cmd += ['-e', "#{k}=#{v}"] + end + + # Name: --name + cmd += ['--name', name] if name + + cmd << image + + output = `#{cmd.join(' ')} 2>&1` + if $? == 0 + output.strip # container ID + else + raise "Docker run failed: #{output}" + end + end + + def inspect(container_id) + json = `docker inspect #{container_id} 2>/dev/null` + return nil if json.empty? + + data = JSON.parse(json) + data.first + rescue JSON::ParserError + nil + end + + def stop(container_id) + system("docker stop #{container_id} > /dev/null 2>&1") + end + + def start(container_id) + system("docker start #{container_id} > /dev/null 2>&1") + end + + def remove(container_id) + system("docker rm #{container_id} > /dev/null 2>&1") + end + + def exec(container_id, command) + output = `docker exec #{container_id} #{command} 2>&1` + [output, $?.exitstatus] + end + + def list(filters: {}) + cmd = ['docker', 'ps', '-a', '--format', '{{json .}}'] + + filters.each do |k, v| + cmd += ['--filter', "#{k}=#{v}"] + end + + output = `#{cmd.join(' ')} 2>/dev/null` + output.lines.map { |l| JSON.parse(l) } + rescue JSON::ParserError + [] + end +end +``` + +### Podman Implementation + +```ruby +class PodmanRuntime < BaseRuntime + def available? + system('podman version > /dev/null 2>&1') + end + + def name; 'podman'; end + + # Identical to DockerRuntime except 'podman' instead of 'docker' + # All CLI flags are the same for the operations we need + def run(image:, ports:, labels:, volumes: [], env: {}, name: nil) + cmd = ['podman', 'run', '-d'] + + ports.each do |host_port, container_port| + cmd += ['-p', "127.0.0.1:#{host_port}:#{container_port}"] + end + + labels.each do |k, v| + cmd += ['--label', "#{k}=#{v}"] + end + + volumes.each do |host_path, container_path| + cmd += ['-v', "#{host_path}:#{container_path}"] + end + + env.each do |k, v| + cmd += ['-e', "#{k}=#{v}"] + end + + cmd += ['--name', name] if name + cmd << image + + output = `#{cmd.join(' ')} 2>&1` + if $? == 0 + output.strip + else + raise "Podman run failed: #{output}" + end + end + + # inspect, stop, start, remove, exec, list identical to DockerRuntime + # with 'podman' instead of 'docker' +end +``` + +## Port Allocation + +```ruby +class PortAllocator + EPHEMERAL_START = 49152 + EPHEMERAL_END = 65535 + + def initialize(used_ports = []) + @used_ports = Set.new(used_ports) + end + + def allocate(preferred = nil) + # 1. Try user-requested port first + if preferred && available?(preferred) + @used_ports.add(preferred) + return preferred + end + + # 2. Fall back to ephemeral range + (EPHEMERAL_START..EPHEMERAL_END).each do |port| + next if @used_ports.include?(port) + if available?(port) + @used_ports.add(port) + return port + end + end + + raise "No available ports in range #{EPHEMERAL_START}-#{EPHEMERAL_END}" + end + + def release(port) + @used_ports.delete(port) + end + + private + + def available?(port) + return false if @used_ports.include?(port) + + server = TCPServer.new('127.0.0.1', port) + server.close + true + rescue Errno::EADDRINUSE + false + end +end +``` + +## Container Label Schema + +All containers created by test_env receive these labels: + +| Label | Value | Purpose | +|-------|-------|---------| +| `msf.vulnenv.instance_id` | `msf-{hostname}-{pid}` | Isolate msfconsole instances | +| `msf.vulnenv.module` | Module fullname | Link to exploit module | +| `msf.vulnenv.version` | Environment version | Track which version | +| `msf.vulnenv.env_id` | Internal registry ID | Cross-reference | +| `msf.vulnenv.created_at` | ISO8601 timestamp | Audit trail | +| `msf.vulnenv.managed_by` | `test_env` | Identify framework-managed | + +## Docker vs Podman Differences + +| Feature | Docker | Podman | Impact on Design | +|---------|--------|--------|----------------| +| Daemon | Required (`dockerd`) | Daemonless | Podman simpler for rootless | +| Rootless | Complex setup | Default | Podman more secure | +| CLI syntax | `docker ...` | `podman ...` | Simple binary substitution | +| Networking | Bridge by default | `slirp4netns` for rootless | Slight performance difference | +| Image storage | Central (`/var/lib/docker`) | Per-user (`~/.local/share/containers`) | Not shared between users | + +## Auto-Detection Strategy + +```ruby +def self.detect + return DockerRuntime.new if DockerRuntime.available? + return PodmanRuntime.new if PodmanRuntime.available? + nil +end +``` + +## How test_env build Handles Port Conflicts + +### Problem + +From my test: +```bash +docker run -d -p 127.0.0.1:8080:80 nginx +# Error: ports are not available: exposing port TCP 127.0.0.1:8080 +``` + +Port 8080 was already in use. Fixed ports fail. + +### Solution: Dynamic Port Allocation with Automatic Fallback + +The `PortAllocator` class above handles this by: +1. Testing if preferred port is available via `TCPServer.new` +2. If not, scanning ephemeral range for available port +3. Tracking used ports to avoid duplicates + +### Integration in test_env build + +```ruby +def cmd_test_env_build(args) + # ... get module, VulnEnv config, definition ... + + shared = definition['shared'] + port_mapping = vuln_env['port_mapping'] # {8080 => 'RPORT'} + + # Get used ports from registry + used = registry.used_ports + + # Allocate ports + allocator = PortAllocator.new(used) + allocated_ports = {} + + shared['ports'].each do |name, container_port| + # Check if user specified a port override: RPORT=8081 + preferred = nil + if datastore_option = port_mapping[container_port] + preferred = args.find { |a| a.start_with?("#{datastore_option}=") }&.split('=')&.last&.to_i + end + + # Allocate (falls back automatically if preferred is taken) + host_port = allocator.allocate(preferred) + allocated_ports[container_port] = host_port + end + + # Start container with allocated ports + container_id = runtime.run( + image: env_config['image'], + ports: allocated_ports # {80 => 49152} + ) + + # Report actual ports to user + allocated_ports.each do |container_port, host_port| + if datastore_option = port_mapping[container_port] + if host_port != (preferred || container_port) + print_status("Port #{preferred || container_port} unavailable, using #{host_port}") + end + print_status("Mapped container:#{container_port} -> host:#{host_port} (#{datastore_option})") + end + end + + # Auto-set datastore options + allocated_ports.each do |container_port, host_port| + if datastore_option = port_mapping[container_port] + mod.datastore[datastore_option] = host_port + print_status("Set #{datastore_option} = #{host_port}") + end + end +end +``` + +### Key Design Principles + +| Principle | Implementation | +|-----------|---------------| +| Never assume a port is available | `TCPServer.new` test before binding | +| Always provide fallback | Ephemeral range scan | +| Respect user preference | Try requested port first | +| Inform user of changes | Print status when fallback occurs | +| Auto-configure module | Set datastore options automatically | + +## Error Handling + +| Error Condition | Message | +|-----------------|---------| +| No runtime available | "No container runtime found. Install Docker or Podman." | +| Image pull failed | "Failed to pull image: {image}" | +| Container start failed (port conflict) | "Failed to start container: {error}. Try without RPORT override." | +| No available ports | "No available ports in range 49152-65535" | +| Container not found | "Container {id} not found" | From 017ee09b1eceb5c120dba1e72d9ef11a61f3f67f Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:48:36 +0300 Subject: [PATCH 06/27] Update formatting --- docs/architecture/05-runtime-adapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/05-runtime-adapter.md b/docs/architecture/05-runtime-adapter.md index bc49a2b5f61ff..2e3c572caed37 100644 --- a/docs/architecture/05-runtime-adapter.md +++ b/docs/architecture/05-runtime-adapter.md @@ -1,4 +1,4 @@ -Runtime Adapter & Port Allocation +# Runtime Adapter & Port Allocation ## What I Verified on My Machine From 251376f2a4226397f66b84ec4efd92f4014075f0 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Sat, 27 Jun 2026 01:32:29 +0300 Subject: [PATCH 07/27] reference modules and workflow drafts --- docs/ci_workflow.md | 194 ++++++++++++++++++++++++++ docs/reference_modules.md | 48 +++++++ docs/workflow.md | 280 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 docs/ci_workflow.md create mode 100644 docs/reference_modules.md create mode 100644 docs/workflow.md diff --git a/docs/ci_workflow.md b/docs/ci_workflow.md new file mode 100644 index 0000000000000..f4c9a90a137af --- /dev/null +++ b/docs/ci_workflow.md @@ -0,0 +1,194 @@ +# CI Workflow: Automated Exploit Verification + + + +This document defines how `test_env` will be used in GitHub Actions to automatically: +- Provision vulnerable environments from shared definitions +- Execute exploits with pre-configured datastore options +- Validate expected outcomes (session creation, command output) +- Clean up all containers to prevent resource leaks + +**Key principle:** CI consumes the same environment definitions used for local testing. No duplicated container configuration. + + +## Directory Structure + +This directory structure will created as part of the project: + +``` +metasploit-framework/ +├── ci/ +│ ├── test_activemq.rc +│ ├── test_jenkins.rc +│ └── test_drupal.rc +├── .github/ +│ └── workflows/ +│ └── vuln-env-test.yml +├── data/ +│ └── vuln_envs/ +│ ├── activemq.yml +│ ├── jenkins.yml +│ └── drupal.yml +└── docs/ + └── ci_workflow.md +``` + + + +A **resource script** with a `.rc` extension that contains msfconsole commands. Instead of typing commands one by one into msfconsole, they will be saved in a file and run: + +```bash +./msfconsole -q -r path/to/script.rc +``` + +Metasploit reads the file and executes each line automatically, as if it's typed. + + + +### Example: ci/test_jenkins.rc + +**What it is:** A text file containing msfconsole commands to test the Jenkins module automatically. + +**What it contains:** +```text +load test_env +use exploit/multi/http/jenkins_script_console +test_env build VERSION=2.361 +test_env exec 1 +test_env remove-all +exit +``` + +**What each line does:** +| Line | Command | Purpose | +|------|---------|---------| +| 1 | `load test_env` | Load the test_env plugin | +| 2 | `use exploit/multi/http/jenkins_script_console` | Select the exploit module | +| 3 | `test_env build VERSION=2.361` | Build environment using Jenkins version 2.361 | +| 4 | `test_env exec 1` | Execute exploit against environment ID 1 | +| 5 | `test_env remove-all` | Stop and remove all containers | +| 6 | `exit` | Close msfconsole | + +**How to run it manually (for testing):** +```bash +./msfconsole -q -r ci/test_jenkins.rc +``` +--- + +## GitHub Actions Workflow + +**What this is:** A YAML file that tells GitHub Actions what to do on every push or pull request. + +**File:** `.github/workflows/vuln-env-test.yml` + +**What it contains:** +```yaml +name: Vulnerable Environment Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-jenkins: + name: Test Jenkins Script Console + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Run Jenkins exploit test + run: | + ./msfconsole -q -r ci/test_jenkins.rc + + - name: Verify session was created + run: | + grep "Session.*opened" ~/.msf4/logs/framework.log || echo "WARNING: No session log found" + + - name: Verify no containers left behind + run: | + REMAINING=$(docker ps -q | wc -l) + if [ "$REMAINING" -eq 0 ]; then + echo "Clean: No containers remaining" + else + echo "FAIL: $REMAINING container(s) still running" + docker ps + exit 1 + fi +``` + +**What each step does:** +| Step | Action | Purpose | +|------|--------|---------| +| Checkout | `actions/checkout@v4` | Download your code | +| Set up Ruby | `ruby/setup-ruby@v1` | Install Ruby 3.2 and gems | +| Set up Docker | `docker/setup-buildx-action@v3` | Install Docker | +| Cache Docker layers | `actions/cache@v3` | Speed up image pulls | +| Run exploit test | `./msfconsole -q -r ci/test_jenkins.rc` | Execute the Jenkins resource script | +| Verify session | `grep "Session.*opened"` | Confirm exploit succeeded | +| Verify cleanup | `docker ps -q` | Confirm no leaked containers | + +--- + +## Validation Criteria + +| Step | Expected Result | How It Is Checked | On Failure | +|------|----------------|-------------------|------------| +| `test_env build` | Container starts, health check passes | Console output contains "Environment ready" | Workflow fails | +| `test_env exec 1` | Session opens | `framework.log` contains "Session.*opened" | Workflow fails | +| `test_env remove-all` | All containers removed | `docker ps -q` returns empty | Workflow fails | +| Post-cleanup | Zero `msf.vulnenv` containers remain | `docker ps -a --filter "label=msf.vulnenv.managed_by=test_env"` returns empty | Workflow fails | + +--- + +## CI Metadata in Environment Definitions + +Environment definitions include a `ci` section so the automation knows what payload to use and what to validate: + +```yaml +# data/vuln_envs/jenkins.yml +ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + TARGETURI: /script + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` + +### Schema Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `ci.exploit.payload` | String | Yes | Payload to use for automated execution | +| `ci.exploit.options` | Hash | No | Datastore options: `LHOST`, `LPORT`, `TARGETURI`, etc. | +| `ci.validation.expected_session` | Boolean | Yes | Whether a session must be created | +| `ci.validation.session_type` | String | No | Expected session type: `meterpreter`, `shell` | +| `ci.validation.expected_output` | String | No | Substring to verify in session output | +| `ci.validation.timeout` | Integer | Yes | Max seconds to wait for validation | + diff --git a/docs/reference_modules.md b/docs/reference_modules.md new file mode 100644 index 0000000000000..eb8d7e5e96516 --- /dev/null +++ b/docs/reference_modules.md @@ -0,0 +1,48 @@ +# Reference Modules for test_env + +## Selection Criteria +- Cover different service types: Java message broker (ActiveMQ), CI server (Jenkins), CMS (Drupal) +- Have clear, single-port (or well-defined multi-port) mappings +- Have existing Docker images with known vulnerable versions +- Demonstrate different health check patterns (API endpoint, login page, root page) +- Include both authenticated and unauthenticated exploit scenarios + +--- + +## Module 1: Apache ActiveMQ Jolokia RCE (Mentor Suggested) +- **Path:** `exploit/multi/http/apache_activemq_jolokia_rce` +- **Type:** Java web application (JMX-over-HTTP) +- **Ports:** 8161 (web console / Jolokia API), 61616 (OpenWire broker) +- **Health Check:** HTTP GET `/api/jolokia/` expecting 200, or GET `/` expecting 200 +- **Why:** h00die suggested PR #21497. Has a verified Docker one-liner. Real-world CVE-2026-34197. +- **VulnEnv Definition:** `activemq` +- **Docker Image:** `apache/activemq-classic:5.18.6` +- **Docker Run:** `docker run -d --name activemq -p 8161:8161 -p 61616:61616 apache/activemq-classic:5.18.6` +- **Credentials:** admin / admin +- **Exploit Context:** Requires authenticated Jolokia access; `TARGETURI` typically `/api/jolokia/` + +--- + +## Module 2: Jenkins Script Console +- **Path:** `exploit/multi/http/jenkins_script_console` +- **Type:** Web application / CI server +- **Port:** 8080 +- **Health Check:** HTTP GET `/login` expecting 200 +- **Why:** Well-documented, multiple versions exist, clear RPORT→8080 mapping, widely used in exploit development tutorials +- **VulnEnv Definition:** `jenkins` +- **Docker Image:** `vulnhub/jenkins:2.361` +- **Credentials:** admin / admin +- **Exploit Context:** Script Console at `/script` allows Groovy execution; `TARGETURI` typically `/script` + +--- + +## Module 3: Drupal Drupalgeddon2 +- **Path:** `exploit/unix/webapp/drupal_drupalgeddon2` +- **Type:** Web application / CMS +- **Port:** 80 +- **Health Check:** HTTP GET `/` expecting 200 +- **Why:** Simple single-port setup, unauthenticated exploit, different architecture from ActiveMQ/Jenkins, large community interest +- **VulnEnv Definition:** `drupal` +- **Docker Image:** `vulnhub/drupal:CVE-2018-7600` +- **Credentials:** None required (unauthenticated) +- **Exploit Context:** SA-CORE-2018-002 (CVE-2018-7600); remote code execution via form API diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000000000..fd04ab6163fff --- /dev/null +++ b/docs/workflow.md @@ -0,0 +1,280 @@ +# test_env User Workflow (Design Specification) + +This document specifies the intended user interaction with `test_env`. Every transcript below is the **target behavior** that implementation must achieve. These are our acceptance criteria. + +--- + +## Scenario 1: Build an Environment for the Active Module + +**Precondition:** The user has selected a module that defines a `VulnEnv` key in its metadata. + +**Input:** +``` +msf > use exploit/multi/http/apache_activemq_jolokia_rce +msf exploit(apache_activemq_jolokia_rce) > test_env build +``` + +**Expected behavior:** +- The plugin detects the active module and reads `mod.info['VulnEnv']` +- It resolves the definition name (`activemq`) and loads `data/vuln_envs/activemq.yml` +- It selects the default version (`5.18.6`) +- It auto-detects the container runtime (Docker preferred, Podman fallback) +- It pulls `apache/activemq-classic:5.18.6` if not already cached +- It starts the container with localhost binding and dynamic port allocation +- It waits for the health check defined in the YAML (`GET /api/jolokia/` → 200) +- Once ready, it prints the mapped host port and suggests datastore settings + +**Expected output:** +``` +[*] Resolving environment for apache_activemq_jolokia_rce... +[*] Definition: activemq | Version: 5.18.6 | Runtime: docker +[*] Pulling image apache/activemq-classic:5.18.6... +[*] Starting container... +[*] Waiting for health check (GET /api/jolokia/)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49152 +[*] TARGETURI => /api/jolokia/ +[*] USERNAME => admin +[*] PASSWORD => admin +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49152; exploit +``` + +**Postcondition:** A container is running. The module's datastore options (`RHOSTS`, `RPORT`, etc.) are automatically populated. The environment is registered in the in-memory registry with a unique ID. + +--- + +## Scenario 2: Build with a Specific Version + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 +``` + +**Expected behavior:** +- The `VERSION=2.375` argument overrides the `default_version` from the module's `VulnEnv` +- The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` +- If the version does not exist, the command fails immediately with a list of available versions + +**Expected output (success):** +``` +[*] Selected version: 2.375 +[*] Pulling image vulnhub/jenkins:2.375... +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49153 +[*] TARGETURI => /script +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49153; exploit +``` + +**Expected output (failure — version not found):** +``` +[-] Version '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 +``` + +--- + +## Scenario 3: Request a Specific Port (with Automatic Fallback) + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build RPORT=8080 +``` + +**Expected behavior:** +- The plugin attempts to bind host port 8080 to the container's exposed port +- If 8080 is already in use on the host, the `PortAllocator` scans the ephemeral range (49152–65535) for an available port +- The user is informed of the fallback. The allocated port is stored in the registry. + +**Expected output:** +``` +[*] Requested port 8080 is unavailable. Using dynamically allocated port 49154. +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49154 +[*] TARGETURI => /script +``` + +--- + +## Scenario 4: List Active Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env list +``` + +**Expected behavior:** +- The plugin queries the in-memory registry and prints a table using `Rex::Ui::Text::Table` +- Each row shows: ID, container ID (truncated), module fullname, RHOST, RPORT, status, version +- The table is sorted by ID + +**Expected output:** +``` +Environments +============ + +ID Container Module RHOST RPORT Status Version +-- --------- ------ ----- ----- ------ ------- +1 4f3a2b1c... exploit/multi/http/jenkins_script_console 127.0.0.1 49153 running 2.375 +2 9e8d7c6b... exploit/multi/http/apache_activemq_jolokia 127.0.0.1 49152 running 5.18.6 +``` + +--- + +## Scenario 5: Execute the Stored Exploit + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env exec 1 +``` + +**Expected behavior:** +- The plugin looks up environment ID 1 in the registry +- It verifies the container is running via `docker inspect` or `podman inspect` +- It automatically sets the module's datastore options (`RHOSTS`, `RPORT`, `TARGETURI`, etc.) from the registry record +- It invokes the module's `exploit` method (or `run_simple`) with these options +- If the environment is stopped, it prints an error telling the user to start it first + +**Expected output (success):** +``` +[*] Executing exploit against environment 1... +[*] Set RHOSTS 127.0.0.1 +[*] Set RPORT 49153 +[*] Set TARGETURI /script +[*] Started reverse TCP handler on 127.0.0.1:4444 +[+] Session 1 opened (127.0.0.1:4444 -> 127.0.0.1:49153) +``` + +**Expected output (failure — environment stopped):** +``` +[-] Environment 1 is not running. Start it with: test_env start 1 +``` + +--- + +## Scenario 6: Stop and Restart an Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env stop 1 +``` + +**Expected behavior:** +- The plugin calls `docker stop` (or `podman stop`) on the container ID stored in the registry +- It updates the registry status to `stopped` +- It preserves the registry record so the environment can be restarted + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[+] Environment 1 stopped. +``` + +**Input (restart):** +``` +msf exploit(multi/http/jenkins_script_console) > test_env start 1 +``` + +**Expected behavior:** +- The plugin calls `docker start` on the container +- It re-runs the health check defined in the environment definition +- It updates the registry status to `running` + +**Expected output:** +``` +[*] Starting container 4f3a2b1c... +[*] Waiting for health check... +[+] Environment 1 running. RPORT=49153 +``` + +--- + +## Scenario 7: Remove a Single Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove 1 +``` + +**Expected behavior:** +- The plugin stops the container if it is running +- It calls `docker rm` (or `podman rm`) to remove the container +- It removes the record from the in-memory registry +- If the database is active (Phase 2), it updates the DB status to `removed` + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[+] Environment 1 removed. +``` + +--- + +## Scenario 8: Remove All Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove-all +``` + +**Expected behavior:** +- The plugin iterates all entries in the registry +- For each, it stops and removes the container +- It clears the in-memory registry +- If any container fails to stop/remove, it prints a warning but continues + +**Expected output:** +``` +[*] Tearing down 2 environment(s)... +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[*] Stopping container 9e8d7c6b... +[*] Removing container 9e8d7c6b... +[+] All environments removed. +``` + +--- + +## Quick Reference: Command Summary + +| Command | Arguments | Description | +|---------|-----------|-------------| +| `test_env build` | `[VERSION=x]` `[RPORT=y]` | Build and launch environment for active module | +| `test_env list` | none | Show all tracked environments | +| `test_env exec` | `` | Execute exploit against environment `` | +| `test_env stop` | `` | Stop a running environment | +| `test_env start` | `` | Restart a stopped environment | +| `test_env remove` | `` | Tear down and remove one environment | +| `test_env remove-all` | none | Tear down all environments | +| `test_env help` | none | Show usage information | + +--- + +## Error Handling Specification + +| Error Condition | Expected Output | Implementation Notes | +|-----------------|----------------|---------------------| +| No active module | `[-] No active module. Use 'use ' first.` | Check `driver.active_module` before any other logic | +| Module has no `VulnEnv` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnEnv']` after resolving active module | +| No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | +| Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | +| Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | +| No available ports | `[-] No available ports in range 49152-65535` | `PortAllocator` raises after exhausting range | +| Health check timeout | `[-] Health check timed out after seconds` | `HealthManager` exceeds `retries * interval` | +| Environment ID not found | `[-] Environment not found` | Registry lookup returns `nil` | +| Environment not running | `[-] Environment is not running. Start it with: test_env start ` | Check `status` field before `exec` | + +--- + +## Notes for Implementers + +- All output prefixes (`[*]`, `[+]`, `[-]`) must use `print_status`, `print_good`, `print_error` respectively +- Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` +- The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) +- Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost From 0d435c9d7899982c490d2765410c3354933b46a8 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Sat, 27 Jun 2026 02:05:39 +0300 Subject: [PATCH 08/27] Week 1 tasks Complete: test_env plugin skeleton and architecture --- docs/test_env/README.md | 50 ++++ docs/test_env/architecture/.gitkeep | 0 .../architecture/01-command-dispatcher.md | 0 .../architecture/02-module-metadata.md | 0 .../architecture/03-database-schema.md | 0 .../architecture/04-environment-schema.md | 0 .../architecture/05-runtime-adapter.md | 0 docs/test_env/ci_workflow.md | 194 ++++++++++++ docs/test_env/reference_modules.md | 48 +++ docs/test_env/workflow.md | 280 ++++++++++++++++++ 10 files changed, 572 insertions(+) create mode 100644 docs/test_env/README.md create mode 100644 docs/test_env/architecture/.gitkeep rename docs/{ => test_env}/architecture/01-command-dispatcher.md (100%) rename docs/{ => test_env}/architecture/02-module-metadata.md (100%) rename docs/{ => test_env}/architecture/03-database-schema.md (100%) rename docs/{ => test_env}/architecture/04-environment-schema.md (100%) rename docs/{ => test_env}/architecture/05-runtime-adapter.md (100%) create mode 100644 docs/test_env/ci_workflow.md create mode 100644 docs/test_env/reference_modules.md create mode 100644 docs/test_env/workflow.md diff --git a/docs/test_env/README.md b/docs/test_env/README.md new file mode 100644 index 0000000000000..d51d4ec0788f5 --- /dev/null +++ b/docs/test_env/README.md @@ -0,0 +1,50 @@ +# test_env Design Documentation + +This directory contains the architecture and workflow design for the `test_env` (VulnEnv) plugin. + +## Architecture Documents + +| Document | Description | +|----------|-------------| +| [01-command-dispatcher.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/01-command-dispatcher.md) | How `test_env` is added to msfconsole via plugin dispatcher | +| [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/02-module-metadata.md) | How modules expose `VulnEnv` metadata and how the plugin reads it | +| [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/03-database-schema.md) | Registry persistence: in-memory Phase 1, PostgreSQL Phase 2 | +| [04-environment-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/04-environment-schema.md) | YAML schema for shared environment definitions in `data/vuln_envs/` | +| [05-runtime-adapter.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/05-runtime-adapter.md) | Docker/Podman abstraction, port allocation, container labels | + +## Workflow & Planning Documents + +| Document | Description | +|----------|-------------| +| [reference_modules.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/reference_modules.md) | 3 reference modules selected for implementation (ActiveMQ, Jenkins, Drupal) | +| [workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/workflow.md) | Target user workflows and console transcripts (acceptance criteria) | +| [ci_workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/ci_workflow.md) | GitHub Actions CI integration with resource scripts | + +## Plugin File + +- `plugins/test_env.rb` — Main plugin implementation (Week 1 skeleton) + +``` +nayera@Nero:~/git/metasploit-framework$ ./msfconsole -q -x "load test_env; exit" +[*] VulnEnv plugin loaded. +[*] Successfully loaded plugin: vulnenv + +nayera@Nero:~/git/metasploit-framework$ ./msfconsole -q -x "load test_env; test_env help; exit" +[*] VulnEnv plugin loaded. +[*] Successfully loaded plugin: vulnenv +Usage: test_env + +Commands: + build Build and launch environment for active module + list List tracked environments + stop Stop a running environment + start Restart a stopped environment + remove Tear down an environment + remove-all Tear down all environments + exec Execute exploit against environment + help Show this help +``` + +## Data Files + +- `data/vuln_envs/jenkins.yml` — Reference environment definition (Week 1 draft) diff --git a/docs/test_env/architecture/.gitkeep b/docs/test_env/architecture/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/architecture/01-command-dispatcher.md b/docs/test_env/architecture/01-command-dispatcher.md similarity index 100% rename from docs/architecture/01-command-dispatcher.md rename to docs/test_env/architecture/01-command-dispatcher.md diff --git a/docs/architecture/02-module-metadata.md b/docs/test_env/architecture/02-module-metadata.md similarity index 100% rename from docs/architecture/02-module-metadata.md rename to docs/test_env/architecture/02-module-metadata.md diff --git a/docs/architecture/03-database-schema.md b/docs/test_env/architecture/03-database-schema.md similarity index 100% rename from docs/architecture/03-database-schema.md rename to docs/test_env/architecture/03-database-schema.md diff --git a/docs/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md similarity index 100% rename from docs/architecture/04-environment-schema.md rename to docs/test_env/architecture/04-environment-schema.md diff --git a/docs/architecture/05-runtime-adapter.md b/docs/test_env/architecture/05-runtime-adapter.md similarity index 100% rename from docs/architecture/05-runtime-adapter.md rename to docs/test_env/architecture/05-runtime-adapter.md diff --git a/docs/test_env/ci_workflow.md b/docs/test_env/ci_workflow.md new file mode 100644 index 0000000000000..f4c9a90a137af --- /dev/null +++ b/docs/test_env/ci_workflow.md @@ -0,0 +1,194 @@ +# CI Workflow: Automated Exploit Verification + + + +This document defines how `test_env` will be used in GitHub Actions to automatically: +- Provision vulnerable environments from shared definitions +- Execute exploits with pre-configured datastore options +- Validate expected outcomes (session creation, command output) +- Clean up all containers to prevent resource leaks + +**Key principle:** CI consumes the same environment definitions used for local testing. No duplicated container configuration. + + +## Directory Structure + +This directory structure will created as part of the project: + +``` +metasploit-framework/ +├── ci/ +│ ├── test_activemq.rc +│ ├── test_jenkins.rc +│ └── test_drupal.rc +├── .github/ +│ └── workflows/ +│ └── vuln-env-test.yml +├── data/ +│ └── vuln_envs/ +│ ├── activemq.yml +│ ├── jenkins.yml +│ └── drupal.yml +└── docs/ + └── ci_workflow.md +``` + + + +A **resource script** with a `.rc` extension that contains msfconsole commands. Instead of typing commands one by one into msfconsole, they will be saved in a file and run: + +```bash +./msfconsole -q -r path/to/script.rc +``` + +Metasploit reads the file and executes each line automatically, as if it's typed. + + + +### Example: ci/test_jenkins.rc + +**What it is:** A text file containing msfconsole commands to test the Jenkins module automatically. + +**What it contains:** +```text +load test_env +use exploit/multi/http/jenkins_script_console +test_env build VERSION=2.361 +test_env exec 1 +test_env remove-all +exit +``` + +**What each line does:** +| Line | Command | Purpose | +|------|---------|---------| +| 1 | `load test_env` | Load the test_env plugin | +| 2 | `use exploit/multi/http/jenkins_script_console` | Select the exploit module | +| 3 | `test_env build VERSION=2.361` | Build environment using Jenkins version 2.361 | +| 4 | `test_env exec 1` | Execute exploit against environment ID 1 | +| 5 | `test_env remove-all` | Stop and remove all containers | +| 6 | `exit` | Close msfconsole | + +**How to run it manually (for testing):** +```bash +./msfconsole -q -r ci/test_jenkins.rc +``` +--- + +## GitHub Actions Workflow + +**What this is:** A YAML file that tells GitHub Actions what to do on every push or pull request. + +**File:** `.github/workflows/vuln-env-test.yml` + +**What it contains:** +```yaml +name: Vulnerable Environment Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-jenkins: + name: Test Jenkins Script Console + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Run Jenkins exploit test + run: | + ./msfconsole -q -r ci/test_jenkins.rc + + - name: Verify session was created + run: | + grep "Session.*opened" ~/.msf4/logs/framework.log || echo "WARNING: No session log found" + + - name: Verify no containers left behind + run: | + REMAINING=$(docker ps -q | wc -l) + if [ "$REMAINING" -eq 0 ]; then + echo "Clean: No containers remaining" + else + echo "FAIL: $REMAINING container(s) still running" + docker ps + exit 1 + fi +``` + +**What each step does:** +| Step | Action | Purpose | +|------|--------|---------| +| Checkout | `actions/checkout@v4` | Download your code | +| Set up Ruby | `ruby/setup-ruby@v1` | Install Ruby 3.2 and gems | +| Set up Docker | `docker/setup-buildx-action@v3` | Install Docker | +| Cache Docker layers | `actions/cache@v3` | Speed up image pulls | +| Run exploit test | `./msfconsole -q -r ci/test_jenkins.rc` | Execute the Jenkins resource script | +| Verify session | `grep "Session.*opened"` | Confirm exploit succeeded | +| Verify cleanup | `docker ps -q` | Confirm no leaked containers | + +--- + +## Validation Criteria + +| Step | Expected Result | How It Is Checked | On Failure | +|------|----------------|-------------------|------------| +| `test_env build` | Container starts, health check passes | Console output contains "Environment ready" | Workflow fails | +| `test_env exec 1` | Session opens | `framework.log` contains "Session.*opened" | Workflow fails | +| `test_env remove-all` | All containers removed | `docker ps -q` returns empty | Workflow fails | +| Post-cleanup | Zero `msf.vulnenv` containers remain | `docker ps -a --filter "label=msf.vulnenv.managed_by=test_env"` returns empty | Workflow fails | + +--- + +## CI Metadata in Environment Definitions + +Environment definitions include a `ci` section so the automation knows what payload to use and what to validate: + +```yaml +# data/vuln_envs/jenkins.yml +ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + TARGETURI: /script + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` + +### Schema Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `ci.exploit.payload` | String | Yes | Payload to use for automated execution | +| `ci.exploit.options` | Hash | No | Datastore options: `LHOST`, `LPORT`, `TARGETURI`, etc. | +| `ci.validation.expected_session` | Boolean | Yes | Whether a session must be created | +| `ci.validation.session_type` | String | No | Expected session type: `meterpreter`, `shell` | +| `ci.validation.expected_output` | String | No | Substring to verify in session output | +| `ci.validation.timeout` | Integer | Yes | Max seconds to wait for validation | + diff --git a/docs/test_env/reference_modules.md b/docs/test_env/reference_modules.md new file mode 100644 index 0000000000000..eb8d7e5e96516 --- /dev/null +++ b/docs/test_env/reference_modules.md @@ -0,0 +1,48 @@ +# Reference Modules for test_env + +## Selection Criteria +- Cover different service types: Java message broker (ActiveMQ), CI server (Jenkins), CMS (Drupal) +- Have clear, single-port (or well-defined multi-port) mappings +- Have existing Docker images with known vulnerable versions +- Demonstrate different health check patterns (API endpoint, login page, root page) +- Include both authenticated and unauthenticated exploit scenarios + +--- + +## Module 1: Apache ActiveMQ Jolokia RCE (Mentor Suggested) +- **Path:** `exploit/multi/http/apache_activemq_jolokia_rce` +- **Type:** Java web application (JMX-over-HTTP) +- **Ports:** 8161 (web console / Jolokia API), 61616 (OpenWire broker) +- **Health Check:** HTTP GET `/api/jolokia/` expecting 200, or GET `/` expecting 200 +- **Why:** h00die suggested PR #21497. Has a verified Docker one-liner. Real-world CVE-2026-34197. +- **VulnEnv Definition:** `activemq` +- **Docker Image:** `apache/activemq-classic:5.18.6` +- **Docker Run:** `docker run -d --name activemq -p 8161:8161 -p 61616:61616 apache/activemq-classic:5.18.6` +- **Credentials:** admin / admin +- **Exploit Context:** Requires authenticated Jolokia access; `TARGETURI` typically `/api/jolokia/` + +--- + +## Module 2: Jenkins Script Console +- **Path:** `exploit/multi/http/jenkins_script_console` +- **Type:** Web application / CI server +- **Port:** 8080 +- **Health Check:** HTTP GET `/login` expecting 200 +- **Why:** Well-documented, multiple versions exist, clear RPORT→8080 mapping, widely used in exploit development tutorials +- **VulnEnv Definition:** `jenkins` +- **Docker Image:** `vulnhub/jenkins:2.361` +- **Credentials:** admin / admin +- **Exploit Context:** Script Console at `/script` allows Groovy execution; `TARGETURI` typically `/script` + +--- + +## Module 3: Drupal Drupalgeddon2 +- **Path:** `exploit/unix/webapp/drupal_drupalgeddon2` +- **Type:** Web application / CMS +- **Port:** 80 +- **Health Check:** HTTP GET `/` expecting 200 +- **Why:** Simple single-port setup, unauthenticated exploit, different architecture from ActiveMQ/Jenkins, large community interest +- **VulnEnv Definition:** `drupal` +- **Docker Image:** `vulnhub/drupal:CVE-2018-7600` +- **Credentials:** None required (unauthenticated) +- **Exploit Context:** SA-CORE-2018-002 (CVE-2018-7600); remote code execution via form API diff --git a/docs/test_env/workflow.md b/docs/test_env/workflow.md new file mode 100644 index 0000000000000..fd04ab6163fff --- /dev/null +++ b/docs/test_env/workflow.md @@ -0,0 +1,280 @@ +# test_env User Workflow (Design Specification) + +This document specifies the intended user interaction with `test_env`. Every transcript below is the **target behavior** that implementation must achieve. These are our acceptance criteria. + +--- + +## Scenario 1: Build an Environment for the Active Module + +**Precondition:** The user has selected a module that defines a `VulnEnv` key in its metadata. + +**Input:** +``` +msf > use exploit/multi/http/apache_activemq_jolokia_rce +msf exploit(apache_activemq_jolokia_rce) > test_env build +``` + +**Expected behavior:** +- The plugin detects the active module and reads `mod.info['VulnEnv']` +- It resolves the definition name (`activemq`) and loads `data/vuln_envs/activemq.yml` +- It selects the default version (`5.18.6`) +- It auto-detects the container runtime (Docker preferred, Podman fallback) +- It pulls `apache/activemq-classic:5.18.6` if not already cached +- It starts the container with localhost binding and dynamic port allocation +- It waits for the health check defined in the YAML (`GET /api/jolokia/` → 200) +- Once ready, it prints the mapped host port and suggests datastore settings + +**Expected output:** +``` +[*] Resolving environment for apache_activemq_jolokia_rce... +[*] Definition: activemq | Version: 5.18.6 | Runtime: docker +[*] Pulling image apache/activemq-classic:5.18.6... +[*] Starting container... +[*] Waiting for health check (GET /api/jolokia/)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49152 +[*] TARGETURI => /api/jolokia/ +[*] USERNAME => admin +[*] PASSWORD => admin +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49152; exploit +``` + +**Postcondition:** A container is running. The module's datastore options (`RHOSTS`, `RPORT`, etc.) are automatically populated. The environment is registered in the in-memory registry with a unique ID. + +--- + +## Scenario 2: Build with a Specific Version + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 +``` + +**Expected behavior:** +- The `VERSION=2.375` argument overrides the `default_version` from the module's `VulnEnv` +- The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` +- If the version does not exist, the command fails immediately with a list of available versions + +**Expected output (success):** +``` +[*] Selected version: 2.375 +[*] Pulling image vulnhub/jenkins:2.375... +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49153 +[*] TARGETURI => /script +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49153; exploit +``` + +**Expected output (failure — version not found):** +``` +[-] Version '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 +``` + +--- + +## Scenario 3: Request a Specific Port (with Automatic Fallback) + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build RPORT=8080 +``` + +**Expected behavior:** +- The plugin attempts to bind host port 8080 to the container's exposed port +- If 8080 is already in use on the host, the `PortAllocator` scans the ephemeral range (49152–65535) for an available port +- The user is informed of the fallback. The allocated port is stored in the registry. + +**Expected output:** +``` +[*] Requested port 8080 is unavailable. Using dynamically allocated port 49154. +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49154 +[*] TARGETURI => /script +``` + +--- + +## Scenario 4: List Active Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env list +``` + +**Expected behavior:** +- The plugin queries the in-memory registry and prints a table using `Rex::Ui::Text::Table` +- Each row shows: ID, container ID (truncated), module fullname, RHOST, RPORT, status, version +- The table is sorted by ID + +**Expected output:** +``` +Environments +============ + +ID Container Module RHOST RPORT Status Version +-- --------- ------ ----- ----- ------ ------- +1 4f3a2b1c... exploit/multi/http/jenkins_script_console 127.0.0.1 49153 running 2.375 +2 9e8d7c6b... exploit/multi/http/apache_activemq_jolokia 127.0.0.1 49152 running 5.18.6 +``` + +--- + +## Scenario 5: Execute the Stored Exploit + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env exec 1 +``` + +**Expected behavior:** +- The plugin looks up environment ID 1 in the registry +- It verifies the container is running via `docker inspect` or `podman inspect` +- It automatically sets the module's datastore options (`RHOSTS`, `RPORT`, `TARGETURI`, etc.) from the registry record +- It invokes the module's `exploit` method (or `run_simple`) with these options +- If the environment is stopped, it prints an error telling the user to start it first + +**Expected output (success):** +``` +[*] Executing exploit against environment 1... +[*] Set RHOSTS 127.0.0.1 +[*] Set RPORT 49153 +[*] Set TARGETURI /script +[*] Started reverse TCP handler on 127.0.0.1:4444 +[+] Session 1 opened (127.0.0.1:4444 -> 127.0.0.1:49153) +``` + +**Expected output (failure — environment stopped):** +``` +[-] Environment 1 is not running. Start it with: test_env start 1 +``` + +--- + +## Scenario 6: Stop and Restart an Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env stop 1 +``` + +**Expected behavior:** +- The plugin calls `docker stop` (or `podman stop`) on the container ID stored in the registry +- It updates the registry status to `stopped` +- It preserves the registry record so the environment can be restarted + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[+] Environment 1 stopped. +``` + +**Input (restart):** +``` +msf exploit(multi/http/jenkins_script_console) > test_env start 1 +``` + +**Expected behavior:** +- The plugin calls `docker start` on the container +- It re-runs the health check defined in the environment definition +- It updates the registry status to `running` + +**Expected output:** +``` +[*] Starting container 4f3a2b1c... +[*] Waiting for health check... +[+] Environment 1 running. RPORT=49153 +``` + +--- + +## Scenario 7: Remove a Single Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove 1 +``` + +**Expected behavior:** +- The plugin stops the container if it is running +- It calls `docker rm` (or `podman rm`) to remove the container +- It removes the record from the in-memory registry +- If the database is active (Phase 2), it updates the DB status to `removed` + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[+] Environment 1 removed. +``` + +--- + +## Scenario 8: Remove All Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove-all +``` + +**Expected behavior:** +- The plugin iterates all entries in the registry +- For each, it stops and removes the container +- It clears the in-memory registry +- If any container fails to stop/remove, it prints a warning but continues + +**Expected output:** +``` +[*] Tearing down 2 environment(s)... +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[*] Stopping container 9e8d7c6b... +[*] Removing container 9e8d7c6b... +[+] All environments removed. +``` + +--- + +## Quick Reference: Command Summary + +| Command | Arguments | Description | +|---------|-----------|-------------| +| `test_env build` | `[VERSION=x]` `[RPORT=y]` | Build and launch environment for active module | +| `test_env list` | none | Show all tracked environments | +| `test_env exec` | `` | Execute exploit against environment `` | +| `test_env stop` | `` | Stop a running environment | +| `test_env start` | `` | Restart a stopped environment | +| `test_env remove` | `` | Tear down and remove one environment | +| `test_env remove-all` | none | Tear down all environments | +| `test_env help` | none | Show usage information | + +--- + +## Error Handling Specification + +| Error Condition | Expected Output | Implementation Notes | +|-----------------|----------------|---------------------| +| No active module | `[-] No active module. Use 'use ' first.` | Check `driver.active_module` before any other logic | +| Module has no `VulnEnv` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnEnv']` after resolving active module | +| No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | +| Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | +| Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | +| No available ports | `[-] No available ports in range 49152-65535` | `PortAllocator` raises after exhausting range | +| Health check timeout | `[-] Health check timed out after seconds` | `HealthManager` exceeds `retries * interval` | +| Environment ID not found | `[-] Environment not found` | Registry lookup returns `nil` | +| Environment not running | `[-] Environment is not running. Start it with: test_env start ` | Check `status` field before `exec` | + +--- + +## Notes for Implementers + +- All output prefixes (`[*]`, `[+]`, `[-]`) must use `print_status`, `print_good`, `print_error` respectively +- Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` +- The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) +- Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost From 1cf782b26338c1bdb923788d5e6f0a5099f2098d Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Sat, 27 Jun 2026 02:17:31 +0300 Subject: [PATCH 09/27] plugin skeleton --- plugins/test_env.rb | 89 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 plugins/test_env.rb diff --git a/plugins/test_env.rb b/plugins/test_env.rb new file mode 100644 index 0000000000000..79cb4162dbbbd --- /dev/null +++ b/plugins/test_env.rb @@ -0,0 +1,89 @@ +module Msf + class Plugin::VulnEnv < Msf::Plugin + + class ConsoleCommandDispatcher + include Msf::Ui::Console::CommandDispatcher + + def name + 'VulnEnv' + end + + def commands + { + 'test_env' => 'Manage vulnerable test environments' + } + end + + def cmd_test_env(*args) + if args.empty? || args.first == '-h' || args.first == '--help' + cmd_test_env_help + return + end + + subcommand = args.shift + + case subcommand + when 'build' + print_status("TODO: test_env build") + when 'list' + print_status("TODO: test_env list") + when 'stop' + print_status("TODO: test_env stop") + when 'start' + print_status("TODO: test_env start") + when 'remove' + print_status("TODO: test_env remove") + when 'remove-all' + print_status("TODO: test_env remove-all") + when 'exec' + print_status("TODO: test_env exec") + when 'help' + cmd_test_env_help + else + print_error("Unknown subcommand: #{subcommand}") + cmd_test_env_help + end + end + + def cmd_test_env_help + print_line("Usage: test_env ") + print_line + print_line("Commands:") + print_line(" build Build and launch environment for active module") + print_line(" list List tracked environments") + print_line(" stop Stop a running environment") + print_line(" start Restart a stopped environment") + print_line(" remove Tear down an environment") + print_line(" remove-all Tear down all environments") + print_line(" exec Execute exploit against environment") + print_line(" help Show this help") + print_line + end + + def cmd_test_env_tabs(str, words) + if words.length == 1 + return %w[build list stop start remove remove-all exec help] + end + [] + end + end + + def initialize(framework, opts) + super + add_console_dispatcher(ConsoleCommandDispatcher) + print_status("VulnEnv plugin loaded.") + end + + def cleanup + remove_console_dispatcher('VulnEnv') + end + + def name + 'vulnenv' + end + + def desc + 'Automated vulnerable environment provisioning' + end + end +end From f43d97a78eb045cec0f51e666ff6479bc336f340 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sat, 27 Jun 2026 20:23:45 +0300 Subject: [PATCH 10/27] Update links in README --- docs/test_env/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/test_env/README.md b/docs/test_env/README.md index d51d4ec0788f5..bda6e89f923fa 100644 --- a/docs/test_env/README.md +++ b/docs/test_env/README.md @@ -6,23 +6,23 @@ This directory contains the architecture and workflow design for the `test_env` | Document | Description | |----------|-------------| -| [01-command-dispatcher.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/01-command-dispatcher.md) | How `test_env` is added to msfconsole via plugin dispatcher | -| [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/02-module-metadata.md) | How modules expose `VulnEnv` metadata and how the plugin reads it | -| [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/03-database-schema.md) | Registry persistence: in-memory Phase 1, PostgreSQL Phase 2 | -| [04-environment-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/04-environment-schema.md) | YAML schema for shared environment definitions in `data/vuln_envs/` | +| [01-command-dispatcher.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/01-command-dispatcher.md) | How `test_env` is added to msfconsole via plugin dispatcher | +| [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/02-module-metadata.md) | How modules expose `VulnEnv` metadata and how the plugin reads it | +| [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/03-database-schema.md) | Registry persistence: in-memory Phase 1, PostgreSQL Phase 2 | +| [04-environment-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/04-environment-schema.md) | YAML schema for shared environment definitions in `data/vuln_envs/` | | [05-runtime-adapter.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/05-runtime-adapter.md) | Docker/Podman abstraction, port allocation, container labels | ## Workflow & Planning Documents | Document | Description | |----------|-------------| -| [reference_modules.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/reference_modules.md) | 3 reference modules selected for implementation (ActiveMQ, Jenkins, Drupal) | -| [workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/workflow.md) | Target user workflows and console transcripts (acceptance criteria) | -| [ci_workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/ci_workflow.md) | GitHub Actions CI integration with resource scripts | +| [reference_modules.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/reference_modules.md) | 3 reference modules selected for implementation (ActiveMQ, Jenkins, Drupal) | +| [workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/workflow.md) | Target user workflows and console transcripts (acceptance criteria) | +| [ci_workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/ci_workflow.md) | GitHub Actions CI integration with resource scripts | ## Plugin File -- `plugins/test_env.rb` — Main plugin implementation (Week 1 skeleton) +- [plugins/test_env.rb](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/plugins/test_env.rb) — Main plugin implementation (Week 1 skeleton) ``` nayera@Nero:~/git/metasploit-framework$ ./msfconsole -q -x "load test_env; exit" @@ -47,4 +47,4 @@ Commands: ## Data Files -- `data/vuln_envs/jenkins.yml` — Reference environment definition (Week 1 draft) +- [data/vuln_envs/jenkins.ym](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/data/vuln_envs/jenkins.yml) — Reference environment definition (Week 1 draft) From 28c5af5cfbe686505ced3b0cce25dcffa785e8be Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:30:47 +0300 Subject: [PATCH 11/27] Enhance command dispatcher with range support --- .../architecture/01-command-dispatcher.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/test_env/architecture/01-command-dispatcher.md b/docs/test_env/architecture/01-command-dispatcher.md index 32075f0fa94ad..1d82184ef60b3 100644 --- a/docs/test_env/architecture/01-command-dispatcher.md +++ b/docs/test_env/architecture/01-command-dispatcher.md @@ -52,13 +52,17 @@ Msf::Plugin |-----------|---------------|--------------| | `build` | `cmd_test_env_build(args)` | Build and launch environment for active module | | `list` | `cmd_test_env_list(args)` | Show all tracked environments | -| `stop ` | `cmd_test_env_stop(args)` | Stop a running container | -| `start ` | `cmd_test_env_start(args)` | Restart a stopped container | -| `remove ` | `cmd_test_env_remove(args)` | Tear down a container | +| `stop ` | `cmd_test_env_stop(args)` | Stop running container(s) | +| `start ` | `cmd_test_env_start(args)` | Restart stopped container(s) | +| `remove ` | `cmd_test_env_remove(args)` | Tear down container(s) | | `remove-all` | `cmd_test_env_remove_all(args)` | Tear down all containers | | `exec ` | `cmd_test_env_exec(args)` | Run exploit against environment | | `help` | `cmd_test_env_help` | Show usage | +## Range Parsing + +Metasploit commands like `sessions -k` support comma-separated and dash-separated ranges (e.g., `1-3,5,7-9`). The `stop`, `start`, and `remove` subcommands follow this pattern. `exec` intentionally accepts only a single ID for safety. + ## Sample code for solid clarification ### Argument Parsing Logic ```ruby @@ -100,9 +104,12 @@ def cmd_test_env_tabs(str, words) # If subcommand is stop/start/remove/exec, suggest environment IDs if words.length == 2 case words[0] - when 'stop', 'start', 'remove', 'exec' + when 'stop', 'start', 'remove' # TODO: Return IDs from registry (Week 6) return [] + when 'exec' + # Single ID only + return [] end end @@ -151,4 +158,5 @@ end | How to parse subcommands? | `case` statement on `args.shift` | Same as `cmd_jobs` | | Tab completion? | `cmd_test_env_tabs` method | For good UX | | Error handling? | `begin/rescue` with `print_error` | Consistent with framework style | +| Range support for IDs? | Comma/dash ranges for `stop`/`start`/`remove`; single ID for `exec` | Matches `sessions -k` pattern; mentor feedback — bulk ops should support ranges, but `exec` is safer single-target | From ebf1f378c12413bb4772a92ede8c7506f6288e15 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:18:30 +0300 Subject: [PATCH 12/27] revise container labels and add payload helpers --- .../architecture/03-database-schema.md | 100 +++++++++++++++--- 1 file changed, 86 insertions(+), 14 deletions(-) diff --git a/docs/test_env/architecture/03-database-schema.md b/docs/test_env/architecture/03-database-schema.md index af3a02693260f..5af4324f037d0 100644 --- a/docs/test_env/architecture/03-database-schema.md +++ b/docs/test_env/architecture/03-database-schema.md @@ -133,37 +133,107 @@ end ### Container Labels (Cross-Session Identification) Since in-memory data is lost on msfconsole restart, use **OCI container labels** -to identify and reconstruct environments: +to identify and reconstruct environments. + +**Design Decision:** Hybrid approach — individual labels for filterable identity +fields, plus a base64-encoded JSON payload for complex metadata. + +**Rationale:** Docker's `--filter` flag only supports string matching on +individual label values. We need native filtering for discovery (`managed_by`) +and session isolation (`instance_id`). However, complex nested data +(datastore hashes, credentials, exploit commands) is better packed into a +single atomic JSON blob to avoid partial label corruption and simplify +schema evolution. ```bash docker run -d \ + --label "msf.vulnenv.managed_by=test_env" \ --label "msf.vulnenv.instance_id=msf-$(hostname)-$$" \ --label "msf.vulnenv.module=exploit/multi/http/jenkins_script_console" \ - --label "msf.vulnenv.version=2.361" \ --label "msf.vulnenv.env_id=1" \ - --label "msf.vulnenv.created_at=2024-06-25T17:37:00Z" \ + --label "msf.vulnenv.payload=eyJkYXRhc3RvcmUiOnsiUkhPU1RTIjoiMTI3LjAuMC4xIiwiUlBPUlQiOjgwODF9LCJleHBsb2l0X2NvbW1hbmQiOiJzZXQgUkhPU1RTIDEyNy4wLjAuMTsgc2V0IFJQT1JUIDgwODE7IGV4cGxvaXQiLCJjcmVkZW50aWFscyI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9LCJ2ZXJzaW9uIjoiMi4zNjEiLCJjcmVhdGVkX2F0IjoiMjAyNC0wNi0yNVQxNzozNzowMFoiLCJydW50aW1lIjoiZG9ja2VyIn0=" \ vulnhub/jenkins:2.361 ``` **Label Schema:** -| Label | Value | Purpose | -|-------|-------|---------| -| `msf.vulnenv.instance_id` | `msf-{hostname}-{pid}` | Identify msfconsole instance | -| `msf.vulnenv.module` | Module fullname | Link to exploit module | -| `msf.vulnenv.version` | Environment version | Track which version | -| `msf.vulnenv.env_id` | Internal registry ID | Cross-reference | -| `msf.vulnenv.created_at` | ISO8601 timestamp | Audit trail | -| `msf.vulnenv.managed_by` | `test_env` | Identify framework-managed | + +| Label | Type | Value | Purpose | +|-------|------|-------|---------| +| `msf.vulnenv.managed_by` | Individual | `test_env` | Discovery filter — find all framework-managed containers | +| `msf.vulnenv.instance_id` | Individual | `msf-{hostname}-{pid}` | Session isolation — identify which msfconsole created it | +| `msf.vulnenv.module` | Individual | Module fullname | Module linkage — filter by target module | +| `msf.vulnenv.env_id` | Individual | `1` | Registry cross-reference — map to in-memory ID | +| `msf.vulnenv.payload` | **Base64 JSON** | Encoded metadata blob | Complete state reconstruction | + +**Payload JSON Structure (before encoding):** +```json +{ + "datastore": {"RHOSTS": "127.0.0.1", "RPORT": 8081}, + "exploit_command": "set RHOSTS 127.0.0.1; set RPORT 8081; exploit", + "credentials": {"username": "admin", "password": "admin"}, + "version": "2.361", + "created_at": "2024-06-25T17:37:00Z", + "runtime": "docker" +} +``` + +**Encoding/Decoding Helpers:** +```ruby +require 'json' +require 'base64' + +def encode_payload(data) + Base64.strict_encode64(data.to_json) +end + +def decode_payload(encoded) + return nil unless encoded + + json = Base64.strict_decode64(encoded) + JSON.parse(json, symbolize_names: true) +rescue ArgumentError, JSON::ParserError => e + print_error("Failed to decode container payload: #{e.message}") + nil +end +``` ### State Reconstruction From Labels (Future Enhancement) ```ruby def reconstruct_from_labels(runtime) + # Step 1: Discover all framework-managed containers via native filter containers = runtime.list(filters: { 'label' => 'msf.vulnenv.managed_by=test_env' }) + containers.each do |container| - labels = container['Labels'] - # Rebuild registry entry from labels - # (Week 6 enhancement) + labels = container['Config']['Labels'] || {} + + # Step 2: Skip containers from other msfconsole instances + instance_id = labels['msf.vulnenv.instance_id'] + next unless instance_id == current_instance_id + + # Step 3: Decode the payload for full metadata + payload = decode_payload(labels['msf.vulnenv.payload']) + next unless payload + + # Step 4: Rebuild registry entry + env_id = labels['msf.vulnenv.env_id'].to_i + @environments[env_id] = { + local_id: env_id, + container_id: container['Id'], + module_fullname: labels['msf.vulnenv.module'], + env_version: payload[:version], + rhost: payload[:datastore]['RHOSTS'], + rport: payload[:datastore]['RPORT'], + runtime: payload[:runtime], + image_ref: container['Config']['Image'], + status: container['State']['Status'], + exploit_command: payload[:exploit_command], + datastore: payload[:datastore], + created_at: Time.parse(payload[:created_at]), + started_at: Time.parse(container['State']['StartedAt']) + } + + @next_id = [@next_id, env_id + 1].max end end ``` @@ -285,3 +355,5 @@ My `vuln_environments` table follows this exact pattern: - `datastore` serialized text - `local_id` equivalent via `env_id` label - Lifecycle timestamps (`created_at`, `started_at`, `stopped_at`, `removed_at`) + + From f75cdc873e13cf2f75d91506278f77a4dcb7b8b9 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:30:58 +0300 Subject: [PATCH 13/27] Enhance environment schema with multi profiles --- .../architecture/04-environment-schema.md | 233 ++++++++++++++---- 1 file changed, 185 insertions(+), 48 deletions(-) diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md index 87822fa797053..a6c7d0df6dc16 100644 --- a/docs/test_env/architecture/04-environment-schema.md +++ b/docs/test_env/architecture/04-environment-schema.md @@ -29,7 +29,7 @@ Health check type: http data/ vuln_envs/ README.md # Schema documentation - jenkins.yml # Jenkins environments (reference implementation) + jenkins.yml # Jenkins with multiple profiles ``` ## File Location @@ -37,6 +37,41 @@ data/ The `{name}` must match the `name` field inside the file. +## Design Principle: Profiles Over Files + +A **single environment definition** represents one vulnerable service. It contains **one set of software versions** and **multiple configuration profiles** that describe different runtime states of that service. + +**Why profiles instead of separate files?** +- **DRY**: Software versions are defined once, not duplicated across files +- **Discoverability**: All variants of a service live in one file +- **Relationship clarity**: `http-stopped` is explicitly a profile of `jenkins`, not a separate service +- **Maintainability**: Adding a new version requires editing one file, not N files + +**Version strings represent actual software versions.** They are never suffixed to indicate configuration variants. The `profile` key selects the runtime configuration. + +## Three-Level Configuration Hierarchy + +When `test_env build` resolves an environment, it merges configuration in this order: + +``` +Level 1: Base shared (all profiles inherit) + ↓ +Level 2: Profile-specific overrides + ↓ +Level 3: Module-level overrides (minor tweaks) +``` + +This gives maximum reusability while allowing precise per-module customization. + +## Decision Matrix: Profile vs. Module Override + +| Scenario | Approach | Example | +|----------|----------|---------| +| Different runtime state (services on/off, different health check type) | **New profile** | `default` (HTTP on) vs `http-stopped` (HTTP off) | +| Different health check endpoint or expected status | **Module override** | Same profile, module overrides `health_check.path` | +| Different datastore default | **Module override** | Same profile, module overrides `datastore_defaults.TARGETURI` | +| Different credentials | **Module override** | Same profile, module overrides `credentials.default` | + ## Schema ### Top-Level Keys @@ -45,8 +80,9 @@ The `{name}` must match the `name` field inside the file. |-----|------|----------|-------------| | `name` | String | Yes | Machine-friendly identifier (matches filename) | | `description` | String | Yes | Human-readable description | -| `versions` | Hash | Yes | Map of version strings to configurations | -| `shared` | Hash | Yes | Configuration shared across all versions | +| `versions` | Hash | Yes | Map of version strings to image configurations | +| `shared` | Hash | Yes | Base configuration inherited by all profiles | +| `profiles` | Hash | Yes | Map of profile names to profile-specific overrides | ### versions Section @@ -70,6 +106,8 @@ versions: ### shared Section +Base configuration inherited by all profiles. Any field here can be overridden by a profile or by module-level metadata. + #### ports (Required) ```yaml shared: @@ -77,7 +115,7 @@ shared: http: 8080 ``` -#### health_check (Required) +#### health_check (Required in base or profile) ```yaml shared: health_check: @@ -116,6 +154,15 @@ shared: TARGETURI: /script ``` +#### volumes (Optional) +```yaml +shared: + volumes: + jenkins_home: + container_path: /var/jenkins_home + persist: false +``` + #### ci (Optional) ```yaml shared: @@ -132,57 +179,121 @@ shared: timeout: 120 ``` +### profiles Section + +Each profile is a key-value pair: +- **Key**: Profile name (e.g., `default`, `http-stopped`) +- **Value**: Hash that overrides or extends `shared` + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `description` | String | Yes | What this profile represents | +| `health_check` | Hash | No | Overrides base `shared.health_check` | +| `datastore_defaults` | Hash | No | Overrides base `shared.datastore_defaults` | +| `credentials` | Hash | No | Overrides base `shared.credentials` | +| `volumes` | Hash | No | Overrides base `shared.volumes` | +| `ci` | Hash | No | Overrides base `shared.ci` | + +**Profile names** must match `[a-z0-9-]+`. + ## Validation Rules 1. `name` must match filename (without `.yml`) 2. `versions` must have at least one entry 3. Each version must have an `image` 4. `shared.ports` must have at least one entry -5. `shared.health_check` must have valid `type` -6. If `type` is `http`, `path` is required -7. If `type` is `command`, `command` and `expected_output` are required +5. `profiles` must have at least one entry +6. `profiles` must contain a `default` profile +7. Profile names must match `[a-z0-9-]+` +8. `health_check` must be defined in `shared` or in every profile +9. Module-level `overrides` are deep-merged into the final profile config -## Loader Implementation (Week 3) +## Resolution & Merge Logic -```ruby -class EnvironmentDefinitionLoader - DEFINITIONS_PATH = File.join(Msf::Config.data_directory, 'vuln_envs') - - def self.load(name) - require 'yaml' - path = File.join(DEFINITIONS_PATH, "#{name}.yml") - raise "Definition not found: #{path}" unless File.exist?(path) - - begin - YAML.safe_load(File.read(path), permitted_classes: [Symbol]) - rescue Psych::SyntaxError => e - raise "Invalid YAML in #{path}: #{e.message}" - end - end - - def self.available_definitions - Dir.glob(File.join(DEFINITIONS_PATH, '*.yml')).map do |f| - File.basename(f, '.yml') - end.sort - end -end -``` +When `test_env build` resolves an environment definition, the loader performs a **three-level deep merge**: -## Integration With Registry +### Merge Order -Environment definitions are loaded by the plugin and used to: -1. Build/pull container images -2. Map container ports to host ports -3. Configure health checks -4. Set module datastore defaults +| Level | Source | What It Contains | +|-------|--------|------------------| +| 1 | `shared` | Base configuration inherited by all profiles | +| 2 | `profiles[profile_name]` | Profile-specific overrides (minus `description`) | +| 3 | `VulnEnv['overrides']` | Module-level tweaks | -See [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/03-database-schema.md) for registry design. +### Merge Rules + +- **Hash fields** (e.g., `health_check`, `datastore_defaults`) are deep-merged: nested keys are combined, not replaced wholesale. +- **Scalar fields** (e.g., `image`, `build_args`) are replaced by the higher level. +- **The `description` key** in a profile is informational only and is excluded from the merge. + +### Resolution Steps + +1. Load the YAML definition file by `name`. +2. Validate that `versions` contains the requested `version`. +3. Validate that `profiles` contains the requested `profile` (default: `'default'`). +4. Start with a copy of `shared`. +5. Deep-merge the selected profile's configuration into it. +6. Deep-merge the module's `VulnEnv['overrides']` (if any). +7. Attach the version-specific `image` and `build_args` from `versions[version]`. + +### Error Cases + +| Condition | Error | +|-----------|-------| +| Definition file not found | `"Definition not found: data/vuln_envs/{name}.yml"` | +| Version not in `versions` | `"Version '{version}' not defined for '{name}'"` | +| Profile not in `profiles` | `"Profile '{profile}' not defined for '{name}'"` | +| No `default` profile exists | Validation fails at load time | + +### Loader Interface (Week 3) + +The `EnvironmentDefinitionLoader` exposes: + +- `load(name)` — Parse and validate a definition file. +- `resolve(name, version, profile, overrides)` — Return the fully merged configuration for a specific environment instance. +- `available_definitions` — List all `.yml` files in `data/vuln_envs/`. +## Module Metadata Integration -## Reference: jenkins.yml +Modules reference a definition and optionally a profile. See [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/02-module-metadata.md) for the full `VulnEnv` schema. + +```ruby +# Standard module — uses default profile +'VulnEnv' => { + 'definition' => 'jenkins', + 'default_version' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' } +} + +# Variant module — uses http-stopped profile +'VulnEnv' => { + 'definition' => 'jenkins', + 'profile' => 'http-stopped', + 'default_version' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' } +} + +# Module with minor override — same profile, different health check +'VulnEnv' => { + 'definition' => 'jenkins', + 'default_version' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' }, + 'overrides' => { + 'health_check' => { + 'path' => '/script', + 'expected_status' => 403 + }, + 'datastore_defaults' => { + 'TARGETURI' => '/script' + } + } +} +``` + +## Reference Implementation: jenkins.yml ```yaml name: jenkins -description: Jenkins CI server with Groovy Script Console enabled +description: Jenkins CI server with Groovy Script Console versions: "2.361": @@ -203,14 +314,6 @@ shared: container_path: /var/jenkins_home persist: false - health_check: - type: http - path: /login - expected_status: 200 - interval: 5 - timeout: 2 - retries: 12 - credentials: default: username: admin @@ -230,4 +333,38 @@ shared: session_type: meterpreter expected_output: "uid=" timeout: 120 + +profiles: + default: + description: Standard Jenkins with HTTP enabled + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 + + http-stopped: + description: Jenkins with HTTP stopped for config-drop exploit + health_check: + type: tcp + port: 8080 + interval: 5 + timeout: 2 + retries: 12 ``` + +## Integration With Registry + +Environment definitions are loaded by the plugin and used to: +1. Build/pull container images +2. Map container ports to host ports +3. Configure health checks +4. Set module datastore defaults +5. Apply profile-specific overrides +6. Apply module-level overrides (if any) + +See [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/03-database-schema.md) for registry design. + + From edda9962230d59feada11b40e586da1885f9d2e4 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sun, 5 Jul 2026 02:58:08 +0300 Subject: [PATCH 14/27] Enhance CI workflow --- docs/test_env/ci_workflow.md | 48 ++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/docs/test_env/ci_workflow.md b/docs/test_env/ci_workflow.md index f4c9a90a137af..5123916508db5 100644 --- a/docs/test_env/ci_workflow.md +++ b/docs/test_env/ci_workflow.md @@ -1,7 +1,5 @@ # CI Workflow: Automated Exploit Verification - - This document defines how `test_env` will be used in GitHub Actions to automatically: - Provision vulnerable environments from shared definitions - Execute exploits with pre-configured datastore options @@ -10,10 +8,19 @@ This document defines how `test_env` will be used in GitHub Actions to automatic **Key principle:** CI consumes the same environment definitions used for local testing. No duplicated container configuration. +## Trigger Strategy + +Environment provisioning and exploit execution are time-consuming operations. To balance validation coverage with CI resource usage, this workflow runs on a **weekly schedule** for regression detection, **on-demand via maintainer-applied labels** for PR validation, and **manually** for debugging. It does not run on every push or pull request. + +| Trigger | When It Runs | Purpose | +|---------|--------------|---------| +| Weekly schedule (`cron: '0 0 * * 0'`) | Every Sunday at midnight UTC | Catch environment bit-rot and upstream image changes | +| Label (`vuln-env-test`) | When a maintainer applies the label to a PR | Validate PRs that modify environment definitions or exploit modules | +| `workflow_dispatch` | Manual button click in GitHub UI | Debugging or pre-release checks | ## Directory Structure -This directory structure will created as part of the project: +This directory structure will be created as part of the project: ``` metasploit-framework/ @@ -33,9 +40,9 @@ metasploit-framework/ └── ci_workflow.md ``` +## Resource Scripts - -A **resource script** with a `.rc` extension that contains msfconsole commands. Instead of typing commands one by one into msfconsole, they will be saved in a file and run: +A **resource script** with a `.rc` extension contains msfconsole commands. Instead of typing commands one by one into msfconsole, they are saved in a file and run: ```bash ./msfconsole -q -r path/to/script.rc @@ -43,8 +50,6 @@ A **resource script** with a `.rc` extension that contains msfconsole commands. Metasploit reads the file and executes each line automatically, as if it's typed. - - ### Example: ci/test_jenkins.rc **What it is:** A text file containing msfconsole commands to test the Jenkins module automatically. @@ -73,11 +78,10 @@ exit ```bash ./msfconsole -q -r ci/test_jenkins.rc ``` ---- ## GitHub Actions Workflow -**What this is:** A YAML file that tells GitHub Actions what to do on every push or pull request. +**What this is:** A YAML file that tells GitHub Actions what to do. **File:** `.github/workflows/vuln-env-test.yml` @@ -86,16 +90,28 @@ exit name: Vulnerable Environment Test on: - push: - branches: [ main ] + # Weekly scheduled run: catches environment bit-rot and upstream image changes + schedule: + - cron: '0 0 * * 0' # Every Sunday at midnight UTC + + # Manual trigger: for debugging or pre-release validation + workflow_dispatch: + + # Label-triggered: maintainers add 'vuln-env-test' label to PRs that modify + # environment definitions or exploit modules pull_request: - branches: [ main ] + types: [labeled] jobs: test-jenkins: name: Test Jenkins Script Console runs-on: ubuntu-latest + # Skip PRs without the vuln-env-test label + if: | + github.event_name != 'pull_request' || + contains(github.event.pull_request.labels.*.name, 'vuln-env-test') + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -148,8 +164,6 @@ jobs: | Verify session | `grep "Session.*opened"` | Confirm exploit succeeded | | Verify cleanup | `docker ps -q` | Confirm no leaked containers | ---- - ## Validation Criteria | Step | Expected Result | How It Is Checked | On Failure | @@ -159,8 +173,6 @@ jobs: | `test_env remove-all` | All containers removed | `docker ps -q` returns empty | Workflow fails | | Post-cleanup | Zero `msf.vulnenv` containers remain | `docker ps -a --filter "label=msf.vulnenv.managed_by=test_env"` returns empty | Workflow fails | ---- - ## CI Metadata in Environment Definitions Environment definitions include a `ci` section so the automation knows what payload to use and what to validate: @@ -173,7 +185,6 @@ ci: options: LHOST: 127.0.0.1 LPORT: 4444 - TARGETURI: /script validation: expected_session: true session_type: meterpreter @@ -186,9 +197,8 @@ ci: | Field | Type | Required | Description | |-------|------|----------|-------------| | `ci.exploit.payload` | String | Yes | Payload to use for automated execution | -| `ci.exploit.options` | Hash | No | Datastore options: `LHOST`, `LPORT`, `TARGETURI`, etc. | +| `ci.exploit.options` | Hash | No | Payload/handler options: `LHOST`, `LPORT` | | `ci.validation.expected_session` | Boolean | Yes | Whether a session must be created | | `ci.validation.session_type` | String | No | Expected session type: `meterpreter`, `shell` | | `ci.validation.expected_output` | String | No | Substring to verify in session output | | `ci.validation.timeout` | Integer | Yes | Max seconds to wait for validation | - From 6d82b63c38a737733dd081d1f42b2252b277b4fe Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sun, 5 Jul 2026 03:19:08 +0300 Subject: [PATCH 15/27] Remove unnecessary codes --- .../architecture/05-runtime-adapter.md | 257 +----------------- 1 file changed, 8 insertions(+), 249 deletions(-) diff --git a/docs/test_env/architecture/05-runtime-adapter.md b/docs/test_env/architecture/05-runtime-adapter.md index 2e3c572caed37..e3d81ca6e0879 100644 --- a/docs/test_env/architecture/05-runtime-adapter.md +++ b/docs/test_env/architecture/05-runtime-adapter.md @@ -135,194 +135,6 @@ class BaseRuntime end ``` -### Docker Implementation - -```ruby -class DockerRuntime < BaseRuntime - def available? - system('docker version > /dev/null 2>&1') - end - - def name; 'docker'; end - - def pull(image) - system("docker pull #{image}") - $? == 0 - end - - def run(image:, ports:, labels:, volumes: [], env: {}, name: nil) - cmd = ['docker', 'run', '-d'] - - # Port mappings: -p 127.0.0.1:HOST:CONTAINER - ports.each do |host_port, container_port| - cmd += ['-p', "127.0.0.1:#{host_port}:#{container_port}"] - end - - # Labels: --label key=value - labels.each do |k, v| - cmd += ['--label', "#{k}=#{v}"] - end - - # Volumes: -v HOST:CONTAINER - volumes.each do |host_path, container_path| - cmd += ['-v', "#{host_path}:#{container_path}"] - end - - # Environment: -e KEY=VALUE - env.each do |k, v| - cmd += ['-e', "#{k}=#{v}"] - end - - # Name: --name - cmd += ['--name', name] if name - - cmd << image - - output = `#{cmd.join(' ')} 2>&1` - if $? == 0 - output.strip # container ID - else - raise "Docker run failed: #{output}" - end - end - - def inspect(container_id) - json = `docker inspect #{container_id} 2>/dev/null` - return nil if json.empty? - - data = JSON.parse(json) - data.first - rescue JSON::ParserError - nil - end - - def stop(container_id) - system("docker stop #{container_id} > /dev/null 2>&1") - end - - def start(container_id) - system("docker start #{container_id} > /dev/null 2>&1") - end - - def remove(container_id) - system("docker rm #{container_id} > /dev/null 2>&1") - end - - def exec(container_id, command) - output = `docker exec #{container_id} #{command} 2>&1` - [output, $?.exitstatus] - end - - def list(filters: {}) - cmd = ['docker', 'ps', '-a', '--format', '{{json .}}'] - - filters.each do |k, v| - cmd += ['--filter', "#{k}=#{v}"] - end - - output = `#{cmd.join(' ')} 2>/dev/null` - output.lines.map { |l| JSON.parse(l) } - rescue JSON::ParserError - [] - end -end -``` - -### Podman Implementation - -```ruby -class PodmanRuntime < BaseRuntime - def available? - system('podman version > /dev/null 2>&1') - end - - def name; 'podman'; end - - # Identical to DockerRuntime except 'podman' instead of 'docker' - # All CLI flags are the same for the operations we need - def run(image:, ports:, labels:, volumes: [], env: {}, name: nil) - cmd = ['podman', 'run', '-d'] - - ports.each do |host_port, container_port| - cmd += ['-p', "127.0.0.1:#{host_port}:#{container_port}"] - end - - labels.each do |k, v| - cmd += ['--label', "#{k}=#{v}"] - end - - volumes.each do |host_path, container_path| - cmd += ['-v', "#{host_path}:#{container_path}"] - end - - env.each do |k, v| - cmd += ['-e', "#{k}=#{v}"] - end - - cmd += ['--name', name] if name - cmd << image - - output = `#{cmd.join(' ')} 2>&1` - if $? == 0 - output.strip - else - raise "Podman run failed: #{output}" - end - end - - # inspect, stop, start, remove, exec, list identical to DockerRuntime - # with 'podman' instead of 'docker' -end -``` - -## Port Allocation - -```ruby -class PortAllocator - EPHEMERAL_START = 49152 - EPHEMERAL_END = 65535 - - def initialize(used_ports = []) - @used_ports = Set.new(used_ports) - end - - def allocate(preferred = nil) - # 1. Try user-requested port first - if preferred && available?(preferred) - @used_ports.add(preferred) - return preferred - end - - # 2. Fall back to ephemeral range - (EPHEMERAL_START..EPHEMERAL_END).each do |port| - next if @used_ports.include?(port) - if available?(port) - @used_ports.add(port) - return port - end - end - - raise "No available ports in range #{EPHEMERAL_START}-#{EPHEMERAL_END}" - end - - def release(port) - @used_ports.delete(port) - end - - private - - def available?(port) - return false if @used_ports.include?(port) - - server = TCPServer.new('127.0.0.1', port) - server.close - true - rescue Errno::EADDRINUSE - false - end -end -``` - ## Container Label Schema All containers created by test_env receive these labels: @@ -348,14 +160,6 @@ All containers created by test_env receive these labels: ## Auto-Detection Strategy -```ruby -def self.detect - return DockerRuntime.new if DockerRuntime.available? - return PodmanRuntime.new if PodmanRuntime.available? - nil -end -``` - ## How test_env build Handles Port Conflicts ### Problem @@ -375,60 +179,15 @@ The `PortAllocator` class above handles this by: 2. If not, scanning ephemeral range for available port 3. Tracking used ports to avoid duplicates -### Integration in test_env build - -```ruby -def cmd_test_env_build(args) - # ... get module, VulnEnv config, definition ... - - shared = definition['shared'] - port_mapping = vuln_env['port_mapping'] # {8080 => 'RPORT'} - - # Get used ports from registry - used = registry.used_ports - - # Allocate ports - allocator = PortAllocator.new(used) - allocated_ports = {} - - shared['ports'].each do |name, container_port| - # Check if user specified a port override: RPORT=8081 - preferred = nil - if datastore_option = port_mapping[container_port] - preferred = args.find { |a| a.start_with?("#{datastore_option}=") }&.split('=')&.last&.to_i - end - - # Allocate (falls back automatically if preferred is taken) - host_port = allocator.allocate(preferred) - allocated_ports[container_port] = host_port - end - - # Start container with allocated ports - container_id = runtime.run( - image: env_config['image'], - ports: allocated_ports # {80 => 49152} - ) - - # Report actual ports to user - allocated_ports.each do |container_port, host_port| - if datastore_option = port_mapping[container_port] - if host_port != (preferred || container_port) - print_status("Port #{preferred || container_port} unavailable, using #{host_port}") - end - print_status("Mapped container:#{container_port} -> host:#{host_port} (#{datastore_option})") - end - end - - # Auto-set datastore options - allocated_ports.each do |container_port, host_port| - if datastore_option = port_mapping[container_port] - mod.datastore[datastore_option] = host_port - print_status("Set #{datastore_option} = #{host_port}") - end - end -end -``` +When `test_env build` runs, it: +- Reads the vulnerability environment's `port_mapping` (e.g., `{8080 => 'RPORT'}`) +- Checks if the user passed an override like `RPORT=8081` +- Allocates each required port, falling back to the ephemeral range if the preferred port is taken +- Starts the container with the allocated mappings +- Reports actual ports to the user +- Auto-sets the corresponding module datastore options +- ### Key Design Principles | Principle | Implementation | From 1c069704f272eeae991405c1de0804aa0a815780 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Mon, 6 Jul 2026 17:03:14 +0300 Subject: [PATCH 16/27] Remove duplicate docs --- docs/ci_workflow.md | 194 -------------------------- docs/reference_modules.md | 48 ------- docs/workflow.md | 280 -------------------------------------- 3 files changed, 522 deletions(-) delete mode 100644 docs/ci_workflow.md delete mode 100644 docs/reference_modules.md delete mode 100644 docs/workflow.md diff --git a/docs/ci_workflow.md b/docs/ci_workflow.md deleted file mode 100644 index f4c9a90a137af..0000000000000 --- a/docs/ci_workflow.md +++ /dev/null @@ -1,194 +0,0 @@ -# CI Workflow: Automated Exploit Verification - - - -This document defines how `test_env` will be used in GitHub Actions to automatically: -- Provision vulnerable environments from shared definitions -- Execute exploits with pre-configured datastore options -- Validate expected outcomes (session creation, command output) -- Clean up all containers to prevent resource leaks - -**Key principle:** CI consumes the same environment definitions used for local testing. No duplicated container configuration. - - -## Directory Structure - -This directory structure will created as part of the project: - -``` -metasploit-framework/ -├── ci/ -│ ├── test_activemq.rc -│ ├── test_jenkins.rc -│ └── test_drupal.rc -├── .github/ -│ └── workflows/ -│ └── vuln-env-test.yml -├── data/ -│ └── vuln_envs/ -│ ├── activemq.yml -│ ├── jenkins.yml -│ └── drupal.yml -└── docs/ - └── ci_workflow.md -``` - - - -A **resource script** with a `.rc` extension that contains msfconsole commands. Instead of typing commands one by one into msfconsole, they will be saved in a file and run: - -```bash -./msfconsole -q -r path/to/script.rc -``` - -Metasploit reads the file and executes each line automatically, as if it's typed. - - - -### Example: ci/test_jenkins.rc - -**What it is:** A text file containing msfconsole commands to test the Jenkins module automatically. - -**What it contains:** -```text -load test_env -use exploit/multi/http/jenkins_script_console -test_env build VERSION=2.361 -test_env exec 1 -test_env remove-all -exit -``` - -**What each line does:** -| Line | Command | Purpose | -|------|---------|---------| -| 1 | `load test_env` | Load the test_env plugin | -| 2 | `use exploit/multi/http/jenkins_script_console` | Select the exploit module | -| 3 | `test_env build VERSION=2.361` | Build environment using Jenkins version 2.361 | -| 4 | `test_env exec 1` | Execute exploit against environment ID 1 | -| 5 | `test_env remove-all` | Stop and remove all containers | -| 6 | `exit` | Close msfconsole | - -**How to run it manually (for testing):** -```bash -./msfconsole -q -r ci/test_jenkins.rc -``` ---- - -## GitHub Actions Workflow - -**What this is:** A YAML file that tells GitHub Actions what to do on every push or pull request. - -**File:** `.github/workflows/vuln-env-test.yml` - -**What it contains:** -```yaml -name: Vulnerable Environment Test - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test-jenkins: - name: Test Jenkins Script Console - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2' - bundler-cache: true - - - name: Set up Docker - uses: docker/setup-buildx-action@v3 - - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Run Jenkins exploit test - run: | - ./msfconsole -q -r ci/test_jenkins.rc - - - name: Verify session was created - run: | - grep "Session.*opened" ~/.msf4/logs/framework.log || echo "WARNING: No session log found" - - - name: Verify no containers left behind - run: | - REMAINING=$(docker ps -q | wc -l) - if [ "$REMAINING" -eq 0 ]; then - echo "Clean: No containers remaining" - else - echo "FAIL: $REMAINING container(s) still running" - docker ps - exit 1 - fi -``` - -**What each step does:** -| Step | Action | Purpose | -|------|--------|---------| -| Checkout | `actions/checkout@v4` | Download your code | -| Set up Ruby | `ruby/setup-ruby@v1` | Install Ruby 3.2 and gems | -| Set up Docker | `docker/setup-buildx-action@v3` | Install Docker | -| Cache Docker layers | `actions/cache@v3` | Speed up image pulls | -| Run exploit test | `./msfconsole -q -r ci/test_jenkins.rc` | Execute the Jenkins resource script | -| Verify session | `grep "Session.*opened"` | Confirm exploit succeeded | -| Verify cleanup | `docker ps -q` | Confirm no leaked containers | - ---- - -## Validation Criteria - -| Step | Expected Result | How It Is Checked | On Failure | -|------|----------------|-------------------|------------| -| `test_env build` | Container starts, health check passes | Console output contains "Environment ready" | Workflow fails | -| `test_env exec 1` | Session opens | `framework.log` contains "Session.*opened" | Workflow fails | -| `test_env remove-all` | All containers removed | `docker ps -q` returns empty | Workflow fails | -| Post-cleanup | Zero `msf.vulnenv` containers remain | `docker ps -a --filter "label=msf.vulnenv.managed_by=test_env"` returns empty | Workflow fails | - ---- - -## CI Metadata in Environment Definitions - -Environment definitions include a `ci` section so the automation knows what payload to use and what to validate: - -```yaml -# data/vuln_envs/jenkins.yml -ci: - exploit: - payload: java/meterpreter/reverse_tcp - options: - LHOST: 127.0.0.1 - LPORT: 4444 - TARGETURI: /script - validation: - expected_session: true - session_type: meterpreter - expected_output: "uid=" - timeout: 120 -``` - -### Schema Fields - -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `ci.exploit.payload` | String | Yes | Payload to use for automated execution | -| `ci.exploit.options` | Hash | No | Datastore options: `LHOST`, `LPORT`, `TARGETURI`, etc. | -| `ci.validation.expected_session` | Boolean | Yes | Whether a session must be created | -| `ci.validation.session_type` | String | No | Expected session type: `meterpreter`, `shell` | -| `ci.validation.expected_output` | String | No | Substring to verify in session output | -| `ci.validation.timeout` | Integer | Yes | Max seconds to wait for validation | - diff --git a/docs/reference_modules.md b/docs/reference_modules.md deleted file mode 100644 index eb8d7e5e96516..0000000000000 --- a/docs/reference_modules.md +++ /dev/null @@ -1,48 +0,0 @@ -# Reference Modules for test_env - -## Selection Criteria -- Cover different service types: Java message broker (ActiveMQ), CI server (Jenkins), CMS (Drupal) -- Have clear, single-port (or well-defined multi-port) mappings -- Have existing Docker images with known vulnerable versions -- Demonstrate different health check patterns (API endpoint, login page, root page) -- Include both authenticated and unauthenticated exploit scenarios - ---- - -## Module 1: Apache ActiveMQ Jolokia RCE (Mentor Suggested) -- **Path:** `exploit/multi/http/apache_activemq_jolokia_rce` -- **Type:** Java web application (JMX-over-HTTP) -- **Ports:** 8161 (web console / Jolokia API), 61616 (OpenWire broker) -- **Health Check:** HTTP GET `/api/jolokia/` expecting 200, or GET `/` expecting 200 -- **Why:** h00die suggested PR #21497. Has a verified Docker one-liner. Real-world CVE-2026-34197. -- **VulnEnv Definition:** `activemq` -- **Docker Image:** `apache/activemq-classic:5.18.6` -- **Docker Run:** `docker run -d --name activemq -p 8161:8161 -p 61616:61616 apache/activemq-classic:5.18.6` -- **Credentials:** admin / admin -- **Exploit Context:** Requires authenticated Jolokia access; `TARGETURI` typically `/api/jolokia/` - ---- - -## Module 2: Jenkins Script Console -- **Path:** `exploit/multi/http/jenkins_script_console` -- **Type:** Web application / CI server -- **Port:** 8080 -- **Health Check:** HTTP GET `/login` expecting 200 -- **Why:** Well-documented, multiple versions exist, clear RPORT→8080 mapping, widely used in exploit development tutorials -- **VulnEnv Definition:** `jenkins` -- **Docker Image:** `vulnhub/jenkins:2.361` -- **Credentials:** admin / admin -- **Exploit Context:** Script Console at `/script` allows Groovy execution; `TARGETURI` typically `/script` - ---- - -## Module 3: Drupal Drupalgeddon2 -- **Path:** `exploit/unix/webapp/drupal_drupalgeddon2` -- **Type:** Web application / CMS -- **Port:** 80 -- **Health Check:** HTTP GET `/` expecting 200 -- **Why:** Simple single-port setup, unauthenticated exploit, different architecture from ActiveMQ/Jenkins, large community interest -- **VulnEnv Definition:** `drupal` -- **Docker Image:** `vulnhub/drupal:CVE-2018-7600` -- **Credentials:** None required (unauthenticated) -- **Exploit Context:** SA-CORE-2018-002 (CVE-2018-7600); remote code execution via form API diff --git a/docs/workflow.md b/docs/workflow.md deleted file mode 100644 index fd04ab6163fff..0000000000000 --- a/docs/workflow.md +++ /dev/null @@ -1,280 +0,0 @@ -# test_env User Workflow (Design Specification) - -This document specifies the intended user interaction with `test_env`. Every transcript below is the **target behavior** that implementation must achieve. These are our acceptance criteria. - ---- - -## Scenario 1: Build an Environment for the Active Module - -**Precondition:** The user has selected a module that defines a `VulnEnv` key in its metadata. - -**Input:** -``` -msf > use exploit/multi/http/apache_activemq_jolokia_rce -msf exploit(apache_activemq_jolokia_rce) > test_env build -``` - -**Expected behavior:** -- The plugin detects the active module and reads `mod.info['VulnEnv']` -- It resolves the definition name (`activemq`) and loads `data/vuln_envs/activemq.yml` -- It selects the default version (`5.18.6`) -- It auto-detects the container runtime (Docker preferred, Podman fallback) -- It pulls `apache/activemq-classic:5.18.6` if not already cached -- It starts the container with localhost binding and dynamic port allocation -- It waits for the health check defined in the YAML (`GET /api/jolokia/` → 200) -- Once ready, it prints the mapped host port and suggests datastore settings - -**Expected output:** -``` -[*] Resolving environment for apache_activemq_jolokia_rce... -[*] Definition: activemq | Version: 5.18.6 | Runtime: docker -[*] Pulling image apache/activemq-classic:5.18.6... -[*] Starting container... -[*] Waiting for health check (GET /api/jolokia/)... -[+] Environment ready. -[*] RHOSTS => 127.0.0.1 -[*] RPORT => 49152 -[*] TARGETURI => /api/jolokia/ -[*] USERNAME => admin -[*] PASSWORD => admin -[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49152; exploit -``` - -**Postcondition:** A container is running. The module's datastore options (`RHOSTS`, `RPORT`, etc.) are automatically populated. The environment is registered in the in-memory registry with a unique ID. - ---- - -## Scenario 2: Build with a Specific Version - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 -``` - -**Expected behavior:** -- The `VERSION=2.375` argument overrides the `default_version` from the module's `VulnEnv` -- The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` -- If the version does not exist, the command fails immediately with a list of available versions - -**Expected output (success):** -``` -[*] Selected version: 2.375 -[*] Pulling image vulnhub/jenkins:2.375... -[*] Starting container... -[*] Waiting for health check (GET /login)... -[+] Environment ready. -[*] RHOSTS => 127.0.0.1 -[*] RPORT => 49153 -[*] TARGETURI => /script -[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49153; exploit -``` - -**Expected output (failure — version not found):** -``` -[-] Version '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 -``` - ---- - -## Scenario 3: Request a Specific Port (with Automatic Fallback) - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env build RPORT=8080 -``` - -**Expected behavior:** -- The plugin attempts to bind host port 8080 to the container's exposed port -- If 8080 is already in use on the host, the `PortAllocator` scans the ephemeral range (49152–65535) for an available port -- The user is informed of the fallback. The allocated port is stored in the registry. - -**Expected output:** -``` -[*] Requested port 8080 is unavailable. Using dynamically allocated port 49154. -[*] Starting container... -[*] Waiting for health check (GET /login)... -[+] Environment ready. -[*] RHOSTS => 127.0.0.1 -[*] RPORT => 49154 -[*] TARGETURI => /script -``` - ---- - -## Scenario 4: List Active Environments - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env list -``` - -**Expected behavior:** -- The plugin queries the in-memory registry and prints a table using `Rex::Ui::Text::Table` -- Each row shows: ID, container ID (truncated), module fullname, RHOST, RPORT, status, version -- The table is sorted by ID - -**Expected output:** -``` -Environments -============ - -ID Container Module RHOST RPORT Status Version --- --------- ------ ----- ----- ------ ------- -1 4f3a2b1c... exploit/multi/http/jenkins_script_console 127.0.0.1 49153 running 2.375 -2 9e8d7c6b... exploit/multi/http/apache_activemq_jolokia 127.0.0.1 49152 running 5.18.6 -``` - ---- - -## Scenario 5: Execute the Stored Exploit - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env exec 1 -``` - -**Expected behavior:** -- The plugin looks up environment ID 1 in the registry -- It verifies the container is running via `docker inspect` or `podman inspect` -- It automatically sets the module's datastore options (`RHOSTS`, `RPORT`, `TARGETURI`, etc.) from the registry record -- It invokes the module's `exploit` method (or `run_simple`) with these options -- If the environment is stopped, it prints an error telling the user to start it first - -**Expected output (success):** -``` -[*] Executing exploit against environment 1... -[*] Set RHOSTS 127.0.0.1 -[*] Set RPORT 49153 -[*] Set TARGETURI /script -[*] Started reverse TCP handler on 127.0.0.1:4444 -[+] Session 1 opened (127.0.0.1:4444 -> 127.0.0.1:49153) -``` - -**Expected output (failure — environment stopped):** -``` -[-] Environment 1 is not running. Start it with: test_env start 1 -``` - ---- - -## Scenario 6: Stop and Restart an Environment - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env stop 1 -``` - -**Expected behavior:** -- The plugin calls `docker stop` (or `podman stop`) on the container ID stored in the registry -- It updates the registry status to `stopped` -- It preserves the registry record so the environment can be restarted - -**Expected output:** -``` -[*] Stopping container 4f3a2b1c... -[+] Environment 1 stopped. -``` - -**Input (restart):** -``` -msf exploit(multi/http/jenkins_script_console) > test_env start 1 -``` - -**Expected behavior:** -- The plugin calls `docker start` on the container -- It re-runs the health check defined in the environment definition -- It updates the registry status to `running` - -**Expected output:** -``` -[*] Starting container 4f3a2b1c... -[*] Waiting for health check... -[+] Environment 1 running. RPORT=49153 -``` - ---- - -## Scenario 7: Remove a Single Environment - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env remove 1 -``` - -**Expected behavior:** -- The plugin stops the container if it is running -- It calls `docker rm` (or `podman rm`) to remove the container -- It removes the record from the in-memory registry -- If the database is active (Phase 2), it updates the DB status to `removed` - -**Expected output:** -``` -[*] Stopping container 4f3a2b1c... -[*] Removing container 4f3a2b1c... -[+] Environment 1 removed. -``` - ---- - -## Scenario 8: Remove All Environments - -**Input:** -``` -msf exploit(multi/http/jenkins_script_console) > test_env remove-all -``` - -**Expected behavior:** -- The plugin iterates all entries in the registry -- For each, it stops and removes the container -- It clears the in-memory registry -- If any container fails to stop/remove, it prints a warning but continues - -**Expected output:** -``` -[*] Tearing down 2 environment(s)... -[*] Stopping container 4f3a2b1c... -[*] Removing container 4f3a2b1c... -[*] Stopping container 9e8d7c6b... -[*] Removing container 9e8d7c6b... -[+] All environments removed. -``` - ---- - -## Quick Reference: Command Summary - -| Command | Arguments | Description | -|---------|-----------|-------------| -| `test_env build` | `[VERSION=x]` `[RPORT=y]` | Build and launch environment for active module | -| `test_env list` | none | Show all tracked environments | -| `test_env exec` | `` | Execute exploit against environment `` | -| `test_env stop` | `` | Stop a running environment | -| `test_env start` | `` | Restart a stopped environment | -| `test_env remove` | `` | Tear down and remove one environment | -| `test_env remove-all` | none | Tear down all environments | -| `test_env help` | none | Show usage information | - ---- - -## Error Handling Specification - -| Error Condition | Expected Output | Implementation Notes | -|-----------------|----------------|---------------------| -| No active module | `[-] No active module. Use 'use ' first.` | Check `driver.active_module` before any other logic | -| Module has no `VulnEnv` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnEnv']` after resolving active module | -| No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | -| Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | -| Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | -| No available ports | `[-] No available ports in range 49152-65535` | `PortAllocator` raises after exhausting range | -| Health check timeout | `[-] Health check timed out after seconds` | `HealthManager` exceeds `retries * interval` | -| Environment ID not found | `[-] Environment not found` | Registry lookup returns `nil` | -| Environment not running | `[-] Environment is not running. Start it with: test_env start ` | Check `status` field before `exec` | - ---- - -## Notes for Implementers - -- All output prefixes (`[*]`, `[+]`, `[-]`) must use `print_status`, `print_good`, `print_error` respectively -- Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` -- The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) -- Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost From 70d95553973a77faa31003838d1b759c0d977f2a Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Thu, 9 Jul 2026 04:56:13 +0300 Subject: [PATCH 17/27] fixing typo --- docs/test_env/architecture/01-command-dispatcher.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/test_env/architecture/01-command-dispatcher.md b/docs/test_env/architecture/01-command-dispatcher.md index 1d82184ef60b3..6c72c0fe1526b 100644 --- a/docs/test_env/architecture/01-command-dispatcher.md +++ b/docs/test_env/architecture/01-command-dispatcher.md @@ -29,7 +29,7 @@ When I type `test_env build`, this happens: From `lib/msf/ui/console/command_dispatcher/jobs.rb`, I saw: - One dispatcher can handle multiple commands via `commands` hash - `cmd_jobs(*args)` uses `args.shift` to get the subcommand -- `cmd_rename_job_tabs` provides tab completion +- ``cmd_jobs_tabs` provides tab completion - `cmd_jobs_help` prints usage information ## My Design: test_env Command Dispatcher From 45f02787cb03a6587ec0885a361dccdee598e678 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:57:57 +0300 Subject: [PATCH 18/27] Enhance doc for VulnerableEnvironment integration based on the mentor's review --- .../architecture/02-module-metadata.md | 341 ++++++++++++------ 1 file changed, 240 insertions(+), 101 deletions(-) diff --git a/docs/test_env/architecture/02-module-metadata.md b/docs/test_env/architecture/02-module-metadata.md index bcf8ed09fb41c..abb8e4bbbc9e0 100644 --- a/docs/test_env/architecture/02-module-metadata.md +++ b/docs/test_env/architecture/02-module-metadata.md @@ -1,8 +1,204 @@ # Module Metadata Integration +## How VulnerableEnvironment Is Defined + +`VulnerableEnvironment` is defined inside a module's `initialize` method, passed to `update_info()` alongside all standard metadata. This follows the exact same pattern as `Name`, `Description`, `Author`, `References`, `Notes`, etc. + + +## Framework Pattern for Reading Metadata + +Metasploit modules expose metadata through **public accessor methods** that read from the protected `module_info` hash. From `lib/msf/core/module/module_info.rb` lines 17–52: + +```ruby +def alias + module_info['Alias'] +end + +def description + module_info['Description'] +end + +def disclosure_date + date_str = Date.parse(module_info['DisclosureDate'].to_s) rescue nil +end + +def name + module_info['Name'] +end + +def notes + module_info['Notes'] +end +``` + +Each method is **public**, reads a single key from `module_info`, and returns the raw value. The framework does not validate or transform the return value at this layer — that responsibility lies with the consumer. + +## Plugin-Only Implementation + +Since the `test_env` plugin cannot modify core framework files, it provides its own accessor that mirrors the framework pattern. The accessor returns a **validated Struct** instead of a raw Hash, ensuring type safety and required-field enforcement. + +### VulnerableEnvironment Struct + +```ruby +module Msf + class Plugin::VulnEnv < Msf::Plugin + # Encapsulates and validates VulnerableEnvironment metadata from a module. + class VulnerableEnvironment + attr_reader :definition, :default_version, :profile, :port_mapping, :overrides + + # Required keys that must be present + REQUIRED_KEYS = %w[definition default_version port_mapping].freeze + + # Valid types for each key + SCHEMA = { + 'definition' => String, + 'default_version' => String, + 'profile' => String, + 'port_mapping' => Hash, + 'overrides' => Hash + }.freeze + + def initialize(raw_hash) + @raw = raw_hash || {} + + validate! + + @definition = @raw['definition'] + @default_version = @raw['default_version'] + @profile = @raw['profile'] || 'default' + @port_mapping = @raw['port_mapping'] || {} + @overrides = @raw['overrides'] || {} + end + + def valid? + errors.empty? + end + + def errors + errs = [] + + REQUIRED_KEYS.each do |key| + errs << "Missing required key: '#{key}'" unless @raw.key?(key) + end + + SCHEMA.each do |key, expected_type| + next unless @raw.key?(key) + unless @raw[key].is_a?(expected_type) + errs << "Key '#{key}' must be #{expected_type}, got #{@raw[key].class}" + end + end + + if @raw['port_mapping'].is_a?(Hash) + @raw['port_mapping'].each do |k, v| + unless k.is_a?(Integer) || k.to_s.match?(/\A\d+\z/) + errs << "port_mapping key must be an integer port, got: #{k.inspect}" + end + unless v.is_a?(String) + errs << "port_mapping value must be a String datastore option name, got: #{v.inspect}" + end + end + end + + errs + end + + private + + def validate! + errs = errors + raise ArgumentError, "Invalid VulnerableEnvironment: #{errs.join('; ')}" unless errs.empty? + end + end + end +end +``` + +### Plugin Accessor (Mirrors Framework Pattern) + +```ruby +class Plugin::VulnEnv < Msf::Plugin + class ConsoleCommandDispatcher + # Reads and validates VulnerableEnvironment metadata from the active module. + # Returns a VulnerableEnvironment Struct, or nil if the module has none. + # + # This mirrors the framework pattern used by #name, #description, #notes, etc. + # but adds validation and returns a typed object instead of a raw Hash. + def vulnerable_environment(mod) + return nil unless mod + + raw = mod.send(:module_info)['VulnerableEnvironment'] + return nil unless raw + + VulnerableEnvironment.new(raw) + rescue ArgumentError => e + print_error("Module has invalid VulnerableEnvironment: #{e.message}") + nil + end + + def cmd_test_env_build(args) + mod = driver.active_module + unless mod + print_error("No active module. Use 'use ' first.") + return + end + + env = vulnerable_environment(mod) + unless env + print_error("Module does not define a vulnerable environment configuration.") + return + end + + # env.definition => 'jenkins' + # env.default_version => '2.361' + # env.profile => 'default' + # env.port_mapping => {8080 => 'RPORT'} + # env.overrides => { ... } + + # ... + end + end +end +``` + +## Why This Approach + +| Concern | Resolution | +|---------|-----------| +| **Framework pattern alignment** | `vulnerable_environment` mirrors `name`, `description`, `notes` — public accessor reading from `module_info` | +| **Collision avoidance** | Full word `vulnerable_environment` instead of abbreviated `vuln_env`; key name `VulnerableEnvironment` instead of `VulnEnv` | +| **Type safety** | `VulnerableEnvironment` Struct validates keys and types at initialization | +| **No core modifications** | Plugin provides the accessor; no changes to `lib/msf/core/module/module_info.rb` required | +| **Future upstream path** | When proposing core integration, the same `VulnerableEnvironment` Struct and accessor can move to `module_info.rb` with minimal changes | + +## Future Core Integration Path + +When proposing upstream integration with `rapid7/metasploit-framework`, the following would be added to `lib/msf/core/module/module_info.rb`: + +```ruby +# Public accessor following the established pattern +def vulnerable_environment + module_info['VulnerableEnvironment'] +end +``` + +And optionally a `merge_info_vulnerableenvironment` method to hook into `merge_check_key`: + +```ruby +protected + +def merge_info_vulnerableenvironment(info, val) + # Deep-merge VulnerableEnvironment hashes when modules inherit + if info['VulnerableEnvironment'].is_a?(Hash) && val.is_a?(Hash) + info['VulnerableEnvironment'] = deep_merge(info['VulnerableEnvironment'], val) + else + info['VulnerableEnvironment'] = val + end +end +``` + ## What I Verified -I wrote and ran `test_final.rb` to verify how to access module metadata. Here is the test script I used: +I wrote `test_final.rb` to confirm that custom keys defined in `update_info()` persist in `module_info` and are readable at runtime by framework extensions. ```ruby #!/usr/bin/env ruby @@ -26,25 +222,25 @@ puts "Has Name? #{info.key?('Name')}" puts "Name: #{info['Name']}" puts "" -# Test: Can we add a custom key? -puts "=== Adding custom key ===" -info['VulnEnv'] = { - 'definition' => 'jenkins', +# Test: Can we add a custom key? (Proof of mechanism only) +puts "=== Adding custom key (runtime injection test) ===" +info['VulnerableEnvironment'] = { + 'definition' => 'jenkins', 'default_version' => '2.361', - 'port_mapping' => { 8080 => 'RPORT' } + 'port_mapping' => { 8080 => 'RPORT' } } -puts "Added VulnEnv" -puts "Has VulnEnv? #{info.key?('VulnEnv')}" -puts "VulnEnv: #{info['VulnEnv'].inspect}" +puts "Added VulnerableEnvironment" +puts "Has VulnerableEnvironment? #{info.key?('VulnerableEnvironment')}" +puts "VulnerableEnvironment: #{info['VulnerableEnvironment'].inspect}" puts "" # Test: Can we read it back? puts "=== Reading back ===" info2 = mod.send(:module_info) puts "Same object? #{info.equal?(info2)}" -puts "Has VulnEnv? #{info2.key?('VulnEnv')}" -puts "VulnEnv: #{info2['VulnEnv'].inspect}" +puts "Has VulnerableEnvironment? #{info2.key?('VulnerableEnvironment')}" +puts "VulnerableEnvironment: #{info2['VulnerableEnvironment'].inspect}" ``` ### Output I Got @@ -58,15 +254,15 @@ Keys count: 18 Has Name? true Name: Jenkins-CI Script-Console Java Execution -=== Adding custom key === -Added VulnEnv -Has VulnEnv? true -VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} +=== Adding custom key (runtime injection test) === +Added VulnerableEnvironment +Has VulnerableEnvironment? true +VulnerableEnvironment: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} === Reading back === Same object? true -Has VulnEnv? true -VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} +Has VulnerableEnvironment? true +VulnerableEnvironment: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} ``` ### What This Proves @@ -75,9 +271,10 @@ VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{ |------|--------| | `mod.send(:module_info)` returns a Hash | ✅ Yes | | The Hash contains standard keys like `Name` | ✅ Yes | -| I can write a custom key `VulnEnv` to it | ✅ Yes | -| The custom key persists when read back | ✅ Yes | -| It's the same object (not a copy) | ✅ Yes | +| Custom keys written during `update_info()` persist | ✅ Yes | +| The Hash is the same object (not a copy) | ✅ Yes | + +> **Note:** The runtime injection in the test script above is for verification only. Production modules must define `VulnerableEnvironment` inside `initialize` as shown in the first code block. ## Source Code Evidence @@ -94,89 +291,23 @@ attr_accessor :module_info - `mod.module_info` raises `NoMethodError` (protected method) - `mod.send(:module_info)` works (bypasses access control) -The framework itself uses `module_info` internally in `lib/msf/core/module.rb`: +The framework itself accesses `module_info` directly in `lib/msf/core/module/module_info.rb`: ```ruby -# Line 116 -self.module_info = info - -# Line 129-132 -self.author = Msf::Author.transform(merge_module_info_with_target_info(module_info, 'Author')) -self.arch = Rex::Transformer.transform(merge_module_info_with_target_info(module_info, 'Arch'), Array, [ String ], 'Arch') -``` - -And in `lib/msf/core/module/module_info.rb`: - -```ruby -# Line 41 def name module_info['Name'] end -``` -## The Correct Way to Access Module Metadata - -```ruby -# In plugin's command dispatcher: -def cmd_test_env_build(args) - # 1. Get active module from driver - mod = driver.active_module - raise "No active module. Use 'use ' first." unless mod - - # 2. Access module_info via send (protected accessor) - info = mod.send(:module_info) - - # 3. Read VulnEnv configuration - vuln_env = info['VulnEnv'] - raise "Module has no VulnEnv config" unless vuln_env - - # 4. Extract values - definition_name = vuln_env['definition'] # 'jenkins' - version = vuln_env['default_version'] # '2.361' - port_mapping = vuln_env['port_mapping'] # {8080 => 'RPORT'} - - # 5. Load YAML definition file - yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{definition_name}.yml") -end -``` - -## Why send(:module_info) Is Acceptable - -- `module_info` is **protected**, not private — meant for subclass/extension access -- Metasploit plugins are **framework extensions**, not external code -- The framework itself accesses `module_info` directly in the `ModuleInfo` mixin -- This is a **standard Ruby pattern** for working with protected framework internals - -## Alternative: Encapsulate in Helper Method (Is it better to do or not?) - -If It's not preferable not to use `send` directly everywhere: - -```ruby -class Plugin::VulnEnv < Msf::Plugin - class ConsoleCommandDispatcher - # Encapsulate the send call for clarity - def get_module_vuln_env(mod) - mod.send(:module_info)['VulnEnv'] - end - - def cmd_test_env_build(args) - mod = driver.active_module - vuln_env = get_module_vuln_env(mod) - # ... - end - end +def notes + module_info['Notes'] end ``` -## VulnEnv Schema +And in `lib/msf/core/module.rb`: ```ruby -'VulnEnv' => { - 'definition' => String, # e.g., 'jenkins' → data/vuln_envs/jenkins.yml - 'default_version' => String, # e.g., '2.361' - 'port_mapping' => Hash, # { container_port => 'RPORT' } - 'datastore_overrides' => Hash # optional: { 'TARGETURI' => '/script' } -} +self.module_info = info +self.author = Msf::Author.transform(merge_module_info_with_target_info(module_info, 'Author')) ``` ## Resolution Flow @@ -184,19 +315,22 @@ end ``` test_env build called ↓ -driver.active_module → Msf::Module instance +driver.active_module → Msf::Module instance (already initialized) ↓ -mod.send(:module_info)['VulnEnv'] → Hash or nil +vulnerable_environment(mod) → VulnerableEnvironment Struct or nil ↓ -if nil: print_error("Module has no VulnEnv configuration") +if nil: print_error("Module does not define a vulnerable environment configuration.") ↓ if present: - definition = vuln_env['definition'] # 'jenkins' - yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{definition}.yml") + env.definition => 'jenkins' + env.default_version => '2.361' + env.profile => 'default' + env.port_mapping => {8080 => 'RPORT'} + env.overrides => { ... } + ↓ + yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{env.definition}.yml") definition_data = YAML.load_file(yaml_path) - version = vuln_env['default_version'] # '2.361' - env_config = definition_data['versions'][version] - shared_config = definition_data['shared'] + env_config = loader.resolve(env.definition, env.default_version, env.profile, env.overrides) ``` ## Error Cases @@ -204,6 +338,11 @@ if present: | Condition | Error Message | |-----------|--------------| | No active module | "No active module. Use 'use ' first." | -| Module has no VulnEnv | "Module does not define a vulnerable environment configuration." | +| Module has no VulnerableEnvironment | "Module does not define a vulnerable environment configuration." | +| Invalid VulnerableEnvironment (missing required key) | "Module has invalid VulnerableEnvironment: Missing required key: 'definition'" | +| Invalid VulnerableEnvironment (wrong type) | "Module has invalid VulnerableEnvironment: Key 'port_mapping' must be Hash, got String" | +| Invalid port_mapping key | "port_mapping key must be an integer port, got: 'abc'" | +| Invalid port_mapping value | "port_mapping value must be a String datastore option name, got: 123" | | Definition file not found | "Environment definition not found: data/vuln_envs/{name}.yml" | | Version not found in definition | "Version '{version}' not defined for '{name}'" | +| Profile not found in definition | "Profile '{profile}' not defined for '{name}'" | From f987e48f51f3e2703db57f5032f32d26370f9f48 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:46:55 +0300 Subject: [PATCH 19/27] Adjust database schema for labels --- .../architecture/03-database-schema.md | 160 ++++++++++-------- 1 file changed, 93 insertions(+), 67 deletions(-) diff --git a/docs/test_env/architecture/03-database-schema.md b/docs/test_env/architecture/03-database-schema.md index 5af4324f037d0..1abc45bf73426 100644 --- a/docs/test_env/architecture/03-database-schema.md +++ b/docs/test_env/architecture/03-database-schema.md @@ -135,67 +135,52 @@ end Since in-memory data is lost on msfconsole restart, use **OCI container labels** to identify and reconstruct environments. -**Design Decision:** Hybrid approach — individual labels for filterable identity -fields, plus a base64-encoded JSON payload for complex metadata. +**Design Decision:** Use only lightweight individual labels for the minimal +dynamic data needed. All static metadata (credentials, datastore defaults, +health checks, exploit commands) is reconstructible from the module's +`VulnerableEnvironment` definition and the environment YAML file. -**Rationale:** Docker's `--filter` flag only supports string matching on -individual label values. We need native filtering for discovery (`managed_by`) -and session isolation (`instance_id`). However, complex nested data -(datastore hashes, credentials, exploit commands) is better packed into a -single atomic JSON blob to avoid partial label corruption and simplify -schema evolution. +**Rationale:** The second mentor correctly observed that if we can identify +the container by `managed_by` + `instance_id` + `module` labels, we can look +up the module's `VulnerableEnvironment` definition from `module_info`. The +definition contains `port_mapping`, `datastore_defaults`, `credentials`, and +`health_check`. The only truly dynamic data that cannot be reconstructed is: + +1. **Allocated port(s)** — dynamically assigned at runtime +2. **Environment version** — which image tag was provisioned +3. **Container runtime ID** — the actual Docker/Podman container ID + +**Label Schema:** + +| Label | Type | Value | Purpose | +|-------|------|-------|---------| +| `msf.vulnenv.managed_by` | String | `test_env` | Discovery filter — find all framework-managed containers | +| `msf.vulnenv.instance_id` | String | `msf-{hostname}-{pid}` | Session isolation — identify which msfconsole created it | +| `msf.vulnenv.module` | String | Module fullname | Module linkage — filter by target module | +| `msf.vulnenv.env_id` | String | `1` | Registry cross-reference — map to in-memory ID | +| `msf.vulnenv.version` | String | `2.361` | **Version used** — not reliably in image tag | +| `msf.vulnenv.ports` | String | `8081:8080` | **Allocated port mapping** — host:container, comma-separated | + +**Example:** ```bash docker run -d \ --label "msf.vulnenv.managed_by=test_env" \ - --label "msf.vulnenv.instance_id=msf-$(hostname)-$$" \ + --label "msf.vulnenv.instance_id=msf-hostname-12345" \ --label "msf.vulnenv.module=exploit/multi/http/jenkins_script_console" \ --label "msf.vulnenv.env_id=1" \ - --label "msf.vulnenv.payload=eyJkYXRhc3RvcmUiOnsiUkhPU1RTIjoiMTI3LjAuMC4xIiwiUlBPUlQiOjgwODF9LCJleHBsb2l0X2NvbW1hbmQiOiJzZXQgUkhPU1RTIDEyNy4wLjAuMTsgc2V0IFJQT1JUIDgwODE7IGV4cGxvaXQiLCJjcmVkZW50aWFscyI6eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9LCJ2ZXJzaW9uIjoiMi4zNjEiLCJjcmVhdGVkX2F0IjoiMjAyNC0wNi0yNVQxNzozNzowMFoiLCJydW50aW1lIjoiZG9ja2VyIn0=" \ + --label "msf.vulnenv.version=2.361" \ + --label "msf.vulnenv.ports=8081:8080" \ vulnhub/jenkins:2.361 ``` -**Label Schema:** +**Why no Base64 JSON payload?** -| Label | Type | Value | Purpose | -|-------|------|-------|---------| -| `msf.vulnenv.managed_by` | Individual | `test_env` | Discovery filter — find all framework-managed containers | -| `msf.vulnenv.instance_id` | Individual | `msf-{hostname}-{pid}` | Session isolation — identify which msfconsole created it | -| `msf.vulnenv.module` | Individual | Module fullname | Module linkage — filter by target module | -| `msf.vulnenv.env_id` | Individual | `1` | Registry cross-reference — map to in-memory ID | -| `msf.vulnenv.payload` | **Base64 JSON** | Encoded metadata blob | Complete state reconstruction | - -**Payload JSON Structure (before encoding):** -```json -{ - "datastore": {"RHOSTS": "127.0.0.1", "RPORT": 8081}, - "exploit_command": "set RHOSTS 127.0.0.1; set RPORT 8081; exploit", - "credentials": {"username": "admin", "password": "admin"}, - "version": "2.361", - "created_at": "2024-06-25T17:37:00Z", - "runtime": "docker" -} -``` - -**Encoding/Decoding Helpers:** -```ruby -require 'json' -require 'base64' - -def encode_payload(data) - Base64.strict_encode64(data.to_json) -end - -def decode_payload(encoded) - return nil unless encoded - - json = Base64.strict_decode64(encoded) - JSON.parse(json, symbolize_names: true) -rescue ArgumentError, JSON::ParserError => e - print_error("Failed to decode container payload: #{e.message}") - nil -end -``` +| Concern | Resolution | +|---------|-----------| +| Docker label size limit (~2KB total) | Lightweight labels stay well under limit | +| Schema evolution | Adding a new label is simpler than versioning a JSON schema | +| No encoding/decoding complexity | No Base64, no JSON parsing errors | ### State Reconstruction From Labels (Future Enhancement) @@ -203,41 +188,82 @@ end def reconstruct_from_labels(runtime) # Step 1: Discover all framework-managed containers via native filter containers = runtime.list(filters: { 'label' => 'msf.vulnenv.managed_by=test_env' }) - + containers.each do |container| labels = container['Config']['Labels'] || {} - + # Step 2: Skip containers from other msfconsole instances instance_id = labels['msf.vulnenv.instance_id'] next unless instance_id == current_instance_id - - # Step 3: Decode the payload for full metadata - payload = decode_payload(labels['msf.vulnenv.payload']) - next unless payload - - # Step 4: Rebuild registry entry + + # Step 3: Extract minimal dynamic data from labels + module_fullname = labels['msf.vulnenv.module'] env_id = labels['msf.vulnenv.env_id'].to_i + version = labels['msf.vulnenv.version'] + + # Parse port mapping: "8081:8080,9090:61616" -> {8080=>8081, 61616=>9090} + ports = parse_port_label(labels['msf.vulnenv.ports']) + + # Step 4: Load module and resolve its VulnerableEnvironment definition + mod = framework.modules.create(module_fullname) + next unless mod + + vuln_env_meta = mod.send(:module_info)['VulnerableEnvironment'] + next unless vuln_env_meta + + definition_name = vuln_env_meta['definition'] + profile = vuln_env_meta['profile'] || 'default' + overrides = vuln_env_meta['overrides'] || {} + + # Step 5: Resolve environment config from YAML + loader = EnvironmentDefinitionLoader.new(Msf::Config.data_directory) + config = loader.resolve(definition_name, version, profile, overrides) + + # Step 6: Build datastore from port_mapping + allocated ports + datastore = { 'RHOSTS' => '127.0.0.1' } + vuln_env_meta['port_mapping'].each do |container_port, ds_option| + datastore[ds_option] = ports[container_port] + end + + # Step 7: Reconstruct registry entry @environments[env_id] = { local_id: env_id, container_id: container['Id'], - module_fullname: labels['msf.vulnenv.module'], - env_version: payload[:version], - rhost: payload[:datastore]['RHOSTS'], - rport: payload[:datastore]['RPORT'], - runtime: payload[:runtime], + module_fullname: module_fullname, + env_version: version, + rhost: '127.0.0.1', + rport: ports.values.first, + runtime: runtime.name, image_ref: container['Config']['Image'], status: container['State']['Status'], - exploit_command: payload[:exploit_command], - datastore: payload[:datastore], - created_at: Time.parse(payload[:created_at]), + exploit_command: build_exploit_command(datastore), + datastore: datastore, + created_at: Time.parse(container['Created']), started_at: Time.parse(container['State']['StartedAt']) } - + @next_id = [@next_id, env_id + 1].max end end + +# Parse "8081:8080,9090:61616" into {8080=>8081, 61616=>9090} +def parse_port_label(label_value) + return {} unless label_value + + label_value.split(',').each_with_object({}) do |pair, hash| + host_port, container_port = pair.split(':') + hash[container_port.to_i] = host_port.to_i + end +end + +# Build exploit command from datastore +def build_exploit_command(datastore) + cmds = datastore.map { |k, v| "set #{k} #{v}" } + cmds.join('; ') + '; exploit' +end ``` + ## Phase 2: Database Integration (Week 6+) When adding PostgreSQL persistence: From 247123afdd6d1da51dfce4302e59b2087422182d Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:09:24 +0300 Subject: [PATCH 20/27] adjust 'versions' section to 'variants' --- .../architecture/04-environment-schema.md | 87 ++++++++++++------- 1 file changed, 57 insertions(+), 30 deletions(-) diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md index a6c7d0df6dc16..be3c38fff9799 100644 --- a/docs/test_env/architecture/04-environment-schema.md +++ b/docs/test_env/architecture/04-environment-schema.md @@ -84,24 +84,35 @@ This gives maximum reusability while allowing precise per-module customization. | `shared` | Hash | Yes | Base configuration inherited by all profiles | | `profiles` | Hash | Yes | Map of profile names to profile-specific overrides | -### versions Section +### variants Section -Each version is a key-value pair: -- **Key**: Version string (e.g., `"2.361"`) -- **Value**: Hash with version-specific configuration +Each variant is a key-value pair: +- **Key**: Variant identifier (arbitrary string, e.g., `"2.361"`, `"2.361-postgres"`, `"latest"`) +- **Value**: Hash with variant-specific configuration | Key | Type | Required | Description | |-----|------|----------|-------------| | `image` | String | Yes | OCI image reference | +| `version` | String | No | Actual software version (informational, e.g., `"2.361"`) | | `build_args` | Hash | No | Docker build arguments | Example: ```yaml -versions: +variants: "2.361": image: vulnhub/jenkins:2.361 + version: "2.361" build_args: JENKINS_VERSION: "2.361" + "2.361-postgres": + image: vulnhub/jenkins:2.361-postgres + version: "2.361" + build_args: + JENKINS_VERSION: "2.361" + DB_BACKEND: "postgresql" + "2.375": + image: vulnhub/jenkins:2.375 + version: "2.375" ``` ### shared Section @@ -199,8 +210,8 @@ Each profile is a key-value pair: ## Validation Rules 1. `name` must match filename (without `.yml`) -2. `versions` must have at least one entry -3. Each version must have an `image` +2. `variants` must have at least one entry +3. Each variant must have an `image` 4. `shared.ports` must have at least one entry 5. `profiles` must have at least one entry 6. `profiles` must contain a `default` profile @@ -218,7 +229,7 @@ When `test_env build` resolves an environment definition, the loader performs a |-------|--------|------------------| | 1 | `shared` | Base configuration inherited by all profiles | | 2 | `profiles[profile_name]` | Profile-specific overrides (minus `description`) | -| 3 | `VulnEnv['overrides']` | Module-level tweaks | +| 3 | `VulnerableEnvironment['overrides']` | Module-level tweaks | ### Merge Rules @@ -229,19 +240,19 @@ When `test_env build` resolves an environment definition, the loader performs a ### Resolution Steps 1. Load the YAML definition file by `name`. -2. Validate that `versions` contains the requested `version`. +2. Validate that `variants` contains the requested `variant`. 3. Validate that `profiles` contains the requested `profile` (default: `'default'`). 4. Start with a copy of `shared`. 5. Deep-merge the selected profile's configuration into it. -6. Deep-merge the module's `VulnEnv['overrides']` (if any). -7. Attach the version-specific `image` and `build_args` from `versions[version]`. +6. Deep-merge the module's `VulnerableEnvironment['overrides']` (if any). +7. Attach the variant-specific `image`, `version`, and `build_args` from `variants[variant]`. ### Error Cases | Condition | Error | |-----------|-------| | Definition file not found | `"Definition not found: data/vuln_envs/{name}.yml"` | -| Version not in `versions` | `"Version '{version}' not defined for '{name}'"` | +| Variant not in `variants` | `"Variant '{variant}' not defined for '{name}'"` | | Profile not in `profiles` | `"Profile '{profile}' not defined for '{name}'"` | | No `default` profile exists | Validation fails at load time | @@ -254,30 +265,37 @@ The `EnvironmentDefinitionLoader` exposes: - `available_definitions` — List all `.yml` files in `data/vuln_envs/`. ## Module Metadata Integration -Modules reference a definition and optionally a profile. See [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/02-module-metadata.md) for the full `VulnEnv` schema. +Modules reference a definition and optionally a profile. See [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/02-module-metadata.md) for the full `VulnerableEnvironment` schema. ```ruby -# Standard module — uses default profile -'VulnEnv' => { - 'definition' => 'jenkins', - 'default_version' => '2.361', - 'port_mapping' => { 8080 => 'RPORT' } +# Standard module — uses default variant and profile +'VulnerableEnvironment' => { + 'definition' => 'jenkins', + 'default_variant' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' } } -# Variant module — uses http-stopped profile -'VulnEnv' => { - 'definition' => 'jenkins', - 'profile' => 'http-stopped', - 'default_version' => '2.361', - 'port_mapping' => { 8080 => 'RPORT' } +# Variant module — uses postgres variant +'VulnerableEnvironment' => { + 'definition' => 'jenkins', + 'default_variant' => '2.361-postgres', + 'port_mapping' => { 8080 => 'RPORT' } +} + +# Module with profile override +'VulnerableEnvironment' => { + 'definition' => 'jenkins', + 'default_variant' => '2.361', + 'profile' => 'http-stopped', + 'port_mapping' => { 8080 => 'RPORT' } } # Module with minor override — same profile, different health check -'VulnEnv' => { - 'definition' => 'jenkins', - 'default_version' => '2.361', - 'port_mapping' => { 8080 => 'RPORT' }, - 'overrides' => { +'VulnerableEnvironment' => { + 'definition' => 'jenkins', + 'default_variant' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' }, + 'overrides' => { 'health_check' => { 'path' => '/script', 'expected_status' => 403 @@ -295,16 +313,25 @@ Modules reference a definition and optionally a profile. See [02-module-metadata name: jenkins description: Jenkins CI server with Groovy Script Console -versions: +variants: "2.361": image: vulnhub/jenkins:2.361 + version: "2.361" + build_args: + JENKINS_VERSION: "2.361" + "2.361-postgres": + image: vulnhub/jenkins:2.361-postgres + version: "2.361" build_args: JENKINS_VERSION: "2.361" + DB_BACKEND: "postgresql" "2.375": image: vulnhub/jenkins:2.375 + version: "2.375" build_args: JENKINS_VERSION: "2.375" + shared: ports: http: 8080 From 389a2e1282aa80121a43dc0d95b4e9a2b1b4359c Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 10 Jul 2026 13:20:54 +0300 Subject: [PATCH 21/27] Change default_version to default_variant in metadata --- .../architecture/02-module-metadata.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/test_env/architecture/02-module-metadata.md b/docs/test_env/architecture/02-module-metadata.md index abb8e4bbbc9e0..ba11e51db9ef3 100644 --- a/docs/test_env/architecture/02-module-metadata.md +++ b/docs/test_env/architecture/02-module-metadata.md @@ -44,15 +44,15 @@ module Msf class Plugin::VulnEnv < Msf::Plugin # Encapsulates and validates VulnerableEnvironment metadata from a module. class VulnerableEnvironment - attr_reader :definition, :default_version, :profile, :port_mapping, :overrides + attr_reader :definition, :default_variant, :profile, :port_mapping, :overrides # Required keys that must be present - REQUIRED_KEYS = %w[definition default_version port_mapping].freeze + REQUIRED_KEYS = %w[definition default_variant port_mapping].freeze # Valid types for each key SCHEMA = { 'definition' => String, - 'default_version' => String, + 'default_variant' => String, 'profile' => String, 'port_mapping' => Hash, 'overrides' => Hash @@ -64,7 +64,7 @@ module Msf validate! @definition = @raw['definition'] - @default_version = @raw['default_version'] + @default_variant = @raw['default_variant'] @profile = @raw['profile'] || 'default' @port_mapping = @raw['port_mapping'] || {} @overrides = @raw['overrides'] || {} @@ -149,7 +149,7 @@ class Plugin::VulnEnv < Msf::Plugin end # env.definition => 'jenkins' - # env.default_version => '2.361' + # env.default_variant => '2.361' # env.profile => 'default' # env.port_mapping => {8080 => 'RPORT'} # env.overrides => { ... } @@ -226,7 +226,7 @@ puts "" puts "=== Adding custom key (runtime injection test) ===" info['VulnerableEnvironment'] = { 'definition' => 'jenkins', - 'default_version' => '2.361', + 'default_variant' => '2.361', 'port_mapping' => { 8080 => 'RPORT' } } @@ -257,12 +257,12 @@ Name: Jenkins-CI Script-Console Java Execution === Adding custom key (runtime injection test) === Added VulnerableEnvironment Has VulnerableEnvironment? true -VulnerableEnvironment: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} +VulnerableEnvironment: {"definition"=>"jenkins", "default_variant"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} === Reading back === Same object? true Has VulnerableEnvironment? true -VulnerableEnvironment: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} +VulnerableEnvironment: {"definition"=>"jenkins", "default_variant"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} ``` ### What This Proves @@ -323,14 +323,14 @@ if nil: print_error("Module does not define a vulnerable environment configurati ↓ if present: env.definition => 'jenkins' - env.default_version => '2.361' + env.default_variant => '2.361' env.profile => 'default' env.port_mapping => {8080 => 'RPORT'} env.overrides => { ... } ↓ yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{env.definition}.yml") definition_data = YAML.load_file(yaml_path) - env_config = loader.resolve(env.definition, env.default_version, env.profile, env.overrides) + env_config = loader.resolve(env.definition, env.default_variant, env.profile, env.overrides) ``` ## Error Cases @@ -344,5 +344,5 @@ if present: | Invalid port_mapping key | "port_mapping key must be an integer port, got: 'abc'" | | Invalid port_mapping value | "port_mapping value must be a String datastore option name, got: 123" | | Definition file not found | "Environment definition not found: data/vuln_envs/{name}.yml" | -| Version not found in definition | "Version '{version}' not defined for '{name}'" | +| Version not found in definition | "Variant '{variant}' not defined for '{name}'" | | Profile not found in definition | "Profile '{profile}' not defined for '{name}'" | From b0ac0db395128ed0ac966d6fd24d86c170863187 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:30:09 +0300 Subject: [PATCH 22/27] Update doc for consistency --- docs/test_env/workflow.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/test_env/workflow.md b/docs/test_env/workflow.md index fd04ab6163fff..ef34558e6c913 100644 --- a/docs/test_env/workflow.md +++ b/docs/test_env/workflow.md @@ -15,7 +15,7 @@ msf exploit(apache_activemq_jolokia_rce) > test_env build ``` **Expected behavior:** -- The plugin detects the active module and reads `mod.info['VulnEnv']` +- The plugin detects the active module and reads `mod.info['VulnerableEnvironment']` - It resolves the definition name (`activemq`) and loads `data/vuln_envs/activemq.yml` - It selects the default version (`5.18.6`) - It auto-detects the container runtime (Docker preferred, Podman fallback) @@ -52,7 +52,7 @@ msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 ``` **Expected behavior:** -- The `VERSION=2.375` argument overrides the `default_version` from the module's `VulnEnv` +- The `VERSION=2.375` argument overrides the `default_variant` from the module's `VulnerableEnvironment` - The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` - If the version does not exist, the command fails immediately with a list of available versions @@ -261,7 +261,7 @@ msf exploit(multi/http/jenkins_script_console) > test_env remove-all | Error Condition | Expected Output | Implementation Notes | |-----------------|----------------|---------------------| | No active module | `[-] No active module. Use 'use ' first.` | Check `driver.active_module` before any other logic | -| Module has no `VulnEnv` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnEnv']` after resolving active module | +| Module has no `VulnerableEnvironment` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnerableEnvironment']` after resolving active module | | No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | | Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | | Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | @@ -278,3 +278,4 @@ msf exploit(multi/http/jenkins_script_console) > test_env remove-all - Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` - The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) - Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost +- The `VulnerableEnvironment` key is the canonical metadata key. No abbreviated form (`VulnEnv`) is accepted. From 72976c380d7a66c54040b048f5e623e604a1030f Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:35:48 +0300 Subject: [PATCH 23/27] minor fix --- docs/test_env/architecture/04-environment-schema.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md index be3c38fff9799..314f671f294f9 100644 --- a/docs/test_env/architecture/04-environment-schema.md +++ b/docs/test_env/architecture/04-environment-schema.md @@ -80,7 +80,7 @@ This gives maximum reusability while allowing precise per-module customization. |-----|------|----------|-------------| | `name` | String | Yes | Machine-friendly identifier (matches filename) | | `description` | String | Yes | Human-readable description | -| `versions` | Hash | Yes | Map of version strings to image configurations | +| `variants` | Hash | Yes | Map of variant strings to image configurations | | `shared` | Hash | Yes | Base configuration inherited by all profiles | | `profiles` | Hash | Yes | Map of profile names to profile-specific overrides | From 3c5c75c1604c58d1100bc86312166a8b4b73f1a7 Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:45:13 +0300 Subject: [PATCH 24/27] minor note --- docs/test_env/workflow.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/test_env/workflow.md b/docs/test_env/workflow.md index ef34558e6c913..f35ede8f38f72 100644 --- a/docs/test_env/workflow.md +++ b/docs/test_env/workflow.md @@ -146,6 +146,9 @@ msf exploit(multi/http/jenkins_script_console) > test_env exec 1 [*] Set RHOSTS 127.0.0.1 [*] Set RPORT 49153 [*] Set TARGETURI /script +[*] Set PAYLOAD java/meterpreter/reverse_tcp +[*] Set LHOST 127.0.0.1 +[*] Set LPORT 4444 [*] Started reverse TCP handler on 127.0.0.1:4444 [+] Session 1 opened (127.0.0.1:4444 -> 127.0.0.1:49153) ``` @@ -278,4 +281,5 @@ msf exploit(multi/http/jenkins_script_console) > test_env remove-all - Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` - The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) - Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost +- `test_env build` must detect if the active module requires a payload. If so, it auto-selects a compatible default payload and sets `LHOST` to `127.0.0.1` with an available `LPORT`. These values are stored in the registry alongside the environment metadata. `test_env exec` then applies the complete stored datastore (including payload options) before running the exploit. - The `VulnerableEnvironment` key is the canonical metadata key. No abbreviated form (`VulnEnv`) is accepted. From 77452aa221fdd716986fcfe06ea77eaec75bbc2e Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:16 +0300 Subject: [PATCH 25/27] Refactor variants section and update validation rules --- .../architecture/04-environment-schema.md | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md index 314f671f294f9..d49d78dd3102e 100644 --- a/docs/test_env/architecture/04-environment-schema.md +++ b/docs/test_env/architecture/04-environment-schema.md @@ -86,35 +86,41 @@ This gives maximum reusability while allowing precise per-module customization. ### variants Section -Each variant is a key-value pair: -- **Key**: Variant identifier (arbitrary string, e.g., `"2.361"`, `"2.361-postgres"`, `"latest"`) -- **Value**: Hash with variant-specific configuration +A list of configuration variants for this service. Each variant is a distinct +runnable configuration — typically a software version, but may also represent +different backends, plugins, or build options for the same version. | Key | Type | Required | Description | |-----|------|----------|-------------| +| `name` | String | Yes | Unique identifier for this variant. Used in `test_env build VARIANT=...` | +| `version` | String | No | The actual software version. For information and future validation (e.g., Rapid7#21583) | | `image` | String | Yes | OCI image reference | -| `version` | String | No | Actual software version (informational, e.g., `"2.361"`) | | `build_args` | Hash | No | Docker build arguments | +| `default` | Boolean | No | If `true`, this variant is selected when no `VARIANT` is specified. Only one variant may be `default` | Example: ```yaml variants: - "2.361": - image: vulnhub/jenkins:2.361 + - name: "2.361" version: "2.361" + image: vulnhub/jenkins:2.361 build_args: JENKINS_VERSION: "2.361" - "2.361-postgres": - image: vulnhub/jenkins:2.361-postgres + default: true + + - name: "2.361-postgres" version: "2.361" + image: vulnhub/jenkins:2.361-pg build_args: JENKINS_VERSION: "2.361" DB_BACKEND: "postgresql" - "2.375": - image: vulnhub/jenkins:2.375 + + - name: "2.375" version: "2.375" + image: vulnhub/jenkins:2.375 + build_args: + JENKINS_VERSION: "2.375" ``` - ### shared Section Base configuration inherited by all profiles. Any field here can be overridden by a profile or by module-level metadata. @@ -210,14 +216,16 @@ Each profile is a key-value pair: ## Validation Rules 1. `name` must match filename (without `.yml`) -2. `variants` must have at least one entry -3. Each variant must have an `image` -4. `shared.ports` must have at least one entry -5. `profiles` must have at least one entry -6. `profiles` must contain a `default` profile -7. Profile names must match `[a-z0-9-]+` -8. `health_check` must be defined in `shared` or in every profile -9. Module-level `overrides` are deep-merged into the final profile config +2. `variants` must be a non-empty list +3. Each variant must have a `name` and `image` +4. Variant `name` must be unique across all variants +5. At most one variant may have `default: true` +6. `shared.ports` must have at least one entry +7. `profiles` must have at least one entry +8. `profiles` must contain a `default` profile +9. Profile names must match `[a-z0-9-]+` +10. `health_check` must be defined in `shared` or in every profile +11. Module-level `overrides` are deep-merged into the final profile config ## Resolution & Merge Logic @@ -314,24 +322,26 @@ name: jenkins description: Jenkins CI server with Groovy Script Console variants: - "2.361": - image: vulnhub/jenkins:2.361 + - name: "2.361" version: "2.361" + image: vulnhub/jenkins:2.361 build_args: JENKINS_VERSION: "2.361" - "2.361-postgres": - image: vulnhub/jenkins:2.361-postgres + default: true + + - name: "2.361-postgres" version: "2.361" + image: vulnhub/jenkins:2.361-pg build_args: JENKINS_VERSION: "2.361" DB_BACKEND: "postgresql" - "2.375": - image: vulnhub/jenkins:2.375 + + - name: "2.375" version: "2.375" + image: vulnhub/jenkins:2.375 build_args: JENKINS_VERSION: "2.375" - shared: ports: http: 8080 From f031093a3e682d86ac636ba3519c976029a6bf93 Mon Sep 17 00:00:00 2001 From: Nayeraneru Date: Sun, 12 Jul 2026 04:43:50 +0300 Subject: [PATCH 26/27] fixes --- .../architecture/02-module-metadata.md | 4 +- .../architecture/04-environment-schema.md | 84 +------------------ docs/test_env/workflow.md | 11 +-- 3 files changed, 12 insertions(+), 87 deletions(-) diff --git a/docs/test_env/architecture/02-module-metadata.md b/docs/test_env/architecture/02-module-metadata.md index ba11e51db9ef3..f0a27d72c7b8d 100644 --- a/docs/test_env/architecture/02-module-metadata.md +++ b/docs/test_env/architecture/02-module-metadata.md @@ -322,7 +322,7 @@ vulnerable_environment(mod) → VulnerableEnvironment Struct or nil if nil: print_error("Module does not define a vulnerable environment configuration.") ↓ if present: - env.definition => 'jenkins' + env.definition => 'jenkins' # maps to variant 'name' in the YAML list env.default_variant => '2.361' env.profile => 'default' env.port_mapping => {8080 => 'RPORT'} @@ -344,5 +344,5 @@ if present: | Invalid port_mapping key | "port_mapping key must be an integer port, got: 'abc'" | | Invalid port_mapping value | "port_mapping value must be a String datastore option name, got: 123" | | Definition file not found | "Environment definition not found: data/vuln_envs/{name}.yml" | -| Version not found in definition | "Variant '{variant}' not defined for '{name}'" | +| Variant not found in definition | "Variant '{variant}' not defined for '{name}'" | | Profile not found in definition | "Profile '{profile}' not defined for '{name}'" | diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md index d49d78dd3102e..e947f4a553d64 100644 --- a/docs/test_env/architecture/04-environment-schema.md +++ b/docs/test_env/architecture/04-environment-schema.md @@ -80,7 +80,7 @@ This gives maximum reusability while allowing precise per-module customization. |-----|------|----------|-------------| | `name` | String | Yes | Machine-friendly identifier (matches filename) | | `description` | String | Yes | Human-readable description | -| `variants` | Hash | Yes | Map of variant strings to image configurations | +| `variants` | Array | Yes | List of variant configurations | | `shared` | Hash | Yes | Base configuration inherited by all profiles | | `profiles` | Hash | Yes | Map of profile names to profile-specific overrides | @@ -93,7 +93,7 @@ different backends, plugins, or build options for the same version. | Key | Type | Required | Description | |-----|------|----------|-------------| | `name` | String | Yes | Unique identifier for this variant. Used in `test_env build VARIANT=...` | -| `version` | String | No | The actual software version. For information and future validation (e.g., Rapid7#21583) | +| `version` | String | Yes | The actual software version. For information and future validation (e.g., Rapid7#21583) | | `image` | String | Yes | OCI image reference | | `build_args` | Hash | No | Docker build arguments | | `default` | Boolean | No | If `true`, this variant is selected when no `VARIANT` is specified. Only one variant may be `default` | @@ -253,7 +253,7 @@ When `test_env build` resolves an environment definition, the loader performs a 4. Start with a copy of `shared`. 5. Deep-merge the selected profile's configuration into it. 6. Deep-merge the module's `VulnerableEnvironment['overrides']` (if any). -7. Attach the variant-specific `image`, `version`, and `build_args` from `variants[variant]`. +7. Attach the variant-specific `image`, `version`, and `build_args` from the matching variant in the `variants` list. ### Error Cases @@ -269,7 +269,7 @@ When `test_env build` resolves an environment definition, the loader performs a The `EnvironmentDefinitionLoader` exposes: - `load(name)` — Parse and validate a definition file. -- `resolve(name, version, profile, overrides)` — Return the fully merged configuration for a specific environment instance. +- `resolve(name, variant, profile, overrides)` — Return the fully merged configuration for a specific environment instance. - `available_definitions` — List all `.yml` files in `data/vuln_envs/`. ## Module Metadata Integration @@ -315,82 +315,6 @@ Modules reference a definition and optionally a profile. See [02-module-metadata } ``` -## Reference Implementation: jenkins.yml - -```yaml -name: jenkins -description: Jenkins CI server with Groovy Script Console - -variants: - - name: "2.361" - version: "2.361" - image: vulnhub/jenkins:2.361 - build_args: - JENKINS_VERSION: "2.361" - default: true - - - name: "2.361-postgres" - version: "2.361" - image: vulnhub/jenkins:2.361-pg - build_args: - JENKINS_VERSION: "2.361" - DB_BACKEND: "postgresql" - - - name: "2.375" - version: "2.375" - image: vulnhub/jenkins:2.375 - build_args: - JENKINS_VERSION: "2.375" - -shared: - ports: - http: 8080 - - volumes: - jenkins_home: - container_path: /var/jenkins_home - persist: false - - credentials: - default: - username: admin - password: admin - - datastore_defaults: - TARGETURI: /script - - ci: - exploit: - payload: java/meterpreter/reverse_tcp - options: - LHOST: 127.0.0.1 - LPORT: 4444 - validation: - expected_session: true - session_type: meterpreter - expected_output: "uid=" - timeout: 120 - -profiles: - default: - description: Standard Jenkins with HTTP enabled - health_check: - type: http - path: /login - expected_status: 200 - interval: 5 - timeout: 2 - retries: 12 - - http-stopped: - description: Jenkins with HTTP stopped for config-drop exploit - health_check: - type: tcp - port: 8080 - interval: 5 - timeout: 2 - retries: 12 -``` ## Integration With Registry diff --git a/docs/test_env/workflow.md b/docs/test_env/workflow.md index f35ede8f38f72..6c42984705751 100644 --- a/docs/test_env/workflow.md +++ b/docs/test_env/workflow.md @@ -48,12 +48,12 @@ msf exploit(apache_activemq_jolokia_rce) > test_env build **Input:** ``` -msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 +msf exploit(multi/http/jenkins_script_console) > test_env build VARIANT=2.375 ``` **Expected behavior:** -- The `VERSION=2.375` argument overrides the `default_variant` from the module's `VulnerableEnvironment` -- The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` +- The `VARIANT=2.375` argument overrides the `default_variant` from the module's `VulnerableEnvironment` +- The plugin loads `jenkins.yml` and selects the variant named `2.375` from the `variants` list - If the version does not exist, the command fails immediately with a list of available versions **Expected output (success):** @@ -71,7 +71,7 @@ msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 **Expected output (failure — version not found):** ``` -[-] Version '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 +[-] Variant '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 ``` --- @@ -248,7 +248,7 @@ msf exploit(multi/http/jenkins_script_console) > test_env remove-all | Command | Arguments | Description | |---------|-----------|-------------| -| `test_env build` | `[VERSION=x]` `[RPORT=y]` | Build and launch environment for active module | +| `test_env build` | `[VARIANT=x]` `[RPORT=y]` | Build and launch environment for active module | | `test_env list` | none | Show all tracked environments | | `test_env exec` | `` | Execute exploit against environment `` | | `test_env stop` | `` | Stop a running environment | @@ -268,6 +268,7 @@ msf exploit(multi/http/jenkins_script_console) > test_env remove-all | No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | | Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | | Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | +| Version not found in definition | `[-] Variant '{variant}' not defined for '{name}'` | Loader validates against `variants` list | | No available ports | `[-] No available ports in range 49152-65535` | `PortAllocator` raises after exhausting range | | Health check timeout | `[-] Health check timed out after seconds` | `HealthManager` exceeds `retries * interval` | | Environment ID not found | `[-] Environment not found` | Registry lookup returns `nil` | From 3b4be6d40421bd79ab7bf4b3109f57893529d96a Mon Sep 17 00:00:00 2001 From: Nayera <115358236+Nayeraneru@users.noreply.github.com> Date: Sun, 12 Jul 2026 04:46:51 +0300 Subject: [PATCH 27/27] Add output for 'variants' in environment schema (jenkins.yml week3 file) --- docs/test_env/architecture/04-environment-schema.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md index e947f4a553d64..8e9dceeee5199 100644 --- a/docs/test_env/architecture/04-environment-schema.md +++ b/docs/test_env/architecture/04-environment-schema.md @@ -5,11 +5,11 @@ I created `data/vuln_envs/jenkins.yml` and validated it with Ruby: ```bash -ruby -e " + ruby -e " require 'yaml' data = YAML.safe_load(File.read('data/vuln_envs/jenkins.yml'), permitted_classes: [Symbol]) puts 'Name: ' + data['name'] -puts 'Versions: ' + data['versions'].keys.inspect +puts 'Variants: ' + data['variants'].map { |v| v['name'] }.inspect # ← NEW: 'variants' list, .map puts 'Ports: ' + data['shared']['ports'].inspect puts 'Health check type: ' + data['shared']['health_check']['type'] " @@ -18,7 +18,7 @@ puts 'Health check type: ' + data['shared']['health_check']['type'] Output: ``` Name: jenkins -Versions: ["2.361", "2.375"] +Variants: ["2.361", "2.375"] Ports: {"http"=>8080} Health check type: http ```