Files
Z. Cliffe Schreuders 19db2f530d Refactor whitespace and comments for consistency in BreakEscape controllers and models
- Cleaned up unnecessary whitespace in `games_controller.rb`, `missions_controller.rb`, `game.rb`, `mission.rb`, `routes.rb`, `seeds.rb`, and migration files to enhance code readability.
- Standardized comment formatting across various files to maintain consistency and improve clarity.
2025-11-30 00:06:54 +00:00

181 lines
6.0 KiB
Ruby

module BreakEscape
class Mission < ApplicationRecord
self.table_name = 'break_escape_missions'
has_many :games, class_name: 'BreakEscape::Game', dependent: :destroy
# CyBOK associations - always use our table for standalone mode
has_many :break_escape_cyboks,
class_name: 'BreakEscape::Cybok',
as: :cybokable,
dependent: :destroy
validates :name, presence: true, uniqueness: true
validates :display_name, presence: true
validates :difficulty_level, inclusion: { in: 1..5 }
scope :published, -> { where(published: true) }
scope :by_collection, ->(collection) { where(collection: collection) }
# Get all distinct collections
def self.collections
distinct.pluck(:collection).compact.sort
end
# Path to scenario directory
def scenario_path
BreakEscape::Engine.root.join('scenarios', name)
end
# Path to mission metadata file
def mission_json_path
scenario_path.join('mission.json')
end
# Check if mission.json exists
def has_mission_json?
File.exist?(mission_json_path)
end
# Load mission metadata from JSON file
def load_mission_metadata
return nil unless has_mission_json?
JSON.parse(File.read(mission_json_path))
rescue JSON::ParserError => e
Rails.logger.error "Invalid mission.json for #{name}: #{e.message}"
nil
end
# Get all CyBOK entries (uses Hacktivity's if available for reads)
def all_cyboks
if defined?(::Cybok) && respond_to?(:cyboks)
cyboks
else
break_escape_cyboks
end
end
# Check if Hacktivity mode is available (VMs and flag service)
def self.hacktivity_mode?
defined?(::VmSet) && defined?(::FlagService)
end
# Check if mission requires VMs (has secgen_scenario configured)
def requires_vms?
secgen_scenario.present?
end
# Get valid VM sets for this mission (Hacktivity mode only)
#
# HACKTIVITY COMPATIBILITY NOTES:
# - Hacktivity uses `sec_gen_batch` (with underscore), not `secgen_batch`
# - The `scenario` field contains the XML path (e.g., "scenarios/ctf/foo.xml")
# - VmSet doesn't have a `display_name` method - use sec_gen_batch.title instead
# - Always eager-load :vms and :sec_gen_batch to avoid N+1 queries
def valid_vm_sets_for_user(user)
return [] unless self.class.hacktivity_mode? && requires_vms?
# Query Hacktivity's vm_sets where:
# - scenario matches our secgen_scenario
# - user owns it (or is on the team)
# - not relinquished
# - build completed successfully
::VmSet.joins(:sec_gen_batch)
.where(sec_gen_batches: { scenario: secgen_scenario })
.where(user: user, relinquished: false)
.where.not(build_status: ['pending', 'error'])
.includes(:vms, :sec_gen_batch)
.order(created_at: :desc)
end
# Generate scenario data via ERB with optional VM context
def generate_scenario_data(vm_context = {})
template_path = scenario_path.join('scenario.json.erb')
raise "Scenario template not found: #{name}" unless File.exist?(template_path)
erb = ERB.new(File.read(template_path))
binding_context = ScenarioBinding.new(vm_context)
output = erb.result(binding_context.get_binding)
JSON.parse(output)
rescue JSON::ParserError => e
raise "Invalid JSON in #{name} after ERB processing: #{e.message}"
end
# Parse SecGen flag_hints.xml to extract flags per VM
# Returns: { "desktop" => ["flag{abc}", "flag{def}"], "kali" => [] }
def self.parse_flag_hints_xml(xml_content)
return {} if xml_content.blank?
require 'nokogiri'
doc = Nokogiri::XML(xml_content)
# Remove namespaces for simpler XPath queries
# SecGen uses xmlns="http://www.github/cliffe/SecGen/marker"
doc.remove_namespaces!
flags_by_vm = {}
doc.xpath('//system').each do |system|
system_name = system.at_xpath('system_name')&.text
next unless system_name.present?
flags = system.xpath('challenge/flag').map(&:text).compact
flags_by_vm[system_name] = flags
end
flags_by_vm
rescue Nokogiri::XML::SyntaxError => e
Rails.logger.error "[BreakEscape] Invalid flag_hints.xml: #{e.message}"
{}
end
# Binding context for ERB variables
class ScenarioBinding
def initialize(vm_context = {})
@random_password = SecureRandom.alphanumeric(8)
@random_pin = rand(1000..9999).to_s
@random_code = SecureRandom.hex(4)
@vm_context = vm_context # VM/flag data for CTF integration
end
attr_reader :random_password, :random_pin, :random_code, :vm_context
# Get a VM from the context by title, or return a fallback VM object
# In Hacktivity mode: returns VM data from the VmSet
# In standalone mode: uses fallback but overrides IP from vm_ips if available
# Usage in ERB:
# "vm": <%= vm_object('kali', {"id":1,"title":"kali","ip":"192.168.1.10","enable_console":true}) %>
def vm_object(title, fallback = {})
if vm_context && vm_context['hacktivity_mode'] && vm_context['vms']
vm = vm_context['vms'].find { |v| v['title'] == title }
return vm.to_json if vm
end
# Standalone mode: use fallback, but override IP from vm_ips if available
result = fallback.dup
if vm_context && vm_context['vm_ips'] && vm_context['vm_ips'][title]
result['ip'] = vm_context['vm_ips'][title]
end
result.to_json
end
# Get flags for a specific VM from the context
# Usage in ERB:
# "flags": <%= flags_for_vm('desktop', ['flag{test1}', 'flag{test2}']) %>
def flags_for_vm(vm_name, fallback = [])
if vm_context && vm_context['flags_by_vm']
flags = vm_context['flags_by_vm'][vm_name]
return flags.to_json if flags
end
fallback.to_json
end
def get_binding
binding
end
end
end
end